Pull to refresh

Hiqus — HIerarhical QUery String

Reading time10 min
Views2.8K
Это формат представления древовидных структур данных в виде одной строки в удобном для человека виде. Является обобщением формата «application/x-www-form-urlencoded» и как следствие — обратно совместим с ним. В основе Hiqus лежит всё тот же принцип представления данных в виде пар «ключ-значение» с той лишь разницей, что ключ может быть составным или пустым.

Данный формат уже используется такими монстрами как Яндекс (http://yandex.ru/yandsearch?date=within&text=hiqus&from_day=28&from_month=4&from_year=2009), Гугл (http://www.google.ru/search?as_q=hiqus&hl=ru&num=10&as_qdr=all) и многими другими, кому требуется передавать иерархические данные в строке запроса. Исключение составляют PHP-сайты, для которых традиционно используется свой, не слишком наглядный формат (пример, навскидку не нашёл, но выглядит он примерно так: ?user%5Bid%5D=123&user%5Bname%5D=Nick).

Описание

Hiqus-строка состоит из 1 и более кусочков, разделённых между собой разделителем, в роли которого может выступать знакомый нам из обычных query string амперсанд, из традиционных ЧПУ — слэш, из коммандной строки — пробел, из cookie string — точка-с-запятой, ну и до кучи — вертикальная чёрточка ;-)

Каждый «кусочек» состоит из собственно значения и идущим перед ним путём. Путь может быть пустым. После каждого элемента пути должен идти специальный разделитель, которым может быть знакомый нам из обычных query string знак равенства, из JSON — двоеточие, и удобный в использовании в HTML-формах — знак подчёркивания (ибо не кодируется при передаче).

Значение и элементы пути должны быть закодированы процентом в соответствии со спецификацией URI. Впрочем, парсеру должны быть важны лишь упомянутые специальные символы. Экранировать ли остальные — зависит от того, где и как будет использоваться hiqus-строка.

Элемент пути может быть пустой строкой, тогда вместо него будет использоваться автоматически генерируемое число. Например, «a==1» эквивалентно «a=0=1» при парсинге.

Если в пути всего 1 элемент, да и тот пустой, то положенный после него разделитель может опускаться. Например, "=blogs/=webstandards" эквивалентно «blogs/webstandards».

«Кусочки» добавляются в целевое дерево последовательно, что позволяет последующим затирать значения и даже поддеревья предыдущих. Например, «a=b=1/a=1» эквивалентно «a=1».

Примеры

Пусть у нас есть такой чпу: /blogs/webstandards/92300/
Если его распарсить как hiqus, то получится объект с таким JSON представлением: { '0': 'blogs', '1': 'webstandarts', '2': '92300' }

Пусть мы отправили форму и получили такую query string: q=hiqus&target_type=blogs&target_subtype=offtopic
Результат парсинга будет примерно такой: { q: 'hiqus', target: { type: 'blogs', subtype: 'offtopic' } }

Пусть мы вызвали наш скрипт из консоли передав ему такие параметры: list users sex=female limit=100
После парсинга получим: { '0': 'list', '1': 'users', 'sex': 'female', limit: '100' }

Пусть нам пришли такие печеньки: sid=1234;login=tenshi
Распарсим и получим: { sid: '1234', login: 'tenshi' }

Эталонная реализация на яваскрипте

var Hiqus= new function(){<br><br>Version: 1<br>Description: "parse, serialize and merge any 'HIerarhical QUery String'"<br>License: 'public domain'<br><br>Implementation:<br><br>var sepHiqusList= '/|; &'<br>var sepPathList= '=:_'<br><br>var sepHiqusDefault= sepHiqusList.charAt(0)<br>var sepPathDefault= sepPathList.charAt(0)<br><br>var sepHiqusRegexp= RegExp( '[' + sepHiqusList + ']', 'g' )<br>var sepPathRegexp= RegExp( '[' + sepPathList + ']', 'g' )<br><br>var encode= function( str ){<br>    return encodeURIComponent( str ).split( '_' ).join( '%5F' )<br>}<br>var decode= function( str ){<br>    return decodeURIComponent( str )<br>}<br><br>var splitHiqus= function( str ){<br>    return str.split( sepHiqusRegexp )<br>}<br>var splitPath= function( str ){<br>    var path= String( str ).split( sepPathRegexp )<br>    for( var j= path.length - 1; j >= 0; --j ) path[ j ]= decode( path[ j ] )<br>    return path<br>}<br><br>var get= function( ){<br>    var obj= this<br>    for( var i= 0, len= arguments.length; i < len; ++i ){<br>        var name= arguments[ i ]<br>        if( !name ) throw new Error( 'name is empty' )<br>        obj= obj[ name ]<br>        if( typeof obj !== 'object' ) return obj<br>    }<br>    return obj<br>}<br><br>var placin= function( ){<br>    var obj= this<br>    for( var i= 0, len= arguments.length; i < len; ++i ){<br>        var name= arguments[ i ]<br>        if( name && Number( name ) != name ){<br>            var o= obj[ name ]<br>            if(( !o )||( typeof o !== 'object' )) o= obj[ name ]= []<br>        } else {<br>            var o= []<br>            obj.push( o )<br>        }<br>        obj= o<br>    }<br>    return obj<br>}<br><br>var put= function( ){<br>    var obj= this<br>    var len= arguments.length<br>    if( !len ) return this<br>    var val= arguments[ len - 1 ]<br>    var key= arguments[ len - 2 ]<br>    if( len > 2 ){<br>        var path= []<br>        for( var i= len - 2; i-- ;) path[ i ]= arguments[ i ]<br>        obj= placin.apply( obj, path )<br>    }<br>    if( typeof val === 'object' ){<br>        var v= ( len === 1 ) ? obj : placin.call( obj, key )<br>        for( var i in val ) if( val.hasOwnProperty( i ) ) put.call( v, i, val[ i ] )<br>    } else {<br>        val= String( val )<br>        if( !key || Number( key ) == key ) obj.push( val )<br>        else obj[ key ]= val<br>    }<br>    return this<br>}<br><br>var parsin= function( str ){ <br>    var chunks= splitHiqus( str )<br>    for( var i= 0, len= chunks.length; i < len; ++i ){<br>        var path= chunks[i]<br>        if( !path ) continue<br>        path= splitPath( path )<br>        put.apply( this, path )<br>    }<br>      return this<br>}<br><br>var serialize= function( prefix, obj ){<br>    if( !obj ) return ''<br>    if( typeof obj !== 'object' ){<br>        if( prefix === sepPathDefault ) prefix= ''<br>        return prefix+= encode( obj )<br>    }<br>    var list= []<br>    for( var key in obj ) if( obj.hasOwnProperty( key ) ){<br>        var k= ( Number( key ) == key ) ? '' : encode( key )<br>        var chunk= serialize( prefix + k + sepPathDefault, obj[ key ] )<br>        if( chunk ) list.push( chunk )<br>    }<br>    return list.join( sepHiqusDefault )<br>}<br><br>var Hiqus= function( ){<br>    var hiqus= ( this instanceof Hiqus ) ? this: new Hiqus<br>    var data= placin.call( hiqus, '_data' )<br>    for( var i= 0, len= arguments.length; i < len; ++i ){<br>        var arg= arguments[i]<br>        if( arg instanceof Hiqus ) arg= arg._data<br>        var invoke= ( Object( arg ) instanceof String ) ? parsin : put<br>        invoke.call( data, arg )<br>    }<br>    return hiqus<br>}<br><br>Hiqus.prototype= new function(){<br><br>    this.put= function(){<br>        var hiqus= Hiqus( this )<br>        put.apply( hiqus._data, arguments )<br>        return hiqus<br>    }<br>    <br>    this.get= function( ){<br>        var val= get.apply( this._data, arguments )<br>        if( typeof val === 'object' ) val= put.call( [], val )<br>        return val<br>    }<br>    <br>    this.sub= function(){<br>        var hiqus= Hiqus()<br>        var val= get.apply( this._data, arguments )<br>        if( typeof val !== 'object' ) val= [ val ]<br>        hiqus._data= val<br>        return hiqus<br>    }<br><br>    this.toString = function(){<br>        var str= serialize( '', this._data )<br>        this.toString= function(){ return str }<br>        return str<br>    }<br><br>}<br><br>Export: return Hiqus<br><br>Usage:<br><br>alert(<br>    Hiqus<br>    (    Hiqus( '| a:b:c:1 / a:b: ; a:b::c & a=b__d' )<br>        .sub( 'a', 'b' )<br>        .put( [ 'e', 'f' ] )<br>    ,    'g/h'<br>    )<br>    .get( 4 )<br>) // alerts 'g'<br><br>}

Hiqus-объекты реализауют паттерн Immutable, принимая необходимые данные через конструктор. У него может быть произвольное число параметров одного из следующих типов:
  1. Строка. Будет произведён её парсинг и добавление в дерево.
  2. Hiqus-объект. Его данные будут примёржены к дереву.
  3. JSON. Аналогично Hiqus-объекту.
Методы hiqus-объекта:
  1. put — возвращает новый объект, являющийся копией исходного, но с установленным значением по определённому пути. Hiqus().put( {user: { name: 'Nick' } } ) эквивалентно Hiqus().put( 'user', { name: 'Nick' } ) и эквивалентно Hiqus().put( 'user', 'name', 'Nick' )
  2. get — возвращает копию значения по определённому пути. Hiqus().get( 'user', 'name' ) вернёт неопределённость, а Hiqus('Nick;John').get() вернёт ['Nick','John']
  3. sub — создаёт новый объект на основе значения по определённому пути. Hiqus('users::Nick|users::John').sub('users') эквивалентно Hiqus('Nick;John')
  4. toString — преобразует в строку, используя слэш и знак равенства в качестве разделителей. Результат сериализации кэшируется.

Тесты

;(function( x, y ){<br>    if( '' + x == y ) return arguments.callee<br>    console.log( x, y )<br>    throw new Error( 'fail test: [ ' + x + ' ]!=[ ' + y + ' ]' )<br>})<br><br>( Hiqus( ),    '' )<br>( Hiqus( {} ), '' )<br>( Hiqus( [] ), '' )<br>( Hiqus( [1] ), '1' )<br>( Hiqus( {'':1} ), '1' )<br>( Hiqus( {'':{'':1}} ), '==1' )<br>( Hiqus( [1,2] ), '1/2' )<br>( Hiqus( {1:2} ), '2' )<br>( Hiqus( {a:1} ), 'a=1' )<br>( Hiqus( {'a_b':1} ), 'a%5Fb=1' )<br>( Hiqus( {a:{b:1}} ), 'a=b=1' )<br>( Hiqus( {a:1,b:2} ), 'a=1/b=2' )<br>( Hiqus( {a:1},{b:2} ), 'a=1/b=2' )<br>( Hiqus( {a:1},'b:2' ), 'a=1/b=2' )<br>( Hiqus( {a:1},{b:2} ), new Hiqus( {a:1},{b:2} ) )<br>( Hiqus( Hiqus({a:1}), Hiqus({b:2}) ), 'a=1/b=2' )<br><br>( Hiqus( '' ), '' )<br>( Hiqus( 'a' ), 'a' )<br>( Hiqus( '=a' ), 'a' )<br>( Hiqus( '==1' ), '==1' )<br>( Hiqus( 'a=1' ), 'a=1' )<br>( Hiqus( 'a:1' ), 'a=1' )<br>( Hiqus( 'a_1' ), 'a=1' )<br>( Hiqus( 'a=b=1' ), 'a=b=1' )<br>( Hiqus( 'a=1/b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1;b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1 b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1&b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1|b=2' ), 'a=1/b=2' )<br><br>( Hiqus( '1/2' ).get(), '1,2' )<br>( Hiqus( 'a=1/b=2' ).get('a'), '1' )<br>( Hiqus( 'a=1=2/b=2=3' ).get('b','0'), '3' )<br>( ( ( a= Hiqus( 'a=1/b=2' ) ).get( 'a' ), a ), 'a=1/b=2' )<br><br>( Hiqus( 'a=1/b=2' ).sub(), 'a=1/b=2' )<br>( Hiqus( 'a=b=1/a=c=2/d=3' ).sub( 'a' ), 'b=1/c=2' )<br>( Hiqus( 'a=b==1/a=b==2' ).sub( 'a', 'b' ), '1/2' )<br>( ( a= Hiqus( 'a=1/b=2' ) ).sub( 'a' ) && a, 'a=1/b=2' )<br><br>( Hiqus( 'a=1' ).put( {b:2} ), 'a=1/b=2' )<br>( Hiqus( 'a=1' ).put( 'a', {b:2} ), 'a=b=2' )<br>( ( a= Hiqus( 'a=1' ) ).put( 'b=2' ) && a, 'a=1' )<br>;
Tags:
Hubs:
Total votes 41: ↑27 and ↓14+13
Comments72

Articles