From b8e5739e1a72a0a1c4f6fe0beb956f2bc50959c2 Mon Sep 17 00:00:00 2001 From: "Johannes J. Schmidt" Date: Tue, 3 Jun 2014 17:16:47 +0200 Subject: [PATCH] Completely new routing engine. --- README.md | 159 +++++++++++++++++++++--------------- index.js | 146 +++++++++++++++++++++++---------- test/definition_test.js | 28 ------- test/docuri-test.js | 51 ++++++++++++ test/merge-test.js | 27 ++++++ test/merge_test.js | 62 -------------- test/parse_test.js | 55 ------------- test/parts_test.js | 36 -------- test/reserved-names-test.js | 26 ++++++ test/stringify_test.js | 140 ------------------------------- 10 files changed, 304 insertions(+), 426 deletions(-) delete mode 100644 test/definition_test.js create mode 100644 test/docuri-test.js create mode 100644 test/merge-test.js delete mode 100644 test/merge_test.js delete mode 100644 test/parse_test.js delete mode 100644 test/parts_test.js create mode 100644 test/reserved-names-test.js delete mode 100644 test/stringify_test.js diff --git a/README.md b/README.md index b713a58..06ea455 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,127 @@ -# docuri [![Build Status](https://travis-ci.org/jo/docuri.svg?branch=master)](https://travis-ci.org/jo/docuri) +# DocURI [![Build Status](https://travis-ci.org/jo/docuri.svg?branch=master)](https://travis-ci.org/jo/docuri) Rich document ids for CouchDB: +```js +'movie/blade-runner/gallery-image/12/medium' ``` -type/id/subtype/index/version -``` - -For example: `movie/blade-runner/gallery-image/12/medium` -Docuris have many advantages: -* You can access the doc type (eg. in changes feed) -* They sort well in Futon -* They tell a lot about the document +### Advantages +* You can access the doc type everywhere (eg. in changes feed, response, view results...) +* They sort well in Futon and` _all_docs` +* DocURIs can tell a lot about the document * You can rely on a schema and construct ids of dependend documents (eg. a specific version of an image) -* You can easily delete related documents (eg. by requesting a range from `_all_docs`) +* Easily delete related documents (eg. by requesting a range from `_all_docs`) -...and I'm sure I forgot to mention the best. - -Give Docuris a try! +_Give DocURIs a try!_ ## Usage +Define methods for certain DocURI fragments and provide a routes hash that pairs routes to methods. +DocURI is inspired by [Backbone.Router](http://backbonejs.org/#Router). + +Routes can contain parameter parts, `:param`, which match a single DocURI component +between slashes; and splat parts `*splat`, which can match any number of DocURI +components. Part of a route can be made optional by surrounding it in +parentheses `(/:optional)`. -### `parse(string)` -Parse id string: +For example, a route of `'movie/:movie_id/gallery-image'` will generate a function which parses ```js -var docuri = require('docuri'); -docuri.parse('mytype/myid/mysubtype/myindex/myversion'); -// { -// type: 'mytype', -// id: 'myid', -// subtype: 'mysubtype', -// index: 'myindex', -// version: 'myversion' -// } +'movie/blade-runner/gallery-image/12' +// => +{ + movie_id: 'blade-runner', + id: '12' +} ``` +and vice versa. -### `stringify(object)` -Build id string from object: +A route of `'movie/:movie_id/:type/*path'` will generate a function which parses ```js -docuri.stringify({ - type: 'mytype', - id: 'myid', - subtype: 'mysubtype', - index: 'myindex', - version: 'myversion' -}); -// 'mytype/myid/mysubtype/myindex/myversion' +'movie/blade-runner/gallery-image/12' +// => +{ + movie_id: 'blade-runner', + type: 'gallery-image', + path: ['12'] +} +// and +'movie/blade-runner/gallery-image/12/medium' +// => +{ + movie_id: 'blade-runner', + type: 'gallery-image', + path: ['12', 'medium'] +} ``` -### `merge(objectOrString)` -Change id string components: +The route `'movie/:movie_id/gallery-image/:id(/:version)'` will generate a +function which parses ```js -docuri.merge('mytype/myid/mysubtype/myindex/myversion', { - type: 'my_new_type', -}); -// 'my_new_type/myid/mysubtype/myindex/myversion' +'movie/blade-runner/gallery-image/12' +// => +{ + movie_id: 'blade-runner', + id: '12' +} +// as well as +'movie/blade-runner/gallery-image/12/medium' +// => +{ + movie_id: 'blade-runner', + id: '12', + version: 'medium' +} ``` -### `parts(objectOrString)` -Array of components. Trailing `undefined` components are stripped off: +### `docuri.route(route, name)` +Create a single route. The `route` argument must be a routing string. The +`name` argument will be the identifier for the resulting function: +`docuri[name]`. Routes added later may override previously declared routes. + ```js -docuri.parts('mytype/myid/'); -// ['mytype', 'myid'] -docuri.parts({ type: 'mytype', subtype: 'mysubtype' }); -// ['mytype', undefined, 'mysubtype'] +// parses 'page/home' as { id: 'home' }: +docuri.route('page/:id', 'page'); ``` -### `docuri.definition([array])` -Access or use custom definition: +### `docuri.routes(map)` +Install a routes hash which maps DocURIs with parameters to functions: ```js -docuri.definition(); -// ['type', 'id', 'subtype', 'index', 'version'] -docuri - .definition(['id', 'meta']) - .parse('42/answer'); -// { -// id: '42', -// meta: 'answer' -// } +docuri.routes({ + 'movie/:id': 'movie', + 'movie/:movie_id/:type/*path': 'movieAsset' + 'movie/:movie_id/gallery-image/:id(/:version)': 'galleryImage', +}); ``` -Note: the (optional) argument to `docuri.definition` MUST be an array of strings -containing at least one item. +### `docuri[name](strOrObj, [obj])` +The functions generated by DocURI can have a different behaviour, depending on +the type and number of the supplied arguments: -## Browser support -To use docid in your client-side application, browserify it like this: +* `name(str)`: parse DocURI string to object +* `name(obj)`: generate DocURI string from object +* `name(str, obj)`: change DocURI string parts with values provided by object returning a string + +The function returns `false` if a string can not be parsed, enabling type +checks. + +#### Example +```js +docuri.movie('movie/blade-runner'); +// { id: 'blade-runner' } +docuri.movieAsset('movie/blade-runner'); +// false +docuri.galleryImage({ movie_id: 'blade-runner', id: 12 }); +// 'movie/blade-runner/gallery-image/12' +docuri.galleryImage('movie/blade-runner/gallery-image/12', { version: 'large' }); +// 'movie/blade-runner/gallery-image/12/large' +``` +## Browser support +To use DocURI in your client-side application, browserify it like this: ```shell browserify -s DocURI path/to/docuri/index.js > path/to/your/assets - ``` -Once added to your DOM, this will leave you with a global DocURI object for use -in your e.g. Backbone Models/Collections. +Or grab it from [browserify-as-a-service: docuri@latest](http://www.modulefarm.com/standalone/docuri@latest). + ## Development To run the unit tests: diff --git a/index.js b/index.js index 7d8d451..940fc38 100644 --- a/index.js +++ b/index.js @@ -1,74 +1,138 @@ /* -* docuri: Rich document ids for CouchDB. +* DocURI: Rich document ids for CouchDB. * * Copyright (c) 2014 null2 GmbH Berlin * Licensed under the MIT license. */ // type/id/subtype/index/version -var DEFINITION = ['type', 'id', 'subtype', 'index', 'version']; -var docuri; -module.exports = exports = docuri = {}; +var docuri = module.exports = exports = {}; -docuri.definition = function(definition) { - if (definition) { - DEFINITION = definition; +var reservedNames = ['routes', 'route']; - return docuri; - } +// Cached regular expressions for matching named param parts and splatted parts +// of route strings. +// http://backbonejs.org/docs/backbone.html#section-158 +var optionalParam = /\((.*?)\)/g; +var namedParam = /(\(\?)?:\w+/g; +var splatParam = /\*\w+/g; +var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - return DEFINITION; -} +// Convert a route string into a regular expression, +// with named regular expressions for named arguments. +// http://backbonejs.org/docs/backbone.html#section-165 +function routeToRegExp(src) { + var keys = []; + + var route = src.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + keys.push(match); + + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, function(match) { + keys.push(match); + return '([^?]*?)'; + }); + + keys = keys.reduce(function(memo, key) { + var value = '\\' + key; + + memo[key] = new RegExp(value + '(\\/|\\)|$)'); + + return memo; + }, {}); -docuri.parts = function(obj) { - if (typeof obj === 'string') { - obj = docuri.parse(obj); + return { + src: src, + exp: new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'), + keys: keys } +} - var parts = DEFINITION.map(function(part) { - return obj[part]; - }); +// Given a route and a DocURI return an object of extracted parameters. +// Unmatched DocURIs will be treated as false. +// http://backbonejs.org/docs/backbone.html#section-166 +function extractParameters(route, fragment) { + var params = route.exp.exec(fragment); - while (parts.length && (typeof parts[parts.length - 1] === 'undefined' || parts[parts.length - 1] === null)) { - parts.pop(); + if (!params) { + return false; } - return parts; -}; + params = params.slice(1); + + return Object.keys(route.keys).reduce(function(memo, key, i) { + var param = params[i]; -docuri.parse = function(str) { - str = str || ''; + if (param) { + if (key[0] === '*') { + param = param.split('/'); + } - return str.split('/').reduce(function(obj, value, i) { - if (value) { - obj[DEFINITION[i]] = value; + memo[key.substr(1)] = param; } - return obj; + return memo; }, {}); -}; +} -docuri.stringify = function(obj) { - obj = obj || {}; +// Insert named parameters from object. +function insertParameters(route, obj) { + var str = route.src; - return docuri.parts(obj).join('/'); -}; + Object.keys(route.keys).forEach(function(key) { + var k = key.substr(1); + var value = obj[k] || ''; -docuri.merge = function(obj, objToMerge) { - objToMerge = objToMerge || {}; + if (Array.isArray(value)) { + value = value.join('/'); + } - if (typeof obj === 'string') { - obj = docuri.parse(obj); + str = str.replace(route.keys[key], value + '$1'); + }); + + return str + .replace(/\(\/\)$/g, '') + .replace(/[)(]/g, ''); +} + + +// Map routes +docuri.routes = function(map) { + Object.keys(map).forEach(function(route) { + docuri.route(route, map[route]); + }); +}; + +// Manually bind a single named route +docuri.route = function(route, name) { + if (reservedNames.indexOf(name) > -1) { + throw('Reserved name "' + name + '" cannot be used.'); } - DEFINITION.forEach(function(part) { - if (part in objToMerge) { - obj[part] = objToMerge[part]; + route = routeToRegExp(route); + + docuri[name] = function(source, target) { + source = source || {}; + + if (target) { + source = extractParameters(route, source); + Object.keys(target).forEach(function(key) { + source[key] = target[key]; + }); } - }); - return docuri.stringify(obj); + if (typeof source === 'object') { + return insertParameters(route, source); + } + + if (typeof source === 'string') { + return extractParameters(route, source); + } + }; }; diff --git a/test/definition_test.js b/test/definition_test.js deleted file mode 100644 index f55269f..0000000 --- a/test/definition_test.js +++ /dev/null @@ -1,28 +0,0 @@ -var test = require('tap').test; -var definition = require('..').definition; - -test('default configuration', function(t) { - t.deepEqual(definition(), ['type', 'id', 'subtype', 'index', 'version'], 'should return default parts'); - t.end(); -}); - -test('set configuration', function(t) { - var parts = ['my', 'parts']; - t.type(definition(parts).merge, 'function', 'should return docuri api: merge'); - t.type(definition(parts).parse, 'function', 'should return docuri api: parse'); - t.type(definition(parts).stringify, 'function', 'should return docuri api: stringify'); - t.end(); -}); - -test('change configuration', function(t) { - var parts = ['my', 'parts']; - definition(parts); - t.deepEqual(definition(), parts, 'should return custom parts'); - t.end(); -}); - -test('use changed configuration', function(t) { - var parts = ['my', 'parts']; - t.deepEqual(definition(parts).parse('one/two'), { my: 'one', parts: 'two'}, 'should use custom parts'); - t.end(); -}); diff --git a/test/docuri-test.js b/test/docuri-test.js new file mode 100644 index 0000000..08f1177 --- /dev/null +++ b/test/docuri-test.js @@ -0,0 +1,51 @@ +var test = require('tap').test; +var docuri = require('..'); + +test('simple route', function(t) { + docuri.route('page', 'page'); + + t.deepEqual(docuri.page('page'), {}, 'parsed page returns empty object'); + t.equal(docuri.page('image'), false, 'parsed image returns false'); + t.equal(docuri.page(), 'page', 'generate page url'); + + t.end(); +}); + +test('named parameter', function(t) { + docuri.route('page/:id', 'page'); + + t.deepEqual(docuri.page('page/mypage'), { id: 'mypage' }, 'parsed page has "id" set to "mypage"'); + t.equal(docuri.page({ id: 'mypage' }), 'page/mypage', 'stringified page results in "page/mypage"'); + + t.end(); +}); + +test('optional parameter', function(t) { + docuri.route('page(/:id)', 'page'); + + t.deepEqual(docuri.page('page'), {}, 'parsed page returns empty object'); + t.deepEqual(docuri.page('page/mypage'), { id: 'mypage' }, 'parsed page has "id" set to "mypage"'); + t.equal(docuri.page(), 'page', 'stringified empty page results in "page"'); + t.equal(docuri.page({ id: 'mypage' }), 'page/mypage', 'stringified page results in "page/mypage"'); + + t.end(); +}); + +test('two named parameters', function(t) { + docuri.route('page/:page_id/content/:id', 'content'); + + t.deepEqual(docuri.content('page/mypage/content/mycontent'), { page_id: 'mypage', id: 'mycontent' }, 'parsed content has "page_id" set to "mypage" and "id" set to "mycontent"'); + t.equal(docuri.content({ page_id: 'mypage', id: 'mycontent' }), 'page/mypage/content/mycontent', 'stringified content results in "page/mypage/content/mycontent"'); + + t.end(); +}); + +test('splat parameter', function(t) { + docuri.route('page/*path', 'page'); + + t.deepEqual(docuri.page('page/mypage/otherpage'), { path: ['mypage', 'otherpage'] }, 'parsed page has "parts" set to ["mypage","otherpage"]'); + t.equal(docuri.page({ path: ['mypage', 'otherpage'] }), 'page/mypage/otherpage', 'stringified page results in "page/mypage/otherpage"'); + + t.end(); +}); + diff --git a/test/merge-test.js b/test/merge-test.js new file mode 100644 index 0000000..77e32cd --- /dev/null +++ b/test/merge-test.js @@ -0,0 +1,27 @@ +var test = require('tap').test; +var docuri = require('..'); + +test('named parameter', function(t) { + docuri.route('page/:id', 'page'); + + t.equal(docuri.page('page/mypage', { id: 'otherpage' }), 'page/otherpage', 'merged page has "id" set to "otherpage"'); + + t.end(); +}); + +test('optional parameter', function(t) { + docuri.route('page(/:id)', 'page'); + + t.deepEqual(docuri.page('page/mypage'), { id: 'mypage' }, 'parsed page has "id" set to "mypage"'); + t.equal(docuri.page('page/mypage', { id: 'otherpage' }), 'page/otherpage', 'merged page has "id" set to "otherpage"'); + + t.end(); +}); + +test('two named parameters', function(t) { + docuri.route('page/:page_id/content/:id', 'content'); + + t.equal(docuri.content('page/mypage/content/mycontent', { id: 'othercontent' }), 'page/mypage/content/othercontent', 'merged content has "id" set to "othercontent"'); + + t.end(); +}); diff --git a/test/merge_test.js b/test/merge_test.js deleted file mode 100644 index 684ba0f..0000000 --- a/test/merge_test.js +++ /dev/null @@ -1,62 +0,0 @@ -var test = require('tap').test; -var merge = require('..').merge; - -test('changing type component', function(t) { - t.equal(merge('type/id/subtype/index/version', {type:'new_type'}), 'new_type/id/subtype/index/version', 'should return docuri string with type changed'); - t.end(); -}); - -test('changing id component', function(t) { - t.equal(merge('type/id/subtype/index/version', {id:'new_id'}), 'type/new_id/subtype/index/version', 'should return docuri string with id changed'); - t.end(); -}); - -test('changing subtype component', function(t) { - t.equal(merge('type/id/subtype/index/version', {subtype:'new_subtype'}), 'type/id/new_subtype/index/version', 'should return docuri string with subtype changed'); - t.end(); -}); - -test('changing index component', function(t) { - t.equal(merge('type/id/subtype/index/version', {index:'new_index'}), 'type/id/subtype/new_index/version', 'should return docuri string with index changed'); - t.end(); -}); - -test('changing version component', function(t) { - t.equal(merge('type/id/subtype/index/version', {version:'new_version'}), 'type/id/subtype/index/new_version', 'should return docuri string with version changed'); - t.end(); -}); - -test('changing unknown component', function(t) { - t.equal(merge('type/id/subtype/index/version', {unknown:'i dont know'}), 'type/id/subtype/index/version', 'should return unchanged docuri'); - t.end(); -}); - - -test('missing second argument', function(t) { - t.equal(merge('type/id/subtype/index/version'), 'type/id/subtype/index/version', 'should return unchanged docuri string'); - t.end(); -}); - - -test('changing type component using object', function(t) { - t.equal(merge({ - type: 'type', - id: 'id', - subtype: 'subtype', - index: 'index', - version: 'version' - }, {type:'new_type'}), 'new_type/id/subtype/index/version', 'should return docuri string with type changed'); - t.end(); -}); - -test('removing component with undefined', function(t) { - t.equal(merge('type/id/subtype/index/version', {version:undefined}), 'type/id/subtype/index', 'should return docuri string with version removed'); - t.end(); -}); - -test('removing component with null', function(t) { - t.equal(merge('type/id/subtype/index/version', {version:null}), 'type/id/subtype/index', 'should return docuri string with version removed'); - t.end(); -}); - - diff --git a/test/parse_test.js b/test/parse_test.js deleted file mode 100644 index 959c9cc..0000000 --- a/test/parse_test.js +++ /dev/null @@ -1,55 +0,0 @@ -var test = require('tap').test; -var parse = require('..').parse; - -test('missing argument', function(t) { - t.deepEqual(parse(), {}, 'should return empty object'); - t.end(); -}); - -test('empty string', function(t) { - t.deepEqual(parse(''), {}, 'should return empty object'); - t.end(); -}); - -test('type', function(t) { - t.deepEqual(parse('mytype'), { type: 'mytype' }, 'should return object with type'); - t.end(); -}); - -test('type/id', function(t) { - t.deepEqual(parse('mytype/myid'), { - type: 'mytype', - id: 'myid' - }, 'should return object with type and id'); - t.end(); -}); - -test('type/id/subtype', function(t) { - t.deepEqual(parse('mytype/myid/mysubtype'), { - type: 'mytype', - id: 'myid', - subtype: 'mysubtype' - }, 'should return object with type, id and subtype'); - t.end(); -}); - -test('type/id/subtype/index', function(t) { - t.deepEqual(parse('mytype/myid/mysubtype/myindex'), { - type: 'mytype', - id: 'myid', - subtype: 'mysubtype', - index: 'myindex' - }, 'should return object with type, id, subtype and index'); - t.end(); -}); - -test('type/id/subtype/index/version', function(t) { - t.deepEqual(parse('mytype/myid/mysubtype/myindex/myversion'), { - type: 'mytype', - id: 'myid', - subtype: 'mysubtype', - index: 'myindex', - version: 'myversion' - }, 'should return object with type, id, subtype, index and version'); - t.end(); -}); diff --git a/test/parts_test.js b/test/parts_test.js deleted file mode 100644 index b88bd7b..0000000 --- a/test/parts_test.js +++ /dev/null @@ -1,36 +0,0 @@ -var test = require('tap').test; -var parts = require('..').parts; - -test('parts of type string', function(t) { - t.deepEqual(parts('type'), ['type'], 'should return array including type'); - t.end(); -}); -test('parts of type/id string', function(t) { - t.deepEqual(parts('type/id'), ['type', 'id'], 'should return array including type and id'); - t.end(); -}); -test('parts of type/id/subtype string', function(t) { - t.deepEqual(parts('type/id/subtype'), ['type', 'id', 'subtype'], 'should return array including type, id and subtype'); - t.end(); -}); -test('parts of type//subtype string', function(t) { - t.deepEqual(parts('type//subtype'), ['type', undefined, 'subtype'], 'should return array including type, undefined and subtype'); - t.end(); -}); - -test('parts of type object', function(t) { - t.deepEqual(parts({ type: 'type' }), ['type'], 'should return array including type'); - t.end(); -}); -test('parts of type/id object', function(t) { - t.deepEqual(parts({ type: 'type', id: 'id'}), ['type', 'id'], 'should return array including type and id'); - t.end(); -}); -test('parts of type/id/subtype object', function(t) { - t.deepEqual(parts({ type: 'type', id: 'id', subtype: 'subtype' }), ['type', 'id', 'subtype'], 'should return array including type, id and subtype'); - t.end(); -}); -test('parts of type//subtype object', function(t) { - t.deepEqual(parts({ type: 'type', subtype: 'subtype' }), ['type', undefined, 'subtype'], 'should return array including type, undefined and subtype'); - t.end(); -}); diff --git a/test/reserved-names-test.js b/test/reserved-names-test.js new file mode 100644 index 0000000..996d028 --- /dev/null +++ b/test/reserved-names-test.js @@ -0,0 +1,26 @@ +var test = require('tap').test; +var docuri = require('..'); + +test('reserved name route', function(t) { + t.plan(1); + + try { + docuri.route('page', 'route'); + } catch(e) { + t.equal(e, 'Reserved name "route" cannot be used.', 'reserved name "route" should throw error'); + } + + t.end(); +}); + +test('reserved name routes', function(t) { + t.plan(1); + + try { + docuri.route('page', 'routes'); + } catch(e) { + t.equal(e, 'Reserved name "routes" cannot be used.', 'reserved name "routes" should throw error'); + } + + t.end(); +}); diff --git a/test/stringify_test.js b/test/stringify_test.js deleted file mode 100644 index 364a6c4..0000000 --- a/test/stringify_test.js +++ /dev/null @@ -1,140 +0,0 @@ -var test = require('tap').test; -var stringify = require('..').stringify; - -test('missing argument', function(t) { - t.deepEqual(stringify(), '', 'should return empty string'); - t.end(); -}); - -test('empty object', function(t) { - t.deepEqual(stringify({}), '', 'should return empty string'); - t.end(); -}); - -test('type', function(t) { - t.deepEqual(stringify({ type: 'mytype' }), 'mytype', 'should return string with type'); - t.end(); -}); - - -test('/id', function(t) { - t.deepEqual(stringify({ - id: 'myid' - }), '/myid', 'should return string with type and id'); - t.end(); -}); - -test('type/id', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - id: 'myid' - }), 'mytype/myid', 'should return string with type and id'); - t.end(); -}); - - -test('//subtype', function(t) { - t.deepEqual(stringify({ - subtype: 'mysubtype' - }), '//mysubtype', 'should return string with type, id and subtype'); - t.end(); -}); - -test('/id/subtype', function(t) { - t.deepEqual(stringify({ - id: 'myid', - subtype: 'mysubtype' - }), '/myid/mysubtype', 'should return string with type, id and subtype'); - t.end(); -}); - -test('type/id/subtype', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - id: 'myid', - subtype: 'mysubtype' - }), 'mytype/myid/mysubtype', 'should return string with type, id and subtype'); - t.end(); -}); - - -test('/id/subtype/index', function(t) { - t.deepEqual(stringify({ - id: 'myid', - subtype: 'mysubtype', - index: 'myindex' - }), '/myid/mysubtype/myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - -test('type//subtype/index', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - subtype: 'mysubtype', - index: 'myindex' - }), 'mytype//mysubtype/myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - -test('type///index', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - index: 'myindex' - }), 'mytype///myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - -test('//subtype/index', function(t) { - t.deepEqual(stringify({ - subtype: 'mysubtype', - index: 'myindex' - }), '//mysubtype/myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - -test('///index', function(t) { - t.deepEqual(stringify({ - index: 'myindex' - }), '///myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - -test('type/id/subtype/index', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - id: 'myid', - subtype: 'mysubtype', - index: 'myindex' - }), 'mytype/myid/mysubtype/myindex', 'should return string with type, id, subtype and index'); - t.end(); -}); - - -test('////version', function(t) { - t.deepEqual(stringify({ - version: 'myversion' - }), '////myversion', 'should return string with type, id, subtype, index and version'); - t.end(); -}); - -test('type/id/subtype/index/version', function(t) { - t.deepEqual(stringify({ - type: 'mytype', - id: 'myid', - subtype: 'mysubtype', - index: 'myindex', - version: 'myversion' - }), 'mytype/myid/mysubtype/myindex/myversion', 'should return string with type, id, subtype, index and version'); - t.end(); -}); - -test('0/1/2/3/4', function(t) { - t.deepEqual(stringify({ - type: 0, - id: 1, - subtype: 2, - index: 3, - version: 4 - }), '0/1/2/3/4', 'should correctly handle number path fields'); - t.end(); -});