2 * @author Alexey Kuzmin <alex.s.kuzmin@gmail.com>
3 * @fileoverview JavaScript implementation of JSON Pointer.
4 * @see http://tools.ietf.org/html/rfc6901
13 * List of special characters and their escape sequences.
14 * Special characters will be unescaped in order they are listed.
16 * @type {Array.<Array.<string>>}
19 var SPECIAL_CHARACTERS = [
26 * Tokens' separator in JSON pointer string.
31 var TOKENS_SEPARATOR = '/';
35 * Prefix for error messages.
39 var ERROR_MESSAGE_PREFIX = 'JSON Pointer: ';
43 * Validates non-empty pointer string.
47 var NON_EMPTY_POINTER_REGEXP = /(\/[^\/]*)+/;
51 * List of error messages.
52 * Please keep it in alphabetical order.
56 HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT:
57 'Implementation does not support "-" token for arrays.',
58 INVALID_DOCUMENT: 'JSON document is not valid.',
59 INVALID_DOCUMENT_TYPE: 'JSON document must be a string or object.',
60 INVALID_POINTER: 'Pointer is not valid.',
61 NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT:
62 'Non-number tokens cannot be used in array context.',
63 TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT:
64 'Token with leading zero cannot be used in array context.'
69 * Returns |target| object's value pointed by |opt_pointer|, returns undefined
70 * if |opt_pointer| points to non-existing value.
71 * If pointer is not provided, validates first argument and returns
72 * evaluator function that takes pointer as argument.
73 * @param {(string|Object|Array)} target Evaluation target.
74 * @param {string=} opt_pointer JSON Pointer string.
75 * @returns {*} Some value.
77 function getPointedValue(target, opt_pointer) {
78 // .get() method implementation.
80 // First argument must be either string or object.
81 if (isString(target)) {
83 // If string it must be valid JSON document.
85 // Let's try to parse it as JSON.
86 target = JSON.parse(target);
89 // If parsing failed, an exception will be thrown.
90 throw getError(ErrorMessage.INVALID_DOCUMENT);
93 else if (!isObject(target)) {
94 // If not object or string, an exception will be thrown.
95 throw getError(ErrorMessage.INVALID_DOCUMENT_TYPE);
98 // |target| is already parsed, let's create evaluator function for it.
99 var evaluator = createPointerEvaluator(target);
101 if (isUndefined(opt_pointer)) {
102 // If pointer was not provided, return evaluator function.
106 // If pointer is provided, return evaluation result.
107 return evaluator(opt_pointer);
113 * Returns function that takes JSON Pointer as single argument
114 * and evaluates it in given |target| context.
115 * Returned function throws an exception if pointer is not valid
116 * or any error occurs during evaluation.
117 * @param {*} target Evaluation target.
118 * @returns {Function}
120 function createPointerEvaluator(target) {
122 // Use cache to store already received values.
125 return function(pointer) {
127 if (!isValidJSONPointer(pointer)) {
128 // If it's not, an exception will be thrown.
129 throw getError(ErrorMessage.INVALID_POINTER);
132 // First, look up in the cache.
133 if (cache.hasOwnProperty(pointer)) {
134 // If cache entry exists, return it's value.
135 return cache[pointer];
138 // Now, when all arguments are valid, we can start evaluation.
139 // First of all, let's convert JSON pointer string to tokens list.
140 var tokensList = parsePointer(pointer);
144 // Evaluation will be continued till tokens list is not empty
145 // and returned value is not an undefined.
146 while (!isUndefined(value) && !isUndefined(token = tokensList.pop())) {
147 // Let's evaluate token in current context.
148 // `getValue()` might throw an exception, but we won't handle it.
149 value = getValue(value, token);
152 // Pointer evaluation is done, save value in the cache and return it.
153 cache[pointer] = value;
160 * Returns true if given |pointer| is valid, returns false otherwise.
161 * @param {!string} pointer
162 * @returns {boolean} Whether pointer is valid.
164 function isValidJSONPointer(pointer) {
165 // Validates JSON pointer string.
167 if (!isString(pointer)) {
168 // If it's not a string, it obviously is not valid.
172 if ('' === pointer) {
173 // If it is string and is an empty string, it's valid.
177 // If it is non-empty string, it must match spec defined format.
178 // Check Section 3 of specification for concrete syntax.
179 return NON_EMPTY_POINTER_REGEXP.test(pointer);
184 * Returns tokens list for given |pointer|. List is reversed, e.g.
185 * '/simple/path' -> ['path', 'simple']
186 * @param {!string} pointer JSON pointer string.
187 * @returns {Array} List of tokens.
189 function parsePointer(pointer) {
190 // Converts JSON pointer string into tokens list.
192 // Let's split pointer string by tokens' separator character.
193 // Also we will reverse resulting array to simplify it's further usage.
194 var tokens = pointer.split(TOKENS_SEPARATOR).reverse();
196 // Last item in resulting array is always an empty string,
197 // we don't need it, let's remove it.
200 // Now tokens' array is ready to use, let's return it.
206 * Decodes all escape sequences in given |rawReferenceToken|.
207 * @param {!string} rawReferenceToken
208 * @returns {string} Unescaped reference token.
210 function unescapeReferenceToken(rawReferenceToken) {
211 // Unescapes reference token. See Section 3 of specification.
213 var referenceToken = rawReferenceToken;
218 // Order of unescaping does matter.
219 // That's why an array is used here and not hash.
220 SPECIAL_CHARACTERS.forEach(function(pair) {
222 escapeSequence = pair[1];
223 replaceRegExp = new RegExp(escapeSequence, 'g');
224 referenceToken = referenceToken.replace(replaceRegExp, character);
227 return referenceToken;
232 * Returns value pointed by |token| in evaluation |context|.
233 * Throws an exception if any error occurs.
234 * @param {*} context Current evaluation context.
235 * @param {!string} token Unescaped reference token.
236 * @returns {*} Some value or undefined if value if not found.
238 function getValue(context, token) {
239 // Reference token evaluation. See Section 4 of spec.
241 // First of all we should unescape all special characters in token.
242 token = unescapeReferenceToken(token);
244 // Further actions depend of context of evaluation.
246 if (isArray(context)) {
247 // In array context there are more strict requirements
251 // Token cannot be a "-" character,
252 // it has no sense in current implementation.
253 throw getError(ErrorMessage.HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT);
255 if (!isNumber(token)) {
256 // Token cannot be non-number.
257 throw getError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT);
259 if (token.length > 1 && '0' === token[0]) {
260 // Token cannot be non-zero number with leading zero.
261 throw getError(ErrorMessage.TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT);
263 // If all conditions are met, simply return element
264 // with token's value index.
265 // It might be undefined, but it's ok.
266 return context[token];
269 if (isObject(context)) {
270 // In object context we can simply return element w/ key equal to token.
271 // It might be undefined, but it's ok.
272 return context[token];
275 // If context is not an array or an object,
276 // token evaluation is not possible.
277 // This is the expected situation and so we won't throw an error,
278 // undefined value is perfectly suitable here.
284 * Returns Error instance for throwing.
285 * @param {string} message Error message.
288 function getError(message) {
289 return new Error(ERROR_MESSAGE_PREFIX + message);
293 function isObject(o) {
294 return 'object' === typeof o && null !== o;
298 function isArray(a) {
299 return Array.isArray(a);
303 function isNumber(n) {
304 return !isNaN(Number(n));
308 function isString(s) {
309 return 'string' === typeof s || s instanceof String;
313 function isUndefined(v) {
314 return 'undefined' === typeof v;
318 // Let's expose API to the world.
324 if ('object' === typeof exports) {
325 // If `exports` is an object, we are in Node.js context.
326 // We are supposed to act as Node.js package.
327 module.exports = jsonpointer;
328 } else if ('function' === typeof define && define.amd) {
329 // If there is global function `define()` and `define.amd` is defined,
330 // we are supposed to act as AMD module.
336 // Let's create global `jsonpointer` object.
337 this.jsonpointer = jsonpointer;
340 }).call((function() {
342 return (typeof window !== 'undefined' ? window : global);