mirror of https://github.com/jo/docuri.git
commit
5c2d85b97d
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
});
|
@ -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();
|
||||
});
|
@ -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();
|
||||
});
|
@ -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();
|
||||
});
|
Loading…
Reference in New Issue