uritemplate
Version:
An UriTemplate implementation of rfc 6570
886 lines (800 loc) • 30 kB
JavaScript
/*global unescape, module, define, window, global*/
/*
UriTemplate Copyright (c) 2012-2013 Franz Antesberger. All Rights Reserved.
Available via the MIT license.
*/
(function (exportCallback) {
"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 () {
function isArray (value) {
return Object.prototype.toString.apply(value) === '[object Array]';
}
function isString (value) {
return Object.prototype.toString.apply(value) === '[object String]';
}
function isNumber (value) {
return Object.prototype.toString.apply(value) === '[object Number]';
}
function isBoolean (value) {
return Object.prototype.toString.apply(value) === '[object Boolean]';
}
function join (arr, separator) {
var
result = '',
first = true,
index;
for (index = 0; index < arr.length; index += 1) {
if (first) {
first = false;
}
else {
result += separator;
}
result += arr[index];
}
return result;
}
function map (arr, mapper) {
var
result = [],
index = 0;
for (; index < arr.length; index += 1) {
result.push(mapper(arr[index]));
}
return result;
}
function filter (arr, predicate) {
var
result = [],
index = 0;
for (; index < arr.length; index += 1) {
if (predicate(arr[index])) {
result.push(arr[index]);
}
}
return result;
}
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,
isString: isString,
isNumber: isNumber,
isBoolean: isBoolean,
join: join,
map: map,
filter: filter,
deepFreeze: deepFreeze
};
}());
var charHelper = (function () {
function isAlpha (chr) {
return (chr >= 'a' && chr <= 'z') || ((chr >= 'A' && chr <= 'Z'));
}
function isDigit (chr) {
return chr >= '0' && chr <= '9';
}
function isHexDigit (chr) {
return isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F');
}
return {
isAlpha: isAlpha,
isDigit: isDigit,
isHexDigit: isHexDigit
};
}());
var pctEncoder = (function () {
var utf8 = {
encode: function (chr) {
// see http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html
return unescape(encodeURIComponent(chr));
},
numBytes: function (firstCharCode) {
if (firstCharCode <= 0x7F) {
return 1;
}
else if (0xC2 <= firstCharCode && firstCharCode <= 0xDF) {
return 2;
}
else if (0xE0 <= firstCharCode && firstCharCode <= 0xEF) {
return 3;
}
else if (0xF0 <= firstCharCode && firstCharCode <= 0xF4) {
return 4;
}
// no valid first octet
return 0;
},
isValidFollowingCharCode: function (charCode) {
return 0x80 <= charCode && charCode <= 0xBF;
}
};
/**
* encodes a character, if needed or not.
* @param chr
* @return pct-encoded character
*/
function encodeCharacter (chr) {
var
result = '',
octets = utf8.encode(chr),
octet,
index;
for (index = 0; index < octets.length; index += 1) {
octet = octets.charCodeAt(index);
result += '%' + (octet < 0x10 ? '0' : '') + octet.toString(16).toUpperCase();
}
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.charAt(start) === '%' && charHelper.isHexDigit(text.charAt(start + 1)) && charHelper.isHexDigit(text.charAt(start + 2));
}
/**
* 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 whether or not the given char sequence is a correctly pct-encoded sequence.
* @param chr
* @return {boolean}
*/
function isPctEncoded (chr) {
if (!isPercentDigitDigit(chr, 0)) {
return false;
}
var firstCharCode = parseHex2(chr, 1);
var numBytes = utf8.numBytes(firstCharCode);
if (numBytes === 0) {
return false;
}
for (var byteNumber = 1; byteNumber < numBytes; byteNumber += 1) {
if (!isPercentDigitDigit(chr, 3*byteNumber) || !utf8.isValidFollowingCharCode(parseHex2(chr, 3*byteNumber + 1))) {
return false;
}
}
return true;
}
/**
* Reads as much as needed from the text, e.g. '%20' or '%C3%B6'. It does not decode!
* @param text
* @param startIndex
* @return the character or pct-string of the text at startIndex
*/
function pctCharAt(text, startIndex) {
var chr = text.charAt(startIndex);
if (!isPercentDigitDigit(text, startIndex)) {
return chr;
}
var utf8CharCode = parseHex2(text, startIndex + 1);
var numBytes = utf8.numBytes(utf8CharCode);
if (numBytes === 0) {
return chr;
}
for (var byteNumber = 1; byteNumber < numBytes; byteNumber += 1) {
if (!isPercentDigitDigit(text, startIndex + 3 * byteNumber) || !utf8.isValidFollowingCharCode(parseHex2(text, startIndex + 3 * byteNumber + 1))) {
return chr;
}
}
return text.substr(startIndex, 3 * numBytes);
}
return {
encodeCharacter: encodeCharacter,
isPctEncoded: isPctEncoded,
pctCharAt: pctCharAt
};
}());
var rfcCharHelper = (function () {
/**
* Returns if an character is an varchar character according 2.3 of rfc 6570
* @param chr
* @return (Boolean)
*/
function isVarchar (chr) {
return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '_' || pctEncoder.isPctEncoded(chr);
}
/**
* Returns if chr is an unreserved character according 1.5 of rfc 6570
* @param chr
* @return {Boolean}
*/
function isUnreserved (chr) {
return charHelper.isAlpha(chr) || charHelper.isDigit(chr) || chr === '-' || chr === '.' || chr === '_' || chr === '~';
}
/**
* Returns if chr is an reserved character according 1.5 of rfc 6570
* or the percent character mentioned in 3.2.1.
* @param chr
* @return {Boolean}
*/
function isReserved (chr) {
return chr === ':' || chr === '/' || chr === '?' || chr === '#' || chr === '[' || chr === ']' || chr === '@' || chr === '!' || chr === '$' || chr === '&' || chr === '(' ||
chr === ')' || chr === '*' || chr === '+' || chr === ',' || chr === ';' || chr === '=' || chr === "'";
}
return {
isVarchar: isVarchar,
isUnreserved: isUnreserved,
isReserved: isReserved
};
}());
/**
* encoding of rfc 6570
*/
var encodingHelper = (function () {
function encode (text, passReserved) {
var
result = '',
index,
chr = '';
if (typeof text === "number" || typeof text === "boolean") {
text = text.toString();
}
for (index = 0; index < text.length; index += chr.length) {
chr = text.charAt(index);
result += rfcCharHelper.isUnreserved(chr) || (passReserved && rfcCharHelper.isReserved(chr)) ? chr : pctEncoder.encodeCharacter(chr);
}
return result;
}
function encodePassReserved (text) {
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 {
encode: encode,
encodePassReserved: encodePassReserved,
encodeLiteral: encodeLiteral,
encodeLiteralCharacter: encodeLiteralCharacter
};
}());
// the operators defined by rfc 6570
var operators = (function () {
var
bySymbol = {};
function create (symbol) {
bySymbol[symbol] = {
symbol: symbol,
separator: (symbol === '?') ? '&' : (symbol === '' || symbol === '+' || symbol === '#') ? ',' : symbol,
named: symbol === ';' || symbol === '&' || symbol === '?',
ifEmpty: (symbol === '&' || symbol === '?') ? '=' : '',
first: (symbol === '+' ) ? '' : symbol,
encode: (symbol === '+' || symbol === '#') ? encodingHelper.encodePassReserved : encodingHelper.encode,
toString: function () {
return this.symbol;
}
};
}
create('');
create('+');
create('#');
create('.');
create('/');
create(';');
create('?');
create('&');
return {
valueOf: function (chr) {
if (bySymbol[chr]) {
return bySymbol[chr];
}
if ("=,!@|".indexOf(chr) >= 0) {
return null;
}
return bySymbol[''];
}
};
}());
/**
* Detects, whether a given element is defined in the sense of rfc 6570
* Section 2.3 of the RFC makes clear defintions:
* * undefined and null are not defined.
* * the empty string is defined
* * 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 property with defined value
* @param object
* @return {Boolean}
*/
function isDefined (object) {
var
propertyName;
if (object === null || object === undefined) {
return false;
}
if (objectHelper.isArray(object)) {
// Section 2.3: A variable defined as a list value is considered undefined if the list contains zero members
return object.length > 0;
}
if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") {
// falsy values like empty strings, false or 0 are "defined"
return true;
}
// else Object
for (propertyName in object) {
if (object.hasOwnProperty(propertyName) && isDefined(object[propertyName])) {
return true;
}
}
return false;
}
var LiteralExpression = (function () {
function LiteralExpression (literal) {
this.literal = encodingHelper.encodeLiteral(literal);
}
LiteralExpression.prototype.expand = function () {
return this.literal;
};
LiteralExpression.prototype.toString = LiteralExpression.prototype.expand;
return LiteralExpression;
}());
var parse = (function () {
function parseExpression (expressionText) {
var
operator,
varspecs = [],
varspec = null,
varnameStart = null,
maxLengthStart = null,
index,
chr = '';
function closeVarname () {
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;
}
function closeMaxLength () {
if (maxLengthStart === index) {
throw new UriTemplateError({expressionText: expressionText, message: "after a ':' you have to specify the length", position: index});
}
varspec.maxLength = parseInt(expressionText.substring(maxLengthStart, index), 10);
maxLengthStart = null;
}
operator = (function (operatorText) {
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;
varnameStart = index;
for (; index < expressionText.length; index += chr.length) {
chr = pctEncoder.pctCharAt(expressionText, index);
if (varnameStart !== null) {
// the spec says: varname = varchar *( ["."] varchar )
// so a dot is allowed except for the first char
if (chr === '.') {
if (varnameStart === index) {
throw new UriTemplateError({expressionText: expressionText, message: "a varname MUST NOT start with a dot", position: index});
}
continue;
}
if (rfcCharHelper.isVarchar(chr)) {
continue;
}
closeVarname();
}
if (maxLengthStart !== null) {
if (index === maxLengthStart && chr === '0') {
throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must not start with digit 0", position: index});
}
if (charHelper.isDigit(chr)) {
if (index - maxLengthStart >= 4) {
throw new UriTemplateError({expressionText: expressionText, message: "A :prefix must have max 4 digits", position: index});
}
continue;
}
closeMaxLength();
}
if (chr === ':') {
if (varspec.maxLength !== null) {
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;
continue;
}
if (chr === '*') {
if (varspec === null) {
throw new UriTemplateError({expressionText: expressionText, message: "exploded without varspec", position: index});
}
if (varspec.exploded) {
throw new UriTemplateError({expressionText: expressionText, message: "exploded twice", position: index});
}
if (varspec.maxLength) {
throw new UriTemplateError({expressionText: expressionText, message: "an explode (*) MUST NOT follow to a prefix", position: index});
}
varspec.exploded = true;
continue;
}
// the only legal character now is the comma
if (chr === ',') {
varspecs.push(varspec);
varspec = null;
varnameStart = index + 1;
continue;
}
throw new UriTemplateError({expressionText: expressionText, message: "illegal character", character: chr, position: index});
} // for chr
if (varnameStart !== null) {
closeVarname();
}
if (maxLengthStart !== null) {
closeMaxLength();
}
varspecs.push(varspec);
return new VariableExpression(expressionText, operator, varspecs);
}
function parse (uriTemplateText) {
// assert filled string
var
index,
chr,
expressions = [],
braceOpenIndex = null,
literalStart = 0;
for (index = 0; index < uriTemplateText.length; index += 1) {
chr = uriTemplateText.charAt(index);
if (literalStart !== null) {
if (chr === '}') {
throw new UriTemplateError({templateText: uriTemplateText, message: "unopened brace closed", position: index});
}
if (chr === '{') {
if (literalStart < index) {
expressions.push(new LiteralExpression(uriTemplateText.substring(literalStart, index)));
}
literalStart = null;
braceOpenIndex = index;
}
continue;
}
if (braceOpenIndex !== null) {
// here just { is forbidden
if (chr === '{') {
throw new UriTemplateError({templateText: uriTemplateText, message: "brace already opened", position: index});
}
if (chr === '}') {
if (braceOpenIndex + 1 === index) {
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;
}
braceOpenIndex = null;
literalStart = index + 1;
}
continue;
}
throw new Error('reached unreachable code');
}
if (braceOpenIndex !== null) {
throw new UriTemplateError({templateText: uriTemplateText, message: "unclosed brace", position: braceOpenIndex});
}
if (literalStart < uriTemplateText.length) {
expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart)));
}
return new UriTemplate(uriTemplateText, expressions);
}
return parse;
}());
var VariableExpression = (function () {
// helper function if JSON is not available
function prettyPrint (value) {
return (JSON && JSON.stringify) ? JSON.stringify(value) : value;
}
function isEmpty (value) {
if (!isDefined(value)) {
return true;
}
if (objectHelper.isString(value)) {
return value === '';
}
if (objectHelper.isNumber(value) || objectHelper.isBoolean(value)) {
return false;
}
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) {
this.templateText = templateText;
this.operator = operator;
this.varspecs = varspecs;
}
VariableExpression.prototype.toString = function () {
return this.templateText;
};
function expandSimpleValue(varspec, operator, value) {
var result = '';
value = value.toString();
if (operator.named) {
result += encodingHelper.encodeLiteral(varspec.varname);
if (value === '') {
result += operator.ifEmpty;
return result;
}
result += '=';
}
if (varspec.maxLength !== null) {
value = value.substr(0, varspec.maxLength);
}
result += operator.encode(value);
return result;
}
function valueDefined (nameValue) {
return isDefined(nameValue.value);
}
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, ',');
}
else {
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;
}
function expandExplodedNamed (varspec, operator, value) {
var
isArray = objectHelper.isArray(value),
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);
}
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;
}
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
for (index = 0; index < this.varspecs.length; index += 1) {
varspec = this.varspecs[index];
value = variables[varspec.varname];
// if (!isDefined(value)) {
// if (variables.hasOwnProperty(varspec.name)) {
if (value === null || value === undefined) {
continue;
}
if (varspec.exploded) {
oneExploded = true;
}
valueIsArr = objectHelper.isArray(value);
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
expanded.push(expandSimpleValue(varspec, operator, value));
}
else if (varspec.maxLength && isDefined(value)) {
// 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));
}
else if (!varspec.exploded) {
if (operator.named || !isEmpty(value)) {
expanded.push(expandNotExploded(varspec, operator, value));
}
}
else if (isDefined(value)) {
if (operator.named) {
expanded.push(expandExplodedNamed(varspec, operator, value));
}
else {
expanded.push(expandExplodedUnnamed(operator, value));
}
}
}
if (expanded.length === 0) {
return "";
}
else {
return operator.first + objectHelper.join(expanded, operator.separator);
}
};
return VariableExpression;
}());
var UriTemplate = (function () {
function UriTemplate (templateText, expressions) {
this.templateText = templateText;
this.expressions = expressions;
objectHelper.deepFreeze(this);
}
UriTemplate.prototype.toString = function () {
return this.templateText;
};
UriTemplate.prototype.expand = function (variables) {
// this.expressions.map(function (expression) {return expression.expand(variables);}).join('');
var
index,
result = '';
for (index = 0; index < this.expressions.length; index += 1) {
result += this.expressions[index].expand(variables);
}
return result;
};
UriTemplate.parse = parse;
UriTemplate.UriTemplateError = UriTemplateError;
return UriTemplate;
}());
exportCallback(UriTemplate);
}(function (UriTemplate) {
"use strict";
// export UriTemplate, when module is present, or pass it to window or global
if (typeof module !== "undefined") {
module.exports = UriTemplate;
}
else if (typeof define === "function") {
define([],function() {
return UriTemplate;
});
}
else if (typeof window !== "undefined") {
window.UriTemplate = UriTemplate;
}
else {
global.UriTemplate = UriTemplate;
}
}
));