Merge branch 'task/JAL-3246_local_getdown_files' into develop
[jalview.git] / examples / biojson-doc / lib / jsonpointer.js
1 /**
2  * @author Alexey Kuzmin <alex.s.kuzmin@gmail.com>
3  * @fileoverview JavaScript implementation of JSON Pointer.
4  * @see http://tools.ietf.org/html/rfc6901
5  */
6
7
8
9 ;(function() {
10   'use strict';
11
12   /**
13    * List of special characters and their escape sequences.
14    * Special characters will be unescaped in order they are listed.
15    * Section 3 of spec.
16    * @type {Array.<Array.<string>>}
17    * @const
18    */
19   var SPECIAL_CHARACTERS = [
20     ['/', '~1'],
21     ['~', '~0']
22   ];
23
24
25   /**
26    * Tokens' separator in JSON pointer string.
27    * Section 3 of spec.
28    * @type {string}
29    * @const
30    */
31   var TOKENS_SEPARATOR = '/';
32
33
34   /**
35    * Prefix for error messages.
36    * @type {string}
37    * @const
38    */
39   var ERROR_MESSAGE_PREFIX = 'JSON Pointer: ';
40
41
42   /**
43    * Validates non-empty pointer string.
44    * @type {RegExp}
45    * @const
46    */
47   var NON_EMPTY_POINTER_REGEXP = /(\/[^\/]*)+/;
48
49
50   /**
51    * List of error messages.
52    * Please keep it in alphabetical order.
53    * @enum {string}
54    */
55   var ErrorMessage = {
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.'
65   };
66
67
68   /**
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.
76    */
77   function getPointedValue(target, opt_pointer) {
78     // .get() method implementation.
79
80     // First argument must be either string or object.
81     if (isString(target)) {
82
83       // If string it must be valid JSON document.
84       try {
85         // Let's try to parse it as JSON.
86         target = JSON.parse(target);
87       }
88       catch (e) {
89         // If parsing failed, an exception will be thrown.
90         throw getError(ErrorMessage.INVALID_DOCUMENT);
91       }
92     }
93     else if (!isObject(target)) {
94       // If not object or string, an exception will be thrown.
95       throw getError(ErrorMessage.INVALID_DOCUMENT_TYPE);
96     }
97
98     // |target| is already parsed, let's create evaluator function for it.
99     var evaluator = createPointerEvaluator(target);
100
101     if (isUndefined(opt_pointer)) {
102       // If pointer was not provided, return evaluator function.
103       return evaluator;
104     }
105     else {
106       // If pointer is provided, return evaluation result.
107       return evaluator(opt_pointer);
108     }
109   }
110
111
112   /**
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}
119    */
120   function createPointerEvaluator(target) {
121
122     // Use cache to store already received values.
123     var cache = {};
124
125     return function(pointer) {
126
127       if (!isValidJSONPointer(pointer)) {
128         // If it's not, an exception will be thrown.
129         throw getError(ErrorMessage.INVALID_POINTER);
130       }
131
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];
136       }
137
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);
141       var token;
142       var value = target;
143
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);
150       }
151
152       // Pointer evaluation is done, save value in the cache and return it.
153       cache[pointer] = value;
154       return value;
155     };
156   }
157
158
159   /**
160    * Returns true if given |pointer| is valid, returns false otherwise.
161    * @param {!string} pointer
162    * @returns {boolean} Whether pointer is valid.
163    */
164   function isValidJSONPointer(pointer) {
165     // Validates JSON pointer string.
166
167     if (!isString(pointer)) {
168       // If it's not a string, it obviously is not valid.
169       return false;
170     }
171
172     if ('' === pointer) {
173       // If it is string and is an empty string, it's valid.
174       return true;
175     }
176
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);
180   }
181
182
183   /**
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.
188    */
189   function parsePointer(pointer) {
190     // Converts JSON pointer string into tokens list.
191
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();
195
196     // Last item in resulting array is always an empty string,
197     // we don't need it, let's remove it.
198     tokens.pop();
199
200     // Now tokens' array is ready to use, let's return it.
201     return tokens;
202   }
203
204
205   /**
206    * Decodes all escape sequences in given |rawReferenceToken|.
207    * @param {!string} rawReferenceToken
208    * @returns {string} Unescaped reference token.
209    */
210   function unescapeReferenceToken(rawReferenceToken) {
211     // Unescapes reference token. See Section 3 of specification.
212
213     var referenceToken = rawReferenceToken;
214     var character;
215     var escapeSequence;
216     var replaceRegExp;
217
218     // Order of unescaping does matter.
219     // That's why an array is used here and not hash.
220     SPECIAL_CHARACTERS.forEach(function(pair) {
221       character = pair[0];
222       escapeSequence = pair[1];
223       replaceRegExp = new RegExp(escapeSequence, 'g');
224       referenceToken = referenceToken.replace(replaceRegExp, character);
225     });
226
227     return referenceToken;
228   }
229
230
231   /**
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.
237    */
238   function getValue(context, token) {
239     // Reference token evaluation. See Section 4 of spec.
240
241     // First of all we should unescape all special characters in token.
242     token = unescapeReferenceToken(token);
243
244     // Further actions depend of context of evaluation.
245
246     if (isArray(context)) {
247       // In array context there are more strict requirements
248       // for token value.
249
250       if ('-' === token) {
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);
254       }
255       if (!isNumber(token)) {
256         // Token cannot be non-number.
257         throw getError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT);
258       }
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);
262       }
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];
267     }
268
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];
273     }
274
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.
279     return;
280   }
281
282
283   /**
284    * Returns Error instance for throwing.
285    * @param {string} message Error message.
286    * @returns {Error}
287    */
288   function getError(message) {
289     return new Error(ERROR_MESSAGE_PREFIX + message);
290   }
291
292
293   function isObject(o) {
294     return 'object' === typeof o && null !== o;
295   }
296
297
298   function isArray(a) {
299     return Array.isArray(a);
300   }
301
302
303   function isNumber(n) {
304     return !isNaN(Number(n));
305   }
306
307
308   function isString(s) {
309     return 'string' === typeof s || s instanceof String;
310   }
311
312
313   function isUndefined(v) {
314     return 'undefined' === typeof v;
315   }
316
317
318   // Let's expose API to the world.
319
320   var jsonpointer = {
321     get: getPointedValue
322   };
323
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.
331     define(function() {
332       return jsonpointer;
333     });
334   } else {
335     // Last resort.
336     // Let's create global `jsonpointer` object.
337     this.jsonpointer = jsonpointer;
338   }
339
340 }).call((function() {
341   'use strict';
342   return (typeof window !== 'undefined' ? window : global);
343 })());