Commit f88149e5 authored by fxa's avatar fxa

bugfixes according double encoding (Thanks to [RubenVerborgh]) and prefix modifiers

parent dd7a3854
......@@ -47,6 +47,7 @@
UNIT_TESTS = new jake.FileList('test/unit/test*.js').toArray(),
INTEGRATION_TESTS = [
path.join('test', 'integration', 'simpleTest.js'),
path.join('test', 'integration', 'testExport.js'),
path.join('test', 'integration', 'testRfcSamples.js')
],
......@@ -59,32 +60,36 @@
ASYNC = {async: true};
var all = new jake.FileList();
all.include('./Jakefile.js', 'own-testcases.json');
all.include('src/**');
all.include('test/**');
all.exclude(TARGET_COMPRESSED);
var TARGET_UNCOMPRESSED_DEPENDENCIES = all.toArray();
var TARGET_UNCOMPRESSED_DEPENDENCIES = (function () {
var all = new jake.FileList();
all.include('./Jakefile.js', 'own-testcases.json');
all.include('src/**');
all.include('test/**');
all.exclude(TARGET_COMPRESSED);
return all.toArray();
}());
function closeTask(err) {
function closeTask (err) {
if (err) {
fail(err);
fail(JSON.stringify(err, null, 4));
}
else {
complete();
}
complete();
}
desc('clean');
desc('removes all artifacts');
task('clean', [], function () {
function unlinkWhenExists(filename, callback) {
function unlinkWhenExists (filename, callback) {
fs.unlink(filename, function (err) {
callback(err && err.code !== 'ENOENT' ? err : undefined);
});
}
async.forEach([TMP_UNTESTED_UNCOMPRESSED, TMP_UNTESTED_COMPRESSED, TARGET_UNCOMPRESSED, TARGET_COMPRESSED], unlinkWhenExists, closeTask);
}, ASYNC);
file(TARGET_UNCOMPRESSED, TARGET_UNCOMPRESSED_DEPENDENCIES, function () {
// there is no other way to pass parameters to a testcase -- sorry
global.URI_TEMPLATE_FILE = TMP_UNTESTED_UNCOMPRESSED;
async.series([
function (callback) {
......@@ -93,7 +98,7 @@
},
function (callback) {
jake.logger.log('unit testing ...');
nodeunit.reporters[NODEUNIT_REPORTER].run(UNIT_TESTS, NODEUNIT_OPTIONS, callback);
nodeunit.reporters['minimal'].run(UNIT_TESTS, NODEUNIT_OPTIONS, callback);
},
function (callback) {
jake.logger.log('build concatenated version ...');
......@@ -138,9 +143,8 @@
}, ASYNC);
// for short test only
desc('unit tests');
desc('unit tests (without jshint)');
task('unit', [], function () {
// here we want the default reporter and not the configured one
nodeunit.reporters['default'].run(UNIT_TESTS, NODEUNIT_OPTIONS, complete);
}, ASYNC);
......@@ -148,7 +152,6 @@
task('build', [TARGET_COMPRESSED], function () {
jake.logger.log('done.');
});
task('default', ['clean', 'release']);
task('default', ['clean', 'build']);
}());
\ No newline at end of file
......@@ -9,6 +9,8 @@ It exposes a constructor function UriTemplate with the two methods:
* (static) parse(uriTemplateText) returning an instance of UriTemplate
* expand(variables) returning an string
Be aware, that a parsed UriTemplate is frozen, so it is stateless. You can reuse instances of UriTemplates.
Requirements
------------
......@@ -60,6 +62,7 @@ MIT License, see http://mit-license.org/
Release Notes
-------------
* 0.2.4 fixed double encoding according [RubenVerborgh] and some Prefix modifiers bugs
* 0.2.3 fixed bug with empty objects ('{?empty}' with '{empty:{}}' shall expand to '?empty=')
* 0.2.2 fixed pct encoding bug with multibyte utf8 chars
* 0.2.1 fixed a bug in package.json
......@@ -67,7 +70,6 @@ Release Notes
Next Steps
----------
* Implementing unit tests (now only dummy test implemented)
* Updating uritemplate-test (mnot added some new tests and removed some wrong. At the moment I cannot update, because the new tests will not pass)
* Implementing more unit tests (now only a view tests are implemented)
* A new method extract(uri), which tries to extract the variables from a given uri.
This is harder, than you might think
This diff is collapsed.
This diff is collapsed.
......@@ -5,13 +5,13 @@ module.exports = (function () {
fs = require('fs'),
async = require('async');
function readFileUtf8(filename, callback) {
function readFileUtf8 (filename, callback) {
// if you call readFile with encoding, the result is the file content as string.
// without encoding it would be a stream, which can be converted to a string with its toString() method
fs.readFile(filename, 'utf-8', callback);
}
function concat(inputFileArr, outputfile, callback, options) {
function concat (inputFileArr, outputfile, callback, options) {
async.map(inputFileArr, readFileUtf8, function (err, contents) {
if (err) {
throw new Error('could not read files: ' + err);
......@@ -41,19 +41,19 @@ module.exports = (function () {
});
}
function startsWith(text, beginning) {
function startsWith (text, beginning) {
return text.substr(0, beginning.length) === beginning;
}
function removeFirstLine(text) {
function removeFirstLine (text) {
var indexOfLinebreak = text.indexOf("\n");
if (indexOfLinebreak < 0) {
return "";
return text;
}
return text.substr(indexOfLinebreak + 1);
}
function removeUseStrict(text) {
function removeUseStrict (text) {
var lines = text.split('\n');
var filteredLines = lines.filter(function (line) {
return line.indexOf('"use strict"') < 0;
......@@ -61,7 +61,7 @@ module.exports = (function () {
return filteredLines.join('\n');
}
function removeJshintOptions(source) {
function removeJshintOptions (source) {
if (startsWith(source, '/*jshint')) {
source = removeFirstLine(source);
}
......
......@@ -22,7 +22,7 @@ module.exports = (function () {
undef: true, // forbids use of undefined variables
unused: true, // forbids unused variables
maxcomplexity: 17 // much too high. should be max. 10
maxcomplexity: 19 // much too high. should be max. 10
},
JSHINT_GLOBALS = {
......@@ -39,13 +39,19 @@ module.exports = (function () {
return;
}
if (err) {
throw new Error('could not read file ' + jsFile + ': ' + err);
callback('could not read file ' + jsFile + ': ' + err);
return;
}
numCheckedFiles += 1;
if (!jshint.jshint(content, JSHINT_OPTIONS, JSHINT_GLOBALS)) {
failed = true;
console.log(jshint.errors);
callback(jshint.errors, jsFile);
jshint.errors.push({filename: jsFile});
// jshint errors are awfully formated, when given to jake.fail()
console.log(JSON.stringify(jshint.errors, null, 4));
callback({
filename: jsFile,
reason: jshint.errors[0].reason
});
}
if (numCheckedFiles === jsFiles.length) {
callback();
......
......@@ -8,5 +8,40 @@
"testcases" : [
["http://localhost:8080/api/search{?q}", "http://localhost:8080/api/search?q=val"]
]
},
"Allowed characters": {
"level": 3,
"variables": {
"dot": "dot"
},
"testcases": [
["{?.dot}", false],
["{a,.b}", false]
]
},
"Prefix modifiers": {
"level": 4,
"variables": {
"text": "I like octal numbers",
"emptyObject": {},
"filledObject": {"composite": "true"},
"emptyArr": [],
"filledArr": [1]
},
"testcases": [
["{text:0}", false],
["{text:08}", false],
["{text:8}", "I%20like%20o"],
["{text:9999}", "I%20like%20octal%20numbers"],
["{text:10000}", false],
["{text:1.0}", false],
["{text:1e+2}", false],
["{text:9876543210987654321098765432109876543210}", false],
["{emptyObject:1}", ""],
["{filledObject:1}", false],
["{emptyArr:1}", ""],
["{filledArr:1}", false]
]
}
}
......@@ -12,12 +12,10 @@
"contributors": [],
"dependencies": {},
"main": "bin/uritemplate.js",
"licenses": [
{
"licenses": [{
"type": "MIT",
"url": "http://www.opensource.org/licenses/mit-license.php"
}
],
}],
"files": [
"src",
"test",
......@@ -27,7 +25,7 @@
"demo.html",
"demo_AMD.html",
"README.md",
"reqire.js",
"require.js",
".gitmodules",
"uritemplate-test/extended-tests.json",
"uritemplate-test/negative-tests.json",
......@@ -35,7 +33,7 @@
"uritemplate-test/spec-examples-by-sections.json",
"uritemplate-test/spec-examples.json"
],
"version": "0.2.3",
"version": "0.2.4",
"readmeFilename": "README.md",
"gitHead": "901b85201a821427dfb4591b56aea3a70d45c67c",
"devDependencies": {
......@@ -48,6 +46,5 @@
"repository": {
"type": "git",
"url": "https://github.com/fxa/uritemplate-js.git"
},
"license": "MIT"
}
}
......@@ -2,25 +2,28 @@
/*global pctEncoder, rfcCharHelper*/
var LiteralExpression = (function () {
"use strict";
function LiteralExpression (literal) {
this.literal = LiteralExpression.encodeLiteral(literal);
}
function encodeLiteral(literal) {
LiteralExpression.encodeLiteral = function (literal) {
var
result = '',
index,
chr = '';
for (index = 0; index < literal.length; index += chr.length) {
chr = literal.charAt(index);
result += rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
chr = pctEncoder.pctCharAt(literal, index);
if (chr.length > 1) {
result += chr;
}
else {
result += rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
}
// chr = literal.charAt(index);
// result += rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
}
return result;
}
function LiteralExpression(literal) {
this.literal = LiteralExpression.encodeLiteral(literal);
}
LiteralExpression.encodeLiteral = encodeLiteral;
};
LiteralExpression.prototype.expand = function () {
return this.literal;
......
/*jshint unused:false */
/*global parse*/
/*global parse, objectHelper*/
var UriTemplate = (function () {
"use strict";
function UriTemplate(templateText, expressions) {
function UriTemplate (templateText, expressions) {
this.templateText = templateText;
this.expressions = expressions;
objectHelper.deepFreeze(this);
}
UriTemplate.prototype.toString = function () {
......@@ -12,6 +13,7 @@ var UriTemplate = (function () {
};
UriTemplate.prototype.expand = function (variables) {
// this.expressions.map(function (expression) {return expression.expand(variables);}).join('');
var
index,
result = '';
......@@ -22,6 +24,5 @@ var UriTemplate = (function () {
};
UriTemplate.parse = parse;
return UriTemplate;
}());
......@@ -2,13 +2,12 @@
/*global pctEncoder, rfcCharHelper, isDefined, LiteralExpression, objectHelper*/
var VariableExpression = (function () {
"use strict";
// helper function if JSON is not available
function prettyPrint(value) {
function prettyPrint (value) {
return JSON ? JSON.stringify(value) : value;
}
function VariableExpression(templateText, operator, varspecs) {
function VariableExpression (templateText, operator, varspecs) {
this.templateText = templateText;
this.operator = operator;
this.varspecs = varspecs;
......@@ -18,7 +17,7 @@ var VariableExpression = (function () {
return this.templateText;
};
VariableExpression.prototype.expand = function expandExpression(variables) {
VariableExpression.prototype.expand = function (variables) {
var
result = '',
index,
......@@ -29,7 +28,7 @@ var VariableExpression = (function () {
operator = this.operator;
// callback to be used within array.reduce
function reduceUnexploded(result, currentValue, currentKey) {
function reduceUnexploded (result, currentValue, currentKey) {
if (isDefined(currentValue)) {
if (result.length > 0) {
result += ',';
......@@ -42,7 +41,7 @@ var VariableExpression = (function () {
return result;
}
function reduceNamedExploded(result, currentValue, currentKey) {
function reduceNamedExploded (result, currentValue, currentKey) {
if (isDefined(currentValue)) {
if (result.length > 0) {
result += operator.separator;
......@@ -53,7 +52,7 @@ var VariableExpression = (function () {
return result;
}
function reduceUnnamedExploded(result, currentValue, currentKey) {
function reduceUnnamedExploded (result, currentValue, currentKey) {
if (isDefined(currentValue)) {
if (result.length > 0) {
result += operator.separator;
......@@ -91,7 +90,7 @@ var VariableExpression = (function () {
}
result += '=';
}
if (varspec.maxLength && value.length > varspec.maxLength) {
if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
}
result += operator.encode(value);
......@@ -135,6 +134,5 @@ var VariableExpression = (function () {
return result;
};
return VariableExpression;
}());
......@@ -11,7 +11,7 @@
* @param object
* @return {Boolean}
*/
function isDefined(object) {
function isDefined (object) {
"use strict";
var
index,
......
/*jshint unused: false */
var objectHelper = (function () {
"use strict";
function isArray(value) {
function isArray (value) {
return Object.prototype.toString.apply(value) === '[object Array]';
}
// performs an array.reduce for objects
// TODO handling if initialValue is undefined
function objectReduce(object, callback, initialValue) {
function objectReduce (object, callback, initialValue) {
var
propertyName,
currentValue = initialValue;
......@@ -22,7 +21,7 @@ var objectHelper = (function () {
// performs an array.reduce, if reduce is not present (older browser...)
// TODO handling if initialValue is undefined
function arrayReduce(array, callback, initialValue) {
function arrayReduce (array, callback, initialValue) {
var
index,
currentValue = initialValue;
......@@ -32,12 +31,39 @@ var objectHelper = (function () {
return currentValue;
}
function reduce(arrayOrObject, callback, initialValue) {
function reduce (arrayOrObject, callback, initialValue) {
return isArray(arrayOrObject) ? arrayReduce(arrayOrObject, callback, initialValue) : objectReduce(arrayOrObject, callback, initialValue);
}
function deepFreezeUsingObjectFreeze (object) {
if (typeof object !== "object" || object === null) {
return object;
}
Object.freeze(object);
var property, propertyName;
for (propertyName in object) {
if (object.hasOwnProperty(propertyName)) {
property = object[propertyName];
// be aware, arrays are 'object', too
if (typeof property === "object") {
deepFreeze(property);
}
}
}
return object;
}
function deepFreeze (object) {
if (typeof Object.freeze === 'function') {
return deepFreezeUsingObjectFreeze(object);
}
return object;
}
return {
isArray: isArray,
reduce: reduce
reduce: reduce,
deepFreeze: deepFreeze
};
}());
/*jshint unused:false */
/*global pctEncoder, operators, charHelper, rfcCharHelper, LiteralExpression, UriTemplate, VariableExpression */
/*global pctEncoder, operators, charHelper, rfcCharHelper, LiteralExpression, UriTemplate, VariableExpression*/
var parse = (function () {
"use strict";
function parseExpression(outerText) {
function parseExpression (outerText) {
var
text,
operator,
......@@ -14,12 +13,12 @@ var parse = (function () {
index,
chr = '';
function closeVarname() {
function closeVarname () {
varspec = {varname: text.substring(varnameStart, index), exploded: false, maxLength: null};
varnameStart = null;
}
function closeMaxLength() {
function closeMaxLength () {
if (maxLengthStart === index) {
throw new Error("after a ':' you have to specify the length. position = " + index);
}
......@@ -27,7 +26,7 @@ var parse = (function () {
maxLengthStart = null;
}
// remove outer {}
// remove outer braces
text = outerText.substr(1, outerText.length - 2);
// determine operator
......@@ -52,7 +51,13 @@ var parse = (function () {
closeVarname();
}
if (maxLengthStart !== null) {
if (index === maxLengthStart && chr === '0') {
throw new Error('A :prefix must not start with digit 0 -- see position ' + index);
}
if (charHelper.isDigit(chr)) {
if (index - maxLengthStart >= 4) {
throw new Error('A :prefix must max 4 digits -- see position ' + index);
}
continue;
}
closeMaxLength();
......@@ -96,7 +101,7 @@ var parse = (function () {
return new VariableExpression(outerText, operator, varspecs);
}
function parseTemplate(uriTemplateText) {
function parseTemplate (uriTemplateText) {
// assert filled string
var
index,
......
......@@ -2,7 +2,6 @@
/*global charHelper, unescape*/
var pctEncoder = (function () {
"use strict";
var utf8 = {
encode: function (chr) {
// see http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html
......@@ -47,16 +46,28 @@ var pctEncoder = (function () {
return result;
}
/**
* Returns, whether the given text at start is in the form 'percent hex-digit hex-digit', like '%3F'
* @param text
* @param start
* @return {boolean|*|*}
*/
function isPercentDigitDigit (text, start) {
return text[start] === '%' && charHelper.isHexDigit(text[start + 1]) && charHelper.isHexDigit(text[start + 2]);
}
function parseHex2(text, start) {
/**
* Parses a hex number from start with length 2.
* @param text a string
* @param start the start index of the 2-digit hex number
* @return {Number}
*/
function parseHex2 (text, start) {
return parseInt(text.substr(start, 2), 16);
}
/**
* Returns wether or not the given char sequence is a correctly pct-encoded sequence.
* Returns whether or not the given char sequence is a correctly pct-encoded sequence.
* @param chr
* @return {boolean}
*/
......
......@@ -7,7 +7,7 @@
if (typeof module !== "undefined") {
module.exports = UriTemplate;
}
else if (typeof define !== "undefined") {
else if (typeof define === "function") {
define([],function() {
return UriTemplate;
});
......
......@@ -8,7 +8,7 @@ var rfcCharHelper = (function () {
* @param chr
* @return (Boolean)
*/
function isVarchar(chr) {
function isVarchar (chr) {
return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '_' || pctEncoder.isPctEncoded(chr);
}
......@@ -17,7 +17,7 @@ var rfcCharHelper = (function () {
* @param chr
* @return {Boolean}
*/
function isUnreserved(chr) {
function isUnreserved (chr) {
return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '-' || chr === '.' || chr === '_' || chr === '~';
}
......@@ -27,9 +27,9 @@ var rfcCharHelper = (function () {
* @param chr
* @return {Boolean}
*/
function isReserved(chr) {
function isReserved (chr) {
return chr === ':' || chr === '/' || chr === '?' || chr === '#' || chr === '[' || chr === ']' || chr === '@' || chr === '!' || chr === '$' || chr === '&' || chr === '(' ||
chr === ')' || chr === '*' || chr === '+' || chr === ',' || chr === ';' || chr === '=' || chr === "'" || chr === '%';
chr === ')' || chr === '*' || chr === '+' || chr === ',' || chr === ';' || chr === '=' || chr === "'";
}
return {
......
......@@ -5,9 +5,19 @@ module.exports = (function () {
UriTemplate = require('../../' + global.URI_TEMPLATE_FILE);
return {
'UriTemplate has a parse function': function (test) {
'UriTemplate has a static parse function': function (test) {
test.equal(typeof UriTemplate.parse, 'function');
test.done();
},
'UriTemplate has a expand function': function (test) {
test.equal(typeof UriTemplate.prototype.expand, 'function');
test.done();
},
'UriTemplate instances are frozen': function (test) {
var ut = new UriTemplate('text', []);
test.ok(Object.isFrozen(ut));
test.ok(Object.isFrozen(ut.expressions));
test.done();
}
};
}());
......@@ -5,27 +5,25 @@ module.exports = (function () {
fs = require('fs'),
path = require('path'),
sandbox = require('nodeunit').utils.sandbox;
// var testCase = require('nodeunit').testCase;
var NOISY = false;
function log(text) {
function log (text) {
if (NOISY) {
console.log(text);
}
}
function loadUriTemplate() {
function loadUriTemplate () {
var context = {module: {}};
sandbox(global.URI_TEMPLATE_FILE, context);
return context.module.exports;
}
function loadTestFile(testFileName) {
function loadTestFile (testFileName) {
return JSON.parse(fs.readFileSync(testFileName));
}
function assertMatches(test, template, variables, expected, chapterName, UriTemplate) {
function assertMatches (test, template, variables, expected, chapterName, UriTemplate) {
var
uriTemplate,
actual,
......@@ -34,13 +32,12 @@ module.exports = (function () {
uriTemplate = UriTemplate.parse(template);
}
catch (error) {
// if expected === false, the error was expected!
if (expected === false) {
log('ok. expected error found');
return;
}
log('error', error);
test.fail('chapter ' + chapterName + ', template ' + template + ' threw error: ' + error);
console.log('error', error);
test.fail('chapter ' + chapterName + ', template ' + template + ' threw error, when parsing: ' + error);
return;
}
test.ok(!!uriTemplate, 'uri template could not be parsed');
......@@ -51,11 +48,12 @@ module.exports = (function () {
return;
}
}
catch (exception) {
catch (error) {
if (expected === false) {
return;
}
test.fail('chapter ' + chapterName + ', template ' + template + ' threw error: ' + JSON.stringify(exception, null, 4));
console.log('error', error);
test.fail('chapter ' + chapterName + ', template ' + template + ' threw error, when expanding: ' + JSON.stringify(error, null, 4));
return;
}
if (expected.constructor === Array) {
......@@ -74,11 +72,11 @@ module.exports = (function () {
test.fail("actual: '" + actual + "', expected: one of " + JSON.stringify(expected) + ', chapter ' + chapterName + ', template ' + template);
}
else {
test.equal(actual, expected, 'actual: ' + actual + ', expected: ' + expected + ', template: ' + template);
test.equal(actual, expected, 'actual: "' + actual + '", expected: "' + expected + '", template: "' + template + '"');
}
}
function runTestFile(test, filename) {
function runTestFile (test, filename) {
var
tests,
chapterName,
......@@ -107,12 +105,15 @@ module.exports = (function () {
test.done();
}
var SPEC_HOME = 'uritemplate-test';
var SPEC_HOME = '../uritemplate-test';
return {
'spec examples': function (test) {
runTestFile(test, path.join(SPEC_HOME, 'spec-examples.json'));
},
'spec examples by section': function (test) {
runTestFile(test, path.join(SPEC_HOME, 'spec-examples-by-section.json'));
},
'extended tests': function (test) {
runTestFile(test, path.join(SPEC_HOME, 'extended-tests.json'));
},
......@@ -121,6 +122,10 @@ module.exports = (function () {
},
'own tests': function (test) {