UNPKG

uritemplate

Version:

An UriTemplate implementation of rfc 6570

886 lines (800 loc) 30 kB
/*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; } } ));