Commit 819db320 authored by fxa's avatar fxa

Bugfix #10 UriTemplate.parse('/foo{?a,b,c}').expand({}); should produce /foo

parent aa5b2ecf
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
return all.toArray(); return all.toArray();
}()); }());
function closeTask (err) { function closeAsyncJakeTask (err) {
if (err) { if (err) {
fail(JSON.stringify(err, null, 4)); fail(JSON.stringify(err, null, 4));
} }
...@@ -86,7 +86,13 @@ ...@@ -86,7 +86,13 @@
callback(err && err.code !== 'ENOENT' ? err : undefined); callback(err && err.code !== 'ENOENT' ? err : undefined);
}); });
} }
async.forEach([TMP_UNTESTED_UNCOMPRESSED, TMP_UNTESTED_COMPRESSED, TARGET_UNCOMPRESSED, TARGET_COMPRESSED], unlinkWhenExists, closeTask); async.forEach([
TMP_UNTESTED_UNCOMPRESSED,
TMP_UNTESTED_COMPRESSED,
TARGET_UNCOMPRESSED,
TARGET_COMPRESSED],
unlinkWhenExists,
closeAsyncJakeTask);
}, ASYNC); }, ASYNC);
file(TARGET_UNCOMPRESSED, TARGET_UNCOMPRESSED_DEPENDENCIES, function () { file(TARGET_UNCOMPRESSED, TARGET_UNCOMPRESSED_DEPENDENCIES, function () {
...@@ -121,7 +127,7 @@ ...@@ -121,7 +127,7 @@
jake.logger.log('move uncompressed version to target directory'); jake.logger.log('move uncompressed version to target directory');
fs.rename(TMP_UNTESTED_UNCOMPRESSED, TARGET_UNCOMPRESSED, callback); fs.rename(TMP_UNTESTED_UNCOMPRESSED, TARGET_UNCOMPRESSED, callback);
} }
], closeTask); ], closeAsyncJakeTask);
}, ASYNC); }, ASYNC);
file(TARGET_COMPRESSED, [TARGET_UNCOMPRESSED], function () { file(TARGET_COMPRESSED, [TARGET_UNCOMPRESSED], function () {
...@@ -140,19 +146,20 @@ ...@@ -140,19 +146,20 @@
jake.logger.log('move compressed version to target ... '); jake.logger.log('move compressed version to target ... ');
fs.rename(TMP_UNTESTED_COMPRESSED, TARGET_COMPRESSED, callback); fs.rename(TMP_UNTESTED_COMPRESSED, TARGET_COMPRESSED, callback);
} }
], closeTask); ], closeAsyncJakeTask);
}, ASYNC); }, ASYNC);
// for short test only desc('unit tests (without jshint, only a shortcut for development)');
desc('unit tests (without jshint)');
task('unit', [], function () { task('unit', [], function () {
nodeunit.reporters['default'].run(UNIT_TESTS, NODEUNIT_OPTIONS, complete); nodeunit.reporters['default'].run(UNIT_TESTS, NODEUNIT_OPTIONS, complete);
}, ASYNC); }, ASYNC);
desc('build'); desc('build and test all artifacts');
task('build', [TARGET_COMPRESSED], function () { task('build', [TARGET_COMPRESSED], function () {
jake.logger.log('done.'); jake.logger.log('done.');
}); });
desc('default -- called, if you call jake without parameters');
task('default', ['clean', 'build']); task('default', ['clean', 'build']);
}()); }());
\ No newline at end of file
...@@ -62,6 +62,7 @@ MIT License, see http://mit-license.org/ ...@@ -62,6 +62,7 @@ MIT License, see http://mit-license.org/
Release Notes Release Notes
------------- -------------
* 0.3.1 fixed https://github.com/fxa/uritemplate-js/issues/10 thank you, Paul-Martin!
* 0.3.0 introduced UriTemplateError as exception, so the interface changed from string to UriTemplateError (as the rfc suggested) * 0.3.0 introduced UriTemplateError as exception, so the interface changed from string to UriTemplateError (as the rfc suggested)
* 0.2.4 fixed double encoding according [RubenVerborgh] and some Prefix modifiers bugs * 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.3 fixed bug with empty objects ('{?empty}' with '{empty:{}}' shall expand to '?empty=')
......
(function(e){"use strict";function r(e){var t,o;if(null===e||void 0===e)return!1;if(n.isArray(e)){for(t=0;e.length>t;t+=1)if(r(e[t]))return!0;return!1}if("string"==typeof e||"number"==typeof e||"boolean"==typeof e)return!0;for(o in e)if(e.hasOwnProperty(o)&&r(e[o]))return!0;return!1}var n=function(){function e(e){return"[object Array]"===Object.prototype.toString.apply(e)}function r(e,r,n){var t,o=n;for(t in e)e.hasOwnProperty(t)&&(o=r(o,e[t],t,e));return o}function n(e,r,n){var t,o=n;for(t=0;e.length>t;t+=1)o=r(o,e[t],t,e);return o}function t(t,o,i){return e(t)?n(t,o,i):r(t,o,i)}function o(e){if("object"!=typeof e||null===e)return e;Object.freeze(e);var r,n;for(n in e)e.hasOwnProperty(n)&&(r=e[n],"object"==typeof r&&i(r));return e}function i(e){return"function"==typeof Object.freeze?o(e):e}return{isArray:e,reduce:t,deepFreeze:i}}(),t=function(){function e(e){return e>="a"&&"z">=e||e>="A"&&"Z">=e}function r(e){return e>="0"&&"9">=e}function n(e){return r(e)||e>="a"&&"f">=e||e>="A"&&"F">=e}return{isAlpha:e,isDigit:r,isHexDigit:n}}(),o=function(){function e(e){var r,n,t="",o=u.encode(e);for(n=0;o.length>n;n+=1)r=o.charCodeAt(n),t+="%"+r.toString(16).toUpperCase();return t}function r(e,r){return"%"===e[r]&&t.isHexDigit(e[r+1])&&t.isHexDigit(e[r+2])}function n(e,r){return parseInt(e.substr(r,2),16)}function o(e){if(!r(e,0))return!1;var t=n(e,1),o=u.numBytes(t);if(0===o)return!1;for(var i=1;o>i;i+=1)if(!r(e,3*i)||!u.isValidFollowingCharCode(n(e,3*i+1)))return!1;return!0}function i(e,t){var o=e[t];if(!r(e,t))return o;var i=n(e,t+1),a=u.numBytes(i);if(0===a)return o;for(var s=1;a>s;s+=1)if(!r(e,t+3*s)||!u.isValidFollowingCharCode(n(e,t+3*s+1)))return o;return e.substr(t,3*a)}var u={encode:function(e){return unescape(encodeURIComponent(e))},numBytes:function(e){return 127>=e?1:e>=194&&223>=e?2:e>=224&&239>=e?3:e>=240&&244>=e?4:0},isValidFollowingCharCode:function(e){return e>=128&&191>=e}};return{encodeCharacter:e,isPctEncoded:o,pctCharAt:i}}(),i=function(){function e(e){return t.isAlpha(e)||t.isDigit(e)||"_"===e||o.isPctEncoded(e)}function r(e){return t.isAlpha(e)||t.isDigit(e)||"-"===e||"."===e||"_"===e||"~"===e}function n(e){return":"===e||"/"===e||"?"===e||"#"===e||"["===e||"]"===e||"@"===e||"!"===e||"$"===e||"&"===e||"("===e||")"===e||"*"===e||"+"===e||","===e||";"===e||"="===e||"'"===e}return{isVarchar:e,isUnreserved:r,isReserved:n}}(),u=function(){function e(e,r){var n,t="",u="";for(("number"==typeof e||"boolean"==typeof e)&&(e=""+e),n=0;e.length>n;n+=u.length)u=e.charAt(n),t+=i.isUnreserved(u)||r&&i.isReserved(u)?u:o.encodeCharacter(u);return t}function r(r){return e(r,!0)}return{encode:e,encodePassReserved:r}}(),a=function(){function e(e){r[e]={symbol:e,separator:"?"===e?"&":""===e||"+"===e||"#"===e?",":e,named:";"===e||"&"===e||"?"===e,ifEmpty:"&"===e||"?"===e?"=":"",first:"+"===e?"":e,encode:"+"===e||"#"===e?u.encodePassReserved:u.encode,toString:function(){return this.symbol}}}var r={};return e(""),e("+"),e("#"),e("."),e("/"),e(";"),e("?"),e("&"),{valueOf:function(e){if(r[e])return r[e];if("=,!@|".indexOf(e)>=0)throw Error('Illegal use of reserved operator "'+e+'"');return r[""]}}}(),s=function(){function e(r){this.literal=e.encodeLiteral(r)}return e.encodeLiteral=function(e){var r,n="",t="";for(r=0;e.length>r;r+=t.length)t=o.pctCharAt(e,r),n+=t.length>1?t:i.isReserved(t)||i.isUnreserved(t)?t:o.encodeCharacter(t);return n},e.prototype.expand=function(){return this.literal},e.prototype.toString=e.prototype.expand,e}(),f=function(){function e(e){function r(){p={varname:u.substring(d,f),exploded:!1,maxLength:null},d=null}function n(){if(h===f)throw Error("after a ':' you have to specify the length. position = "+f);p.maxLength=parseInt(u.substring(h,f),10),h=null}var u,s,f,c=[],p=null,d=null,h=null,g="";for(u=e.substr(1,e.length-2),s=a.valueOf(u.charAt(0)),f=""===s.symbol?0:1,d=f;u.length>f;f+=g.length){if(g=o.pctCharAt(u,f),null!==d){if("."===g){if(d===f)throw Error("a varname MUST NOT start with a dot -- see position "+f);continue}if(i.isVarchar(g))continue;r()}if(null!==h){if(f===h&&"0"===g)throw Error("A :prefix must not start with digit 0 -- see position "+f);if(t.isDigit(g)){if(f-h>=4)throw Error("A :prefix must max 4 digits -- see position "+f);continue}n()}if(":"!==g)if("*"!==g){if(","!==g)throw Error("illegal character '"+g+"' at position "+f+' of "'+u+'"');c.push(p),p=null,d=f+1}else{if(null===p)throw Error("explode exploded at position "+f);if(p.exploded)throw Error("explode exploded twice at position "+f);if(p.maxLength)throw Error("an explode (*) MUST NOT follow to a prefix, see position "+f);p.exploded=!0}else{if(null!==p.maxLength)throw Error("only one :maxLength is allowed per varspec at position "+f);h=f+1}}return null!==d&&r(),null!==h&&n(),c.push(p),new l(e,s,c)}function r(r){var n,t,o=[],i=null,u=0;for(n=0;r.length>n;n+=1)if(t=r.charAt(n),null===u){if(null===i)throw Error("reached unreachable code");if("{"===t)throw Error("brace was opened in position "+i+" and cannot be reopened in position "+n);if("}"===t){if(i+1===n)throw Error("empty braces on position "+i);o.push(e(r.substring(i,n+1))),i=null,u=n+1}}else{if("}"===t)throw Error("brace was closed in position "+n+" but never opened");"{"===t&&(n>u&&o.push(new s(r.substring(u,n))),u=null,i=n)}if(null!==i)throw Error("brace was opened on position "+i+", but never closed");return r.length>u&&o.push(new s(r.substr(u))),new c(r,o)}return r}(),l=function(){function e(e){return JSON?JSON.stringify(e):e}function t(e,r,n){this.templateText=e,this.operator=r,this.varspecs=n}return t.prototype.toString=function(){return this.templateText},t.prototype.expand=function(t){function o(e,n,t){return r(n)&&(e.length>0&&(e+=","),c||(e+=h.encode(t)+","),e+=h.encode(n)),e}function i(e,n,t){return r(n)&&(e.length>0&&(e+=h.separator),e+=c?s.encodeLiteral(f.varname):h.encode(t),e+="="+h.encode(n)),e}function u(e,n,t){return r(n)&&(e.length>0&&(e+=h.separator),c||(e+=h.encode(t)+"="),e+=h.encode(n)),e}var a,f,l,c,p="",d=!0,h=this.operator;for(a=0;this.varspecs.length>a;a+=1)if(f=this.varspecs[a],l=t[f.varname],r(l))if(d?(p+=h.first,d=!1):p+=h.separator,c=n.isArray(l),"string"==typeof l||"number"==typeof l||"boolean"==typeof l){if(l=""+l,h.named){if(p+=s.encodeLiteral(f.varname),""===l){p+=h.ifEmpty;continue}p+="="}null!==f.maxLength&&(l=l.substr(0,f.maxLength)),p+=h.encode(l)}else{if(f.maxLength)throw Error("Prefix modifiers are not applicable to variables that have composite values. You tried to expand "+this+" with "+e(l));if(f.exploded)p+=n.reduce(l,h.named?i:u,"");else{if(h.named){if(p+=s.encodeLiteral(f.varname),!r(l)){p+=h.ifEmpty;continue}p+="="}p+=n.reduce(l,o,"")}}if(d){var g=!1;for(a=0;this.varspecs.length>a;a+=1)if(this.varspecs[a].exploded){g=!0;break}h.named&&!g&&(p+=h.symbol,p+=f.varname+h.ifEmpty)}return p},t}(),c=function(){function e(e,r){this.templateText=e,this.expressions=r,n.deepFreeze(this)}return e.prototype.toString=function(){return this.templateText},e.prototype.expand=function(e){var r,n="";for(r=0;this.expressions.length>r;r+=1)n+=this.expressions[r].expand(e);return n},e.parse=f,e}();e(c)})(function(e){"use strict";"undefined"!=typeof module?module.exports=e:"function"==typeof define?define([],function(){return e}):"undefined"!=typeof window?window.UriTemplate=e:global.UriTemplate=e}); (function(e){"use strict";function n(e){var t;if(null===e||void 0===e)return!1;if(r.isArray(e))return e.length>0;if("string"==typeof e||"number"==typeof e||"boolean"==typeof e)return!0;for(t in e)if(e.hasOwnProperty(t)&&n(e[t]))return!0;return!1}var t=function(){function e(e){this.options=e}return e.prototype.toString=function(){return JSON&&JSON.stringify?JSON.stringify(this.options):this.options},e}(),r=function(){function e(e){return"[object Array]"===Object.prototype.toString.apply(e)}function n(e,n){var t,r="",i=!0;for(t=0;e.length>t;t+=1)i?i=!1:r+=n,r+=e[t];return r}function t(e,n){for(var t=[],r=0;e.length>r;r+=1)t.push(n(e[r]));return t}function r(e,n){for(var t=[],r=0;e.length>r;r+=1)n(e[r])&&t.push(e[r]);return t}function i(e){if("object"!=typeof e||null===e)return e;Object.freeze(e);var n,t;for(t in e)e.hasOwnProperty(t)&&(n=e[t],"object"==typeof n&&o(n));return e}function o(e){return"function"==typeof Object.freeze?i(e):e}return{isArray:e,join:n,map:t,filter:r,deepFreeze:o}}(),i=function(){function e(e){return e>="a"&&"z">=e||e>="A"&&"Z">=e}function n(e){return e>="0"&&"9">=e}function t(e){return n(e)||e>="a"&&"f">=e||e>="A"&&"F">=e}return{isAlpha:e,isDigit:n,isHexDigit:t}}(),o=function(){function e(e){var n,t,r="",i=s.encode(e);for(t=0;i.length>t;t+=1)n=i.charCodeAt(t),r+="%"+n.toString(16).toUpperCase();return r}function n(e,n){return"%"===e[n]&&i.isHexDigit(e[n+1])&&i.isHexDigit(e[n+2])}function t(e,n){return parseInt(e.substr(n,2),16)}function r(e){if(!n(e,0))return!1;var r=t(e,1),i=s.numBytes(r);if(0===i)return!1;for(var o=1;i>o;o+=1)if(!n(e,3*o)||!s.isValidFollowingCharCode(t(e,3*o+1)))return!1;return!0}function o(e,r){var i=e[r];if(!n(e,r))return i;var o=t(e,r+1),a=s.numBytes(o);if(0===a)return i;for(var u=1;a>u;u+=1)if(!n(e,r+3*u)||!s.isValidFollowingCharCode(t(e,r+3*u+1)))return i;return e.substr(r,3*a)}var s={encode:function(e){return unescape(encodeURIComponent(e))},numBytes:function(e){return 127>=e?1:e>=194&&223>=e?2:e>=224&&239>=e?3:e>=240&&244>=e?4:0},isValidFollowingCharCode:function(e){return e>=128&&191>=e}};return{encodeCharacter:e,isPctEncoded:r,pctCharAt:o}}(),s=function(){function e(e){return i.isAlpha(e)||i.isDigit(e)||"_"===e||o.isPctEncoded(e)}function n(e){return i.isAlpha(e)||i.isDigit(e)||"-"===e||"."===e||"_"===e||"~"===e}function t(e){return":"===e||"/"===e||"?"===e||"#"===e||"["===e||"]"===e||"@"===e||"!"===e||"$"===e||"&"===e||"("===e||")"===e||"*"===e||"+"===e||","===e||";"===e||"="===e||"'"===e}return{isVarchar:e,isUnreserved:n,isReserved:t}}(),a=function(){function e(e,n){var t,r="",i="";for(("number"==typeof e||"boolean"==typeof e)&&(e=""+e),t=0;e.length>t;t+=i.length)i=e.charAt(t),r+=s.isUnreserved(i)||n&&s.isReserved(i)?i:o.encodeCharacter(i);return r}function n(n){return e(n,!0)}function t(e,n){var t=o.pctCharAt(e,n);return t.length>1?t:s.isReserved(t)||s.isUnreserved(t)?t:o.encodeCharacter(t)}function r(e){var n,t="",r="";for(n=0;e.length>n;n+=r.length)r=o.pctCharAt(e,n),t+=r.length>1?r:s.isReserved(r)||s.isUnreserved(r)?r:o.encodeCharacter(r);return t}return{encode:e,encodePassReserved:n,encodeLiteral:r,encodeLiteralCharacter:t}}(),u=function(){function e(e){n[e]={symbol:e,separator:"?"===e?"&":""===e||"+"===e||"#"===e?",":e,named:";"===e||"&"===e||"?"===e,ifEmpty:"&"===e||"?"===e?"=":"",first:"+"===e?"":e,encode:"+"===e||"#"===e?a.encodePassReserved:a.encode,toString:function(){return this.symbol}}}var n={};return e(""),e("+"),e("#"),e("."),e("/"),e(";"),e("?"),e("&"),{valueOf:function(e){return n[e]?n[e]:"=,!@|".indexOf(e)>=0?null:n[""]}}}(),f=function(){function e(e){this.literal=a.encodeLiteral(e)}return e.prototype.expand=function(){return this.literal},e.prototype.toString=e.prototype.expand,e}(),p=function(){function e(e){function n(){var n=e.substring(h,f);if(0===n.length)throw new t({expressionText:e,message:"a varname must be specified",position:f});c={varname:n,exploded:!1,maxLength:null},h=null}function r(){if(d===f)throw new t({expressionText:e,message:"after a ':' you have to specify the length",position:f});c.maxLength=parseInt(e.substring(d,f),10),d=null}var a,f,p=[],c=null,h=null,d=null,m="";for(a=function(n){var r=u.valueOf(n);if(null===r)throw new t({expressionText:e,message:"illegal use of reserved operator",position:f,operator:n});return r}(e.charAt(0)),f=a.symbol.length,h=f;e.length>f;f+=m.length){if(m=o.pctCharAt(e,f),null!==h){if("."===m){if(h===f)throw new t({expressionText:e,message:"a varname MUST NOT start with a dot",position:f});continue}if(s.isVarchar(m))continue;n()}if(null!==d){if(f===d&&"0"===m)throw new t({expressionText:e,message:"A :prefix must not start with digit 0",position:f});if(i.isDigit(m)){if(f-d>=4)throw new t({expressionText:e,message:"A :prefix must have max 4 digits",position:f});continue}r()}if(":"!==m)if("*"!==m){if(","!==m)throw new t({expressionText:e,message:"illegal character",character:m,position:f});p.push(c),c=null,h=f+1}else{if(null===c)throw new t({expressionText:e,message:"exploded without varspec",position:f});if(c.exploded)throw new t({expressionText:e,message:"exploded twice",position:f});if(c.maxLength)throw new t({expressionText:e,message:"an explode (*) MUST NOT follow to a prefix",position:f});c.exploded=!0}else{if(null!==c.maxLength)throw new t({expressionText:e,message:"only one :maxLength is allowed per varspec",position:f});if(c.exploded)throw new t({expressionText:e,message:"an exploeded varspec MUST NOT be varspeced",position:f});d=f+1}}return null!==h&&n(),null!==d&&r(),p.push(c),new l(e,a,p)}function n(n){var r,i,o=[],s=null,a=0;for(r=0;n.length>r;r+=1)if(i=n.charAt(r),null===a){if(null===s)throw Error("reached unreachable code");if("{"===i)throw new t({templateText:n,message:"brace already opened",position:r});if("}"===i){if(s+1===r)throw new t({templateText:n,message:"empty braces",position:s});try{o.push(e(n.substring(s+1,r)))}catch(u){if(u.prototype===t.prototype)throw new t({templateText:n,message:u.options.message,position:s+u.options.position,details:u.options});throw u}s=null,a=r+1}}else{if("}"===i)throw new t({templateText:n,message:"unopened brace closed",position:r});"{"===i&&(r>a&&o.push(new f(n.substring(a,r))),a=null,s=r)}if(null!==s)throw new t({templateText:n,message:"unclosed brace",position:s});return n.length>a&&o.push(new f(n.substr(a))),new c(n,o)}return n}(),l=function(){function e(e){return JSON?JSON.stringify(e):e}function t(e){if(!n(e))return!0;if(""===e)return!0;if(r.isArray(e))return 0===e.length;for(var t in e)if(e.hasOwnProperty(t))return!1;return!0}function i(e){var n,t=[];for(n in e)e.hasOwnProperty(n)&&t.push({name:n,value:e[n]});return t}function o(e,n,t){this.templateText=e,this.operator=n,this.varspecs=t}function s(e,n,t){var r="";if(t=""+t,n.named){if(r+=a.encodeLiteral(e.varname),""===t)return r+=n.ifEmpty;r+="="}return null!==e.maxLength&&(t=t.substr(0,e.maxLength)),r+=n.encode(t)}function u(e){return n(e.value)}function f(e,o,s){var f=[],p="";if(o.named){if(p+=a.encodeLiteral(e.varname),t(s))return p+=o.ifEmpty;p+="="}return r.isArray(s)?(f=s,f=r.filter(f,n),f=r.map(f,o.encode),p+=r.join(f,",")):(f=i(s),f=r.filter(f,u),f=r.map(f,function(e){return o.encode(e.name)+","+o.encode(e.value)}),p+=r.join(f,",")),p}function p(e,o,s){var f=r.isArray(s),p=[];return f?(p=s,p=r.filter(p,n),p=r.map(p,function(n){var r=a.encodeLiteral(e.varname);return r+=t(n)?o.ifEmpty:"="+o.encode(n)})):(p=i(s),p=r.filter(p,u),p=r.map(p,function(e){var n=a.encodeLiteral(e.name);return n+=t(e.value)?o.ifEmpty:"="+o.encode(e.value)})),r.join(p,o.separator)}function l(e,t){var o=[],s="";return r.isArray(t)?(o=t,o=r.filter(o,n),o=r.map(o,e.encode),s+=r.join(o,e.separator)):(o=i(t),o=r.filter(o,function(e){return n(e.value)}),o=r.map(o,function(n){return e.encode(n.name)+"="+e.encode(n.value)}),s+=r.join(o,e.separator)),s}return o.prototype.toString=function(){return this.templateText},o.prototype.expand=function(i){var o,a,u,c,h=[],d=!1,m=this.operator;for(o=0;this.varspecs.length>o;o+=1)if(a=this.varspecs[o],u=i[a.varname],null!==u&&void 0!==u)if(a.exploded&&(d=!0),c=r.isArray(u),"string"==typeof u||"number"==typeof u||"boolean"==typeof u)h.push(s(a,m,u));else{if(a.maxLength&&n(u))throw Error("Prefix modifiers are not applicable to variables that have composite values. You tried to expand "+this+" with "+e(u));a.exploded?n(u)&&(m.named?h.push(p(a,m,u)):h.push(l(m,u))):(m.named||!t(u))&&h.push(f(a,m,u))}return 0===h.length?"":m.first+r.join(h,m.separator)},o}(),c=function(){function e(e,n){this.templateText=e,this.expressions=n,r.deepFreeze(this)}return e.prototype.toString=function(){return this.templateText},e.prototype.expand=function(e){var n,t="";for(n=0;this.expressions.length>n;n+=1)t+=this.expressions[n].expand(e);return t},e.parse=p,e.UriTemplateError=t,e}();e(c)})(function(e){"use strict";"undefined"!=typeof module?module.exports=e:"function"==typeof define?define([],function(){return e}):"undefined"!=typeof window?window.UriTemplate=e:global.UriTemplate=e});
\ No newline at end of file \ No newline at end of file
...@@ -8,39 +8,67 @@ ...@@ -8,39 +8,67 @@
(function (exportCallback) { (function (exportCallback) {
"use strict"; "use strict";
var UriTemplateError = (function () {
function UriTemplateError (options) {
this.options = options;
}
UriTemplateError.prototype.toString = function () {
if (JSON && JSON.stringify) {
return JSON.stringify(this.options);
}
else {
return this.options;
}
};
return UriTemplateError;
}());
var objectHelper = (function () { var objectHelper = (function () {
function isArray (value) { function isArray (value) {
return Object.prototype.toString.apply(value) === '[object Array]'; return Object.prototype.toString.apply(value) === '[object Array]';
} }
// performs an array.reduce for objects function join (arr, separator) {
// TODO handling if initialValue is undefined
function objectReduce (object, callback, initialValue) {
var var
propertyName, result = '',
currentValue = initialValue; first = true,
for (propertyName in object) { index;
if (object.hasOwnProperty(propertyName)) { for (index = 0; index < arr.length; index += 1) {
currentValue = callback(currentValue, object[propertyName], propertyName, object); if (first) {
first = false;
} }
else {
result += separator;
}
result += arr[index];
} }
return currentValue; return result;
} }
// performs an array.reduce, if reduce is not present (older browser...) function map (arr, mapper) {
// TODO handling if initialValue is undefined
function arrayReduce (array, callback, initialValue) {
var var
index, result = [],
currentValue = initialValue; index = 0;
for (index = 0; index < array.length; index += 1) { for (; index < arr.length; index += 1) {
currentValue = callback(currentValue, array[index], index, array); result.push(mapper(arr[index]));
} }
return currentValue; return result;
} }
function reduce (arrayOrObject, callback, initialValue) { function filter (arr, predicate) {
return isArray(arrayOrObject) ? arrayReduce(arrayOrObject, callback, initialValue) : objectReduce(arrayOrObject, callback, initialValue); var
result = [],
index = 0;
for (; index < arr.length; index += 1) {
if (predicate(arr[index])) {
result.push(arr[index]);
}
}
return result;
} }
function deepFreezeUsingObjectFreeze (object) { function deepFreezeUsingObjectFreeze (object) {
...@@ -71,22 +99,24 @@ var objectHelper = (function () { ...@@ -71,22 +99,24 @@ var objectHelper = (function () {
return { return {
isArray: isArray, isArray: isArray,
reduce: reduce, join: join,
map: map,
filter: filter,
deepFreeze: deepFreeze deepFreeze: deepFreeze
}; };
}()); }());
var charHelper = (function () { var charHelper = (function () {
function isAlpha(chr) { function isAlpha (chr) {
return (chr >= 'a' && chr <= 'z') || ((chr >= 'A' && chr <= 'Z')); return (chr >= 'a' && chr <= 'z') || ((chr >= 'A' && chr <= 'Z'));
} }
function isDigit(chr) { function isDigit (chr) {
return chr >= '0' && chr <= '9'; return chr >= '0' && chr <= '9';
} }
function isHexDigit(chr) { function isHexDigit (chr) {
return isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F'); return isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F');
} }
...@@ -278,9 +308,38 @@ var encodingHelper = (function () { ...@@ -278,9 +308,38 @@ var encodingHelper = (function () {
return encode(text, true); return encode(text, true);
} }
function encodeLiteralCharacter (literal, index) {
var chr = pctEncoder.pctCharAt(literal, index);
if (chr.length > 1) {
return chr;
}
else {
return rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
}
}
function encodeLiteral (literal) {
var
result = '',
index,
chr = '';
for (index = 0; index < literal.length; index += chr.length) {
chr = pctEncoder.pctCharAt(literal, index);
if (chr.length > 1) {
result += chr;
}
else {
result += rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
}
}
return result;
}
return { return {
encode: encode, encode: encode,
encodePassReserved: encodePassReserved encodePassReserved: encodePassReserved,
encodeLiteral: encodeLiteral,
encodeLiteralCharacter: encodeLiteralCharacter
}; };
}()); }());
...@@ -292,7 +351,7 @@ var operators = (function () { ...@@ -292,7 +351,7 @@ var operators = (function () {
var var
bySymbol = {}; bySymbol = {};
function create(symbol) { function create (symbol) {
bySymbol[symbol] = { bySymbol[symbol] = {
symbol: symbol, symbol: symbol,
separator: (symbol === '?') ? '&' : (symbol === '' || symbol === '+' || symbol === '#') ? ',' : symbol, separator: (symbol === '?') ? '&' : (symbol === '' || symbol === '+' || symbol === '#') ? ',' : symbol,
...@@ -314,15 +373,17 @@ var operators = (function () { ...@@ -314,15 +373,17 @@ var operators = (function () {
create(';'); create(';');
create('?'); create('?');
create('&'); create('&');
return {valueOf: function (chr) { return {
valueOf: function (chr) {
if (bySymbol[chr]) { if (bySymbol[chr]) {
return bySymbol[chr]; return bySymbol[chr];
} }
if ("=,!@|".indexOf(chr) >= 0) { if ("=,!@|".indexOf(chr) >= 0) {
throw new Error('Illegal use of reserved operator "' + chr + '"'); return null;
} }
return bySymbol['']; return bySymbol[''];
}}; }
};
}()); }());
...@@ -331,25 +392,20 @@ var operators = (function () { ...@@ -331,25 +392,20 @@ var operators = (function () {
* Section 2.3 of the RFC makes clear defintions: * Section 2.3 of the RFC makes clear defintions:
* * undefined and null are not defined. * * undefined and null are not defined.
* * the empty string is defined * * the empty string is defined
* * an array ("list") is defined, if it contains at least one defined element * * an array ("list") is defined, if it is not empty (even if all elements are not defined)
* * an object ("map") is defined, if it contains at least one defined property * * an object ("map") is defined, if it contains at least one property with defined value
* @param object * @param object
* @return {Boolean} * @return {Boolean}
*/ */
function isDefined (object) { function isDefined (object) {
var var
index,
propertyName; propertyName;
if (object === null || object === undefined) { if (object === null || object === undefined) {
return false; return false;
} }
if (objectHelper.isArray(object)) { if (objectHelper.isArray(object)) {
for (index = 0; index < object.length; index += 1) { // Section 2.3: A variable defined as a list value is considered undefined if the list contains zero members
if (isDefined(object[index])) { return object.length > 0;
return true;
}
}
return false;
} }
if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") { if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") {
// falsy values like empty strings, false or 0 are "defined" // falsy values like empty strings, false or 0 are "defined"
...@@ -366,27 +422,8 @@ function isDefined (object) { ...@@ -366,27 +422,8 @@ function isDefined (object) {
var LiteralExpression = (function () { var LiteralExpression = (function () {
function LiteralExpression (literal) { function LiteralExpression (literal) {
this.literal = LiteralExpression.encodeLiteral(literal); this.literal = encodingHelper.encodeLiteral(literal);
}
LiteralExpression.encodeLiteral = function (literal) {
var
result = '',
index,
chr = '';
for (index = 0; index < literal.length; index += chr.length) {
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;
};
LiteralExpression.prototype.expand = function () { LiteralExpression.prototype.expand = function () {
return this.literal; return this.literal;
...@@ -398,9 +435,9 @@ var LiteralExpression = (function () { ...@@ -398,9 +435,9 @@ var LiteralExpression = (function () {
}()); }());
var parse = (function () { var parse = (function () {
function parseExpression (outerText) {
function parseExpression (expressionText) {
var var
text,
operator, operator,
varspecs = [], varspecs = [],
varspec = null, varspec = null,
...@@ -410,34 +447,42 @@ var parse = (function () { ...@@ -410,34 +447,42 @@ var parse = (function () {
chr = ''; chr = '';
function closeVarname () { function closeVarname () {
varspec = {varname: text.substring(varnameStart, index), exploded: false, maxLength: null}; var varname = expressionText.substring(varnameStart, index);
if (varname.length === 0) {
throw new UriTemplateError({expressionText: expressionText, message: "a varname must be specified", position: index});
}
varspec = {varname: varname, exploded: false, maxLength: null};
varnameStart = null; varnameStart = null;
} }
function closeMaxLength () { function closeMaxLength () {
if (maxLengthStart === index) { if (maxLengthStart === index) {
throw new Error("after a ':' you have to specify the length. position = " + index); throw new UriTemplateError({expressionText: expressionText, message: "after a ':' you have to specify the length", position: index});
} }
varspec.maxLength = parseInt(text.substring(maxLengthStart, index), 10); varspec.maxLength = parseInt(expressionText.substring(maxLengthStart, index), 10);
maxLengthStart = null; maxLengthStart = null;
} }
// remove outer braces operator = (function (operatorText) {
text = outerText.substr(1, outerText.length - 2); var op = operators.valueOf(operatorText);
if (op === null) {
throw new UriTemplateError({expressionText: expressionText, message: "illegal use of reserved operator", position: index, operator: operatorText});
}
return op;
}(expressionText.charAt(0)));
index = operator.symbol.length;
// determine operator
operator = operators.valueOf(text.charAt(0));
index = (operator.symbol === '') ? 0 : 1;
varnameStart = index; varnameStart = index;
for (; index < text.length; index += chr.length) { for (; index < expressionText.length; index += chr.length) {
chr = pctEncoder.pctCharAt(text, index); chr = pctEncoder.pctCharAt(expressionText, index);
if (varnameStart !== null) { if (varnameStart !== null) {
// the spec says: varname = varchar *( ["."] varchar ) // the spec says: varname = varchar *( ["."] varchar )
// so a dot is allowed except for the first char // so a dot is allowed except for the first char
if (chr === '.') { if (chr === '.') {
if (varnameStart === index) { if (varnameStart === index) {
throw new Error('a varname MUST NOT start with a dot -- see position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "a varname MUST NOT start with a dot", position: index});
} }
continue; continue;
} }
...@@ -448,11 +493,11 @@ var parse = (function () { ...@@ -448,11 +493,11 @@ var parse = (function () {
} }
if (maxLengthStart !== null) { if (maxLengthStart !== null) {
if (index === maxLengthStart && chr === '0') { if (index === maxLengthStart && chr === '0') {
throw new Error('A :prefix must not start with digit 0 -- see position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must not start with digit 0", position: index});
} }
if (charHelper.isDigit(chr)) { if (charHelper.isDigit(chr)) {
if (index - maxLengthStart >= 4) { if (index - maxLengthStart >= 4) {
throw new Error('A :prefix must max 4 digits -- see position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must have max 4 digits", position: index});
} }
continue; continue;
} }
...@@ -460,20 +505,23 @@ var parse = (function () { ...@@ -460,20 +505,23 @@ var parse = (function () {
} }
if (chr === ':') { if (chr === ':') {
if (varspec.maxLength !== null) { if (varspec.maxLength !== null) {
throw new Error('only one :maxLength is allowed per varspec at position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "only one :maxLength is allowed per varspec", position: index});
}
if (varspec.exploded) {
throw new UriTemplateError({expressionText: expressionText, message: "an exploeded varspec MUST NOT be varspeced", position: index});
} }
maxLengthStart = index + 1; maxLengthStart = index + 1;
continue; continue;
} }
if (chr === '*') { if (chr === '*') {
if (varspec === null) { if (varspec === null) {
throw new Error('explode exploded at position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "exploded without varspec", position: index});
} }
if (varspec.exploded) { if (varspec.exploded) {
throw new Error('explode exploded twice at position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "exploded twice", position: index});
} }
if (varspec.maxLength) { if (varspec.maxLength) {
throw new Error('an explode (*) MUST NOT follow to a prefix, see position ' + index); throw new UriTemplateError({expressionText: expressionText, message: "an explode (*) MUST NOT follow to a prefix", position: index});
} }
varspec.exploded = true; varspec.exploded = true;
continue; continue;
...@@ -485,7 +533,7 @@ var parse = (function () { ...@@ -485,7 +533,7 @@ var parse = (function () {
varnameStart = index + 1; varnameStart = index + 1;
continue; continue;
} }
throw new Error("illegal character '" + chr + "' at position " + index + ' of "' + text + '"'); throw new UriTemplateError({expressionText: expressionText, message: "illegal character", character: chr, position: index});
} // for chr } // for chr
if (varnameStart !== null) { if (varnameStart !== null) {
closeVarname(); closeVarname();
...@@ -494,10 +542,10 @@ var parse = (function () { ...@@ -494,10 +542,10 @@ var parse = (function () {
closeMaxLength(); closeMaxLength();
} }
varspecs.push(varspec); varspecs.push(varspec);
return new VariableExpression(outerText, operator, varspecs); return new VariableExpression(expressionText, operator, varspecs);
} }
function parseTemplate (uriTemplateText) { function parse (uriTemplateText) {
// assert filled string // assert filled string
var var
index, index,
...@@ -509,7 +557,7 @@ var parse = (function () { ...@@ -509,7 +557,7 @@ var parse = (function () {
chr = uriTemplateText.charAt(index); chr = uriTemplateText.charAt(index);
if (literalStart !== null) { if (literalStart !== null) {
if (chr === '}') { if (chr === '}') {
throw new Error('brace was closed in position ' + index + " but never opened"); throw new UriTemplateError({templateText: uriTemplateText, message: "unopened brace closed", position: index});
} }
if (chr === '{') { if (chr === '{') {
if (literalStart < index) { if (literalStart < index) {
...@@ -524,13 +572,21 @@ var parse = (function () { ...@@ -524,13 +572,21 @@ var parse = (function () {
if (braceOpenIndex !== null) { if (braceOpenIndex !== null) {
// here just { is forbidden // here just { is forbidden
if (chr === '{') { if (chr === '{') {
throw new Error('brace was opened in position ' + braceOpenIndex + " and cannot be reopened in position " + index); throw new UriTemplateError({templateText: uriTemplateText, message: "brace already opened", position: index});
} }
if (chr === '}') { if (chr === '}') {
if (braceOpenIndex + 1 === index) { if (braceOpenIndex + 1 === index) {
throw new Error("empty braces on position " + braceOpenIndex); throw new UriTemplateError({templateText: uriTemplateText, message: "empty braces", position: braceOpenIndex});
}
try {
expressions.push(parseExpression(uriTemplateText.substring(braceOpenIndex + 1, index)));
}
catch (error) {
if (error.prototype === UriTemplateError.prototype) {
throw new UriTemplateError({templateText: uriTemplateText, message: error.options.message, position: braceOpenIndex + error.options.position, details: error.options});
}
throw error;
} }
expressions.push(parseExpression(uriTemplateText.substring(braceOpenIndex, index + 1)));
braceOpenIndex = null; braceOpenIndex = null;
literalStart = index + 1; literalStart = index + 1;
} }
...@@ -539,7 +595,7 @@ var parse = (function () { ...@@ -539,7 +595,7 @@ var parse = (function () {
throw new Error('reached unreachable code'); throw new Error('reached unreachable code');
} }
if (braceOpenIndex !== null) { if (braceOpenIndex !== null) {
throw new Error("brace was opened on position " + braceOpenIndex + ", but never closed"); throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex});
} }
if (literalStart < uriTemplateText.length) { if (literalStart < uriTemplateText.length) {
expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart))); expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart)));
...@@ -547,7 +603,7 @@ var parse = (function () { ...@@ -547,7 +603,7 @@ var parse = (function () {
return new UriTemplate(uriTemplateText, expressions); return new UriTemplate(uriTemplateText, expressions);
} }
return parseTemplate; return parse;
}()); }());
var VariableExpression = (function () { var VariableExpression = (function () {
...@@ -556,6 +612,36 @@ var VariableExpression = (function () { ...@@ -556,6 +612,36 @@ var VariableExpression = (function () {
return JSON ? JSON.stringify(value) : value; return JSON ? JSON.stringify(value) : value;
} }
function isEmpty (value) {
if (!isDefined(value)) {
return true;
}
if (value === '') {
return true;
}
if (objectHelper.isArray(value)) {
return value.length === 0;
}
for (var propertyName in value) {
if (value.hasOwnProperty(propertyName)) {
return false;
}
}
return true;
}
function propertyArray (object) {
var
result = [],
propertyName;
for (propertyName in object) {
if (object.hasOwnProperty(propertyName)) {
result.push({name: propertyName, value: object[propertyName]});
}
}
return result;
}
function VariableExpression (templateText, operator, varspecs) { function VariableExpression (templateText, operator, varspecs) {
this.templateText = templateText; this.templateText = templateText;
this.operator = operator; this.operator = operator;
...@@ -566,121 +652,167 @@ var VariableExpression = (function () { ...@@ -566,121 +652,167 @@ var VariableExpression = (function () {
return this.templateText; return this.templateText;
}; };
VariableExpression.prototype.expand = function (variables) { function expandSimpleValue(varspec, operator, value) {
var var result = '';
result = '', value = value.toString();
index, if (operator.named) {
varspec, result += encodingHelper.encodeLiteral(varspec.varname);
value, if (value === '') {
valueIsArr, result += operator.ifEmpty;
isFirstVarspec = true, return result;
operator = this.operator;
// callback to be used within array.reduce
function reduceUnexploded (result, currentValue, currentKey) {
if (isDefined(currentValue)) {
if (result.length > 0) {
result += ',';
} }
if (!valueIsArr) { result += '=';
result += operator.encode(currentKey) + ',';
} }
result += operator.encode(currentValue); if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
} }
result += operator.encode(value);
return result; return result;
} }
function reduceNamedExploded (result, currentValue, currentKey) { function valueDefined (nameValue) {
if (isDefined(currentValue)) { return isDefined(nameValue.value);
if (result.length > 0) { }
result += operator.separator;
function expandNotExploded(varspec, operator, value) {
var
arr = [],
result = '';
if (operator.named) {
result += encodingHelper.encodeLiteral(varspec.varname);
if (isEmpty(value)) {
result += operator.ifEmpty;
return result;
}
result += '=';
}
if (objectHelper.isArray(value)) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, operator.encode);
result += objectHelper.join(arr, ',');
} }
result += (valueIsArr) ? LiteralExpression.encodeLiteral(varspec.varname) : operator.encode(currentKey); else {
result += '=' + operator.encode(currentValue); arr = propertyArray(value);
arr = objectHelper.filter(arr, valueDefined);
arr = objectHelper.map(arr, function (nameValue) {
return operator.encode(nameValue.name) + ',' + operator.encode(nameValue.value);
});
result += objectHelper.join(arr, ',');
} }
return result; return result;
} }
function reduceUnnamedExploded (result, currentValue, currentKey) { function expandExplodedNamed (varspec, operator, value) {
if (isDefined(currentValue)) { var
if (result.length > 0) { isArray = objectHelper.isArray(value),
result += operator.separator; arr = [];
if (isArray) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, function (listElement) {
var tmp = encodingHelper.encodeLiteral(varspec.varname);
if (isEmpty(listElement)) {
tmp += operator.ifEmpty;
}
else {
tmp += '=' + operator.encode(listElement);
}
return tmp;
});
} }
if (!valueIsArr) { else {
result += operator.encode(currentKey) + '='; arr = propertyArray(value);
arr = objectHelper.filter(arr, valueDefined);
arr = objectHelper.map(arr, function (nameValue) {
var tmp = encodingHelper.encodeLiteral(nameValue.name);
if (isEmpty(nameValue.value)) {
tmp += operator.ifEmpty;
}
else {
tmp += '=' + operator.encode(nameValue.value);
} }
result += operator.encode(currentValue); return tmp;
});
}
return objectHelper.join(arr, operator.separator);
}
function expandExplodedUnnamed (operator, value) {
var
arr = [],
result = '';
if (objectHelper.isArray(value)) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, operator.encode);
result += objectHelper.join(arr, operator.separator);
}
else {
arr = propertyArray(value);
arr = objectHelper.filter(arr, function (nameValue) {
return isDefined(nameValue.value);
});
arr = objectHelper.map(arr, function (nameValue) {
return operator.encode(nameValue.name) + '=' + operator.encode(nameValue.value);
});
result += objectHelper.join(arr, operator.separator);
} }
return result; return result;
} }
VariableExpression.prototype.expand = function (variables) {
var
expanded = [],
index,
varspec,
value,
valueIsArr,
oneExploded = false,
operator = this.operator;
// expand each varspec and join with operator's separator // expand each varspec and join with operator's separator
for (index = 0; index < this.varspecs.length; index += 1) { for (index = 0; index < this.varspecs.length; index += 1) {
varspec = this.varspecs[index]; varspec = this.varspecs[index];
value = variables[varspec.varname]; value = variables[varspec.varname];
if (!isDefined(value)) { // if (!isDefined(value)) {
// if (variables.hasOwnProperty(varspec.name)) {
if (value === null || value === undefined) {
continue; continue;
} }
if (isFirstVarspec) { if (varspec.exploded) {
result += operator.first; oneExploded = true;
isFirstVarspec = false;
}
else {
result += operator.separator;
} }
valueIsArr = objectHelper.isArray(value); valueIsArr = objectHelper.isArray(value);
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
value = value.toString(); expanded.push(expandSimpleValue(varspec, operator, value));
if (operator.named) {
result += LiteralExpression.encodeLiteral(varspec.varname);
if (value === '') {
result += operator.ifEmpty;
continue;
}
result += '=';
}
if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
}
result += operator.encode(value);
} }
else if (varspec.maxLength) { else if (varspec.maxLength && isDefined(value)) {
// 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values." // 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values."
throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + prettyPrint(value)); throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + prettyPrint(value));
} }
else if (!varspec.exploded) { else if (!varspec.exploded) {
if (operator.named) { if (operator.named || !isEmpty(value)) {
result += LiteralExpression.encodeLiteral(varspec.varname); expanded.push(expandNotExploded(varspec, operator, value));
if (!isDefined(value)) {
result += operator.ifEmpty;
continue;
} }
result += '=';
} }
result += objectHelper.reduce(value, reduceUnexploded, ''); else if (isDefined(value)) {
if (operator.named) {
expanded.push(expandExplodedNamed(varspec, operator, value));
} }
else { else {
// exploded and not string expanded.push(expandExplodedUnnamed(operator, value));
result += objectHelper.reduce(value, operator.named ? reduceNamedExploded : reduceUnnamedExploded, '');
} }
} }
if (isFirstVarspec) {
// so no varspecs produced output.
var oneExploded = false;
for (index = 0; index < this.varspecs.length; index += 1) {
if (this.varspecs[index].exploded) {
oneExploded = true;
break;
} }
if (expanded.length === 0) {
return "";
} }
if (operator.named && !oneExploded) { else {
result += operator.symbol; return operator.first + objectHelper.join(expanded, operator.separator);
result += varspec.varname + operator.ifEmpty;
}
} }
return result;
}; };
return VariableExpression; return VariableExpression;
...@@ -709,6 +841,7 @@ var UriTemplate = (function () { ...@@ -709,6 +841,7 @@ var UriTemplate = (function () {
}; };
UriTemplate.parse = parse; UriTemplate.parse = parse;
UriTemplate.UriTemplateError = UriTemplateError;
return UriTemplate; return UriTemplate;
}()); }());
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"uritemplate-test/spec-examples-by-sections.json", "uritemplate-test/spec-examples-by-sections.json",
"uritemplate-test/spec-examples.json" "uritemplate-test/spec-examples.json"
], ],
"version": "0.3.0", "version": "0.3.1",
"readmeFilename": "README.md", "readmeFilename": "README.md",
"gitHead": "901b85201a821427dfb4591b56aea3a70d45c67c", "gitHead": "901b85201a821427dfb4591b56aea3a70d45c67c",
"devDependencies": { "devDependencies": {
......
...@@ -7,6 +7,36 @@ var VariableExpression = (function () { ...@@ -7,6 +7,36 @@ var VariableExpression = (function () {
return JSON ? JSON.stringify(value) : value; return JSON ? JSON.stringify(value) : value;
} }
function isEmpty (value) {
if (!isDefined(value)) {
return true;
}
if (value === '') {
return true;
}
if (objectHelper.isArray(value)) {
return value.length === 0;
}
for (var propertyName in value) {
if (value.hasOwnProperty(propertyName)) {
return false;
}
}
return true;
}
function propertyArray (object) {
var
result = [],
propertyName;
for (propertyName in object) {
if (object.hasOwnProperty(propertyName)) {
result.push({name: propertyName, value: object[propertyName]});
}
}
return result;
}
function VariableExpression (templateText, operator, varspecs) { function VariableExpression (templateText, operator, varspecs) {
this.templateText = templateText; this.templateText = templateText;
this.operator = operator; this.operator = operator;
...@@ -17,121 +47,167 @@ var VariableExpression = (function () { ...@@ -17,121 +47,167 @@ var VariableExpression = (function () {
return this.templateText; return this.templateText;
}; };
VariableExpression.prototype.expand = function (variables) { function expandSimpleValue(varspec, operator, value) {
var var result = '';
result = '', value = value.toString();
index, if (operator.named) {
varspec, result += encodingHelper.encodeLiteral(varspec.varname);
value, if (value === '') {
valueIsArr, result += operator.ifEmpty;
isFirstVarspec = true, return result;
operator = this.operator;
// callback to be used within array.reduce
function reduceUnexploded (result, currentValue, currentKey) {
if (isDefined(currentValue)) {
if (result.length > 0) {
result += ',';
} }
if (!valueIsArr) { result += '=';
result += operator.encode(currentKey) + ',';
} }
result += operator.encode(currentValue); if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
} }
result += operator.encode(value);
return result; return result;
} }
function reduceNamedExploded (result, currentValue, currentKey) { function valueDefined (nameValue) {
if (isDefined(currentValue)) { return isDefined(nameValue.value);
if (result.length > 0) { }
result += operator.separator;
function expandNotExploded(varspec, operator, value) {
var
arr = [],
result = '';
if (operator.named) {
result += encodingHelper.encodeLiteral(varspec.varname);
if (isEmpty(value)) {
result += operator.ifEmpty;
return result;
}
result += '=';
}
if (objectHelper.isArray(value)) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, operator.encode);
result += objectHelper.join(arr, ',');
} }
result += (valueIsArr) ? encodingHelper.encodeLiteral(varspec.varname) : operator.encode(currentKey); else {
result += '=' + operator.encode(currentValue); arr = propertyArray(value);
arr = objectHelper.filter(arr, valueDefined);
arr = objectHelper.map(arr, function (nameValue) {
return operator.encode(nameValue.name) + ',' + operator.encode(nameValue.value);
});
result += objectHelper.join(arr, ',');
} }
return result; return result;
} }
function reduceUnnamedExploded (result, currentValue, currentKey) { function expandExplodedNamed (varspec, operator, value) {
if (isDefined(currentValue)) { var
if (result.length > 0) { isArray = objectHelper.isArray(value),
result += operator.separator; arr = [];
if (isArray) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, function (listElement) {
var tmp = encodingHelper.encodeLiteral(varspec.varname);
if (isEmpty(listElement)) {
tmp += operator.ifEmpty;
}
else {
tmp += '=' + operator.encode(listElement);
}
return tmp;
});
}
else {
arr = propertyArray(value);
arr = objectHelper.filter(arr, valueDefined);
arr = objectHelper.map(arr, function (nameValue) {
var tmp = encodingHelper.encodeLiteral(nameValue.name);
if (isEmpty(nameValue.value)) {
tmp += operator.ifEmpty;
}
else {
tmp += '=' + operator.encode(nameValue.value);
} }
if (!valueIsArr) { return tmp;
result += operator.encode(currentKey) + '='; });
} }
result += operator.encode(currentValue); return objectHelper.join(arr, operator.separator);
}
function expandExplodedUnnamed (operator, value) {
var
arr = [],
result = '';
if (objectHelper.isArray(value)) {
arr = value;
arr = objectHelper.filter(arr, isDefined);
arr = objectHelper.map(arr, operator.encode);
result += objectHelper.join(arr, operator.separator);
}
else {
arr = propertyArray(value);
arr = objectHelper.filter(arr, function (nameValue) {
return isDefined(nameValue.value);
});
arr = objectHelper.map(arr, function (nameValue) {
return operator.encode(nameValue.name) + '=' + operator.encode(nameValue.value);
});
result += objectHelper.join(arr, operator.separator);
} }
return result; return result;
} }
VariableExpression.prototype.expand = function (variables) {
var
expanded = [],
index,
varspec,
value,
valueIsArr,
oneExploded = false,
operator = this.operator;
// expand each varspec and join with operator's separator // expand each varspec and join with operator's separator
for (index = 0; index < this.varspecs.length; index += 1) { for (index = 0; index < this.varspecs.length; index += 1) {
varspec = this.varspecs[index]; varspec = this.varspecs[index];
value = variables[varspec.varname]; value = variables[varspec.varname];
if (!isDefined(value)) { // if (!isDefined(value)) {
// if (variables.hasOwnProperty(varspec.name)) {
if (value === null || value === undefined) {
continue; continue;
} }
if (isFirstVarspec) { if (varspec.exploded) {
result += operator.first; oneExploded = true;
isFirstVarspec = false;
}
else {
result += operator.separator;
} }
valueIsArr = objectHelper.isArray(value); valueIsArr = objectHelper.isArray(value);
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
value = value.toString(); expanded.push(expandSimpleValue(varspec, operator, value));
if (operator.named) {
result += encodingHelper.encodeLiteral(varspec.varname);
if (value === '') {
result += operator.ifEmpty;
continue;
}
result += '=';
}
if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
}
result += operator.encode(value);
} }
else if (varspec.maxLength) { else if (varspec.maxLength && isDefined(value)) {
// 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values." // 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values."
throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + prettyPrint(value)); throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + prettyPrint(value));
} }
else if (!varspec.exploded) { else if (!varspec.exploded) {
if (operator.named) { if (operator.named || !isEmpty(value)) {
result += encodingHelper.encodeLiteral(varspec.varname); expanded.push(expandNotExploded(varspec, operator, value));
if (!isDefined(value)) {
result += operator.ifEmpty;
continue;
} }
result += '=';
} }
result += objectHelper.reduce(value, reduceUnexploded, ''); else if (isDefined(value)) {
if (operator.named) {
expanded.push(expandExplodedNamed(varspec, operator, value));
} }
else { else {
// exploded and not string expanded.push(expandExplodedUnnamed(operator, value));
result += objectHelper.reduce(value, operator.named ? reduceNamedExploded : reduceUnnamedExploded, '');
}
} }
if (isFirstVarspec) {
// so no varspecs produced output.
var oneExploded = false;
for (index = 0; index < this.varspecs.length; index += 1) {
if (this.varspecs[index].exploded) {
oneExploded = true;
break;
} }
} }
if (operator.named && !oneExploded) {
result += operator.symbol; if (expanded.length === 0) {
result += varspec.varname + operator.ifEmpty; return "";
} }
else {
return operator.first + objectHelper.join(expanded, operator.separator);
} }
return result;
}; };
return VariableExpression; return VariableExpression;
......
...@@ -25,6 +25,16 @@ var encodingHelper = (function () { ...@@ -25,6 +25,16 @@ var encodingHelper = (function () {
return encode(text, true); return encode(text, true);
} }
function encodeLiteralCharacter (literal, index) {
var chr = pctEncoder.pctCharAt(literal, index);
if (chr.length > 1) {
return chr;
}
else {
return rfcCharHelper.isReserved(chr) || rfcCharHelper.isUnreserved(chr) ? chr : pctEncoder.encodeCharacter(chr);
}
}
function encodeLiteral (literal) { function encodeLiteral (literal) {
var var
result = '', result = '',
...@@ -45,7 +55,8 @@ var encodingHelper = (function () { ...@@ -45,7 +55,8 @@ var encodingHelper = (function () {
return { return {
encode: encode, encode: encode,
encodePassReserved: encodePassReserved, encodePassReserved: encodePassReserved,
encodeLiteral: encodeLiteral encodeLiteral: encodeLiteral,
encodeLiteralCharacter: encodeLiteralCharacter
}; };
}()); }());
...@@ -6,26 +6,21 @@ ...@@ -6,26 +6,21 @@
* Section 2.3 of the RFC makes clear defintions: * Section 2.3 of the RFC makes clear defintions:
* * undefined and null are not defined. * * undefined and null are not defined.
* * the empty string is defined * * the empty string is defined
* * an array ("list") is defined, if it contains at least one defined element * * an array ("list") is defined, if it is not empty (even if all elements are not defined)
* * an object ("map") is defined, if it contains at least one defined property * * an object ("map") is defined, if it contains at least one property with defined value
* @param object * @param object
* @return {Boolean} * @return {Boolean}
*/ */
function isDefined (object) { function isDefined (object) {
"use strict"; "use strict";
var var
index,
propertyName; propertyName;
if (object === null || object === undefined) { if (object === null || object === undefined) {
return false; return false;
} }
if (objectHelper.isArray(object)) { if (objectHelper.isArray(object)) {
for (index = 0; index < object.length; index += 1) { // Section 2.3: A variable defined as a list value is considered undefined if the list contains zero members
if (isDefined(object[index])) { return object.length > 0;
return true;
}
}
return false;
} }
if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") { if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") {
// falsy values like empty strings, false or 0 are "defined" // falsy values like empty strings, false or 0 are "defined"
......
...@@ -5,34 +5,43 @@ var objectHelper = (function () { ...@@ -5,34 +5,43 @@ var objectHelper = (function () {
return Object.prototype.toString.apply(value) === '[object Array]'; return Object.prototype.toString.apply(value) === '[object Array]';
} }
// performs an array.reduce for objects function join (arr, separator) {
// TODO handling if initialValue is undefined
function objectReduce (object, callback, initialValue) {
var var
propertyName, result = '',
currentValue = initialValue; first = true,
for (propertyName in object) { index;
if (object.hasOwnProperty(propertyName)) { for (index = 0; index < arr.length; index += 1) {
currentValue = callback(currentValue, object[propertyName], propertyName, object); if (first) {
first = false;
}
else {
result += separator;
} }
result += arr[index];
} }
return currentValue; return result;
} }
// performs an array.reduce, if reduce is not present (older browser...) function map (arr, mapper) {
// TODO handling if initialValue is undefined
function arrayReduce (array, callback, initialValue) {
var var
index, result = [],
currentValue = initialValue; index = 0;
for (index = 0; index < array.length; index += 1) { for (; index < arr.length; index += 1) {
currentValue = callback(currentValue, array[index], index, array); result.push(mapper(arr[index]));
} }
return currentValue; return result;
} }
function reduce (arrayOrObject, callback, initialValue) { function filter (arr, predicate) {
return isArray(arrayOrObject) ? arrayReduce(arrayOrObject, callback, initialValue) : objectReduce(arrayOrObject, callback, initialValue); var
result = [],
index = 0;
for (; index < arr.length; index += 1) {
if (predicate(arr[index])) {
result.push(arr[index]);
}
}
return result;
} }
function deepFreezeUsingObjectFreeze (object) { function deepFreezeUsingObjectFreeze (object) {
...@@ -63,7 +72,9 @@ var objectHelper = (function () { ...@@ -63,7 +72,9 @@ var objectHelper = (function () {
return { return {
isArray: isArray, isArray: isArray,
reduce: reduce, join: join,
map: map,
filter: filter,
deepFreeze: deepFreeze deepFreeze: deepFreeze
}; };
}()); }());
...@@ -14,7 +14,7 @@ module.exports = (function () { ...@@ -14,7 +14,7 @@ module.exports = (function () {
} }
function loadUriTemplate () { function loadUriTemplate () {
var context = {module: {}}; var context = {module: {}, console: console};
sandbox(global.URI_TEMPLATE_FILE, context); sandbox(global.URI_TEMPLATE_FILE, context);
return context.module.exports; return context.module.exports;
} }
......
module.exports = (function () {
"use strict";
var isDefined = (function () {
var context = {};
require('nodeunit').utils.sandbox('src/objectHelper.js', context);
require('nodeunit').utils.sandbox('src/isDefined.js', context);
return context.isDefined;
}());
return {
'special "undefined" value, such as undef or null are not "defined"': function (test) {
test.ok(!isDefined(null));
test.ok(!isDefined(undefined));
test.done();
},
'A variable value that is a string of length zero is not considered undefined': function (test) {
test.ok(isDefined(''));
test.done();
},
'A variable defined as a list value is considered undefined if the list contains zero members': {
/*
'empty array is undefined': function (test) {
test.ok(!isDefined([]));
test.done();
},
*/
'array containing only null is defined': function (test) {
test.ok(isDefined([null]));
test.done();
},
'array containing only undefined is defined': function (test) {
test.ok(isDefined([undefined]));
test.done();
},
'array containing only empty array is defined': function (test) {
test.ok(isDefined([
[]
]));
test.done();
},
'array containing only empty object is defined': function (test) {
test.ok(isDefined([
{}
]));
test.done();
},
'array containing empty string is defined': function (test) {
test.ok(isDefined(['']));
test.done();
}
},
// propably the longest id I ever wrote. The text is from the RFC
'A variable defined as an associative array of (name, value) pairs is considered undefined if the array contains zero members or if all member names in the array are associated with undefined values': {
'the empty object is not "defined"': function (test) {
test.ok(!isDefined({}));
test.done();
},
'null is a pretty member name': function (test) {
test.ok(isDefined({null: 1}));
test.done();
},
'not defined if all values are not "defined': function (test) {
test.ok(!isDefined({a: null, b: undefined, c: {}}));
test.done();
}
}
};
}());
...@@ -6,30 +6,6 @@ module.exports = (function () { ...@@ -6,30 +6,6 @@ module.exports = (function () {
var objectHelper = context.objectHelper; var objectHelper = context.objectHelper;
return { return {
'reduce works with initial value': function (test) {
var callNum = 0;
var result = objectHelper.reduce({a: 1, b: 2, c: 17}, function (current, value, name) {
if (callNum === 0) {
test.equal(current, -1);
test.equal(value, 1);
test.equal(name, 'a');
}
else if (callNum === 1) {
test.equal(current, 1);
test.equal(value, 2);
test.equal(name, 'b');
}
else {
test.equal(current, 2);
test.equal(value, 17);
test.equal(name, 'c');
}
callNum += 1;
return Math.max(current, value);
}, -1);
test.equal(result, 17);
test.done();
},
'deepFreeze': { 'deepFreeze': {
'deepFreeze freezes an object': function (test) { 'deepFreeze freezes an object': function (test) {
var object = {}; var object = {};
......
module.exports = (function () { module.exports = (function () {
"use strict"; "use strict";
var var
sandbox = require('nodeunit').utils.sandbox, sandbox = require('nodeunit').utils.sandbox,
context = {}; context = {console: console};
sandbox('src/objectHelper.js', context); sandbox('src/objectHelper.js', context);
sandbox('src/charHelper.js', context); sandbox('src/charHelper.js', context);
sandbox('src/pctEncoder.js', context); sandbox('src/pctEncoder.js', context);
...@@ -18,6 +17,65 @@ module.exports = (function () { ...@@ -18,6 +17,65 @@ module.exports = (function () {
var VariableExpression = context.VariableExpression; var VariableExpression = context.VariableExpression;
return { return {
'unexploded': {
'empty is in list': function (test) {
var ve = new VariableExpression("{x,empty}", operators.valueOf(''), [
{varname: 'x', exploded: false, maxLength: null},
{varname: 'empty', exploded: false, maxLength: null}
]);
test.equal(ve.expand({x: 'x', empty:''}), 'x,');
test.done();
},
'null is not in list': function (test) {
var ve = new VariableExpression("{x,undef}", operators.valueOf(''), [
{varname: 'x', exploded: false, maxLength: null},
{varname: 'empty', exploded: false, maxLength: null}
]);
test.equal(ve.expand({x: 'x', undef: null}), 'x');
test.done();
},
'when empty list and not named, the operator is not printed': function (test) {
var ve = new VariableExpression("{.empty_list}", operators.valueOf('.'), [
{varname: 'empty_list', exploded: false, maxLength: null}
]);
test.equal(ve.expand({empty_list: []}), '');
test.done();
},
'when empty list and named, the operator is printed': function (test) {
var ve = new VariableExpression("{?empty_list}", operators.valueOf('?'), [
{varname: 'empty_list', exploded: false, maxLength: null}
]);
test.equal(ve.expand({empty_list: []}), '?empty_list=');
test.done();
}
},
'exploded': {
'unnamed': {
'a map shows a key-val list': function (test) {
var ve = new VariableExpression("{keys*}", operators.valueOf(''), [
{varname: 'keys', exploded: true, maxLength: null}
]);
test.equal(ve.expand({keys: {a: 'a', b: 'b', c: 'c'}}), 'a=a,b=b,c=c');
test.done();
},
'a empty map prints no operator': function (test) {
var ve = new VariableExpression("{.empty_map*}", operators.valueOf('.'), [
{varname: 'empty_map', exploded: true, maxLength: null}
]);
test.equal(ve.expand({empty_map: {}}), '');
test.done();
}
},
'named': {
'a named, exploded list repeats the name': function (test) {
var ve = new VariableExpression("{;count*}", operators.valueOf(';'), [
{varname: 'count', exploded: true, maxLength: null}
]);
test.equal(ve.expand({count: ['one', 'two', 'three']}), ';count=one;count=two;count=three');
test.done();
}
}
},
"there must be no separator at the end of the level3 list": function (test) { "there must be no separator at the end of the level3 list": function (test) {
var ve = new VariableExpression("{+x,y}", operators.valueOf('+'), [ var ve = new VariableExpression("{+x,y}", operators.valueOf('+'), [
{varname: 'x', exploded: false, maxLength: null}, {varname: 'x', exploded: false, maxLength: null},
...@@ -28,17 +86,17 @@ module.exports = (function () { ...@@ -28,17 +86,17 @@ module.exports = (function () {
test.done(); test.done();
}, },
"empty lists with ? must show the name": function (test) { "empty lists with ? must show the name": function (test) {
var ve = new VariableExpression("{?empty}", operators.valueOf('?'), [ var ve = new VariableExpression("{?empty_list}", operators.valueOf('?'), [
{varname: 'empty', exploded: false, maxLength: null} {varname: 'empty_list', exploded: false, maxLength: null}
]); ]);
test.equal(ve.expand({empty: {}}), '?empty='); test.equal(ve.expand({empty_list: {}}), '?empty_list=');
test.done(); test.done();
}, },
"exploded empty lists with ? must not show the name": function (test) { "exploded empty lists with ? must not show the name": function (test) {
var ve = new VariableExpression("{?empty*}", operators.valueOf('?'), [ var ve = new VariableExpression("{?empty_list*}", operators.valueOf('?'), [
{varname: 'empty', exploded: true, maxLength: null} {varname: 'empty_list', exploded: true, maxLength: null}
]); ]);
test.equal(ve.expand({empty: {}}), ''); test.equal(ve.expand({empty_list: []}), '');
test.done(); test.done();
}, },
"double encode if ?": function (test) { "double encode if ?": function (test) {
...@@ -61,6 +119,23 @@ module.exports = (function () { ...@@ -61,6 +119,23 @@ module.exports = (function () {
]); ]);
test.equal(ve.expand({one: 'two'}), 't'); test.equal(ve.expand({one: 'two'}), 't');
test.done(); test.done();
},
'query expression with 1 varname will expand to empty, if data is undef': function (test) {
var ve = new VariableExpression("{?a}", operators.valueOf('?'), [
{varname: 'a', exploded: false}
]);
test.equal(ve.expand({}), '');
test.done();
},
'query expression with more than 1 varname will expand to empty, if data is undef': function (test) {
var ve = new VariableExpression("{?a,b,c}", operators.valueOf('?'), [
{varname: 'a', exploded: false},
{varname: 'b', exploded: false},
{varname: 'c', exploded: false}
]);
test.equal(ve.expand({}), '');
test.done();
} }
}; };
}()); }());
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment