MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / dataschema-json / dataschema-json.js
blob09fe66e02d4eb890f0abbbcf2b1340dcba146b71
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('dataschema-json', function(Y) {
9 /**
10 Provides a DataSchema implementation which can be used to work with JSON data.
12 @module dataschema
13 @submodule dataschema-json
14 **/
16 /**
17 Provides a DataSchema implementation which can be used to work with JSON data.
19 See the `apply` method for usage.
21 @class DataSchema.JSON
22 @extends DataSchema.Base
23 @static
24 **/
25 var LANG = Y.Lang,
26     isFunction = LANG.isFunction,
27     isObject   = LANG.isObject,
28     isArray    = LANG.isArray,
29     // TODO: I don't think the calls to Base.* need to be done via Base since
30     // Base is mixed into SchemaJSON.  Investigate for later.
31     Base       = Y.DataSchema.Base,
33     SchemaJSON;
34     
35 SchemaJSON = {
37 /////////////////////////////////////////////////////////////////////////////
39 // DataSchema.JSON static methods
41 /////////////////////////////////////////////////////////////////////////////
42     /**
43      * Utility function converts JSON locator strings into walkable paths
44      *
45      * @method getPath
46      * @param locator {String} JSON value locator.
47      * @return {String[]} Walkable path to data value.
48      * @static
49      */
50     getPath: function(locator) {
51         var path = null,
52             keys = [],
53             i = 0;
55         if (locator) {
56             // Strip the ["string keys"] and [1] array indexes
57             // TODO: the first two steps can probably be reduced to one with
58             // /\[\s*(['"])?(.*?)\1\s*\]/g, but the array indices would be
59             // stored as strings.  This is not likely an issue.
60             locator = locator.
61                 replace(/\[\s*(['"])(.*?)\1\s*\]/g,
62                 function (x,$1,$2) {keys[i]=$2;return '.@'+(i++);}).
63                 replace(/\[(\d+)\]/g,
64                 function (x,$1) {keys[i]=parseInt($1,10)|0;return '.@'+(i++);}).
65                 replace(/^\./,''); // remove leading dot
67             // Validate against problematic characters.
68             // commented out because the path isn't sent to eval, so it
69             // should be safe. I'm not sure what makes a locator invalid.
70             //if (!/[^\w\.\$@]/.test(locator)) {
71             path = locator.split('.');
72             for (i=path.length-1; i >= 0; --i) {
73                 if (path[i].charAt(0) === '@') {
74                     path[i] = keys[parseInt(path[i].substr(1),10)];
75                 }
76             }
77             /*}
78             else {
79             }
80             */
81         }
82         return path;
83     },
85     /**
86      * Utility function to walk a path and return the value located there.
87      *
88      * @method getLocationValue
89      * @param path {String[]} Locator path.
90      * @param data {String} Data to traverse.
91      * @return {Object} Data value at location.
92      * @static
93      */
94     getLocationValue: function (path, data) {
95         var i = 0,
96             len = path.length;
97         for (;i<len;i++) {
98             if (isObject(data) && (path[i] in data)) {
99                 data = data[path[i]];
100             } else {
101                 data = undefined;
102                 break;
103             }
104         }
105         return data;
106     },
108     /**
109     Applies a schema to an array of data located in a JSON structure, returning
110     a normalized object with results in the `results` property. Additional
111     information can be parsed out of the JSON for inclusion in the `meta`
112     property of the response object.  If an error is encountered during
113     processing, an `error` property will be added.
115     The input _data_ is expected to be an object or array.  If it is a string,
116     it will be passed through `Y.JSON.parse()`.
118     If _data_ contains an array of data records to normalize, specify the
119     _schema.resultListLocator_ as a dot separated path string just as you would
120     reference it in JavaScript.  So if your _data_ object has a record array at
121     _data.response.results_, use _schema.resultListLocator_ =
122     "response.results". Bracket notation can also be used for array indices or
123     object properties (e.g. "response['results']");  This is called a "path
124     locator"
126     Field data in the result list is extracted with field identifiers in
127     _schema.resultFields_.  Field identifiers are objects with the following
128     properties:
130       * `key`   : <strong>(required)</strong> The path locator (String)
131       * `parser`: A function or the name of a function on `Y.Parsers` used
132             to convert the input value into a normalized type.  Parser
133             functions are passed the value as input and are expected to
134             return a value.
136     If no value parsing is needed, you can use path locators (strings) 
137     instead of field identifiers (objects) -- see example below.
139     If no processing of the result list array is needed, _schema.resultFields_
140     can be omitted; the `response.results` will point directly to the array.
142     If the result list contains arrays, `response.results` will contain an
143     array of objects with key:value pairs assuming the fields in
144     _schema.resultFields_ are ordered in accordance with the data array
145     values.
147     If the result list contains objects, the identified _schema.resultFields_
148     will be used to extract a value from those objects for the output result.
150     To extract additional information from the JSON, include an array of
151     path locators in _schema.metaFields_.  The collected values will be
152     stored in `response.meta`.
155     @example
156         // Process array of arrays
157         var schema = {
158                 resultListLocator: 'produce.fruit',
159                 resultFields: [ 'name', 'color' ]
160             },
161             data = {
162                 produce: {
163                     fruit: [
164                         [ 'Banana', 'yellow' ],
165                         [ 'Orange', 'orange' ],
166                         [ 'Eggplant', 'purple' ]
167                     ]
168                 }
169             };
171         var response = Y.DataSchema.JSON.apply(schema, data);
173         // response.results[0] is { name: "Banana", color: "yellow" }
175         
176         // Process array of objects + some metadata
177         schema.metaFields = [ 'lastInventory' ];
179         data = {
180             produce: {
181                 fruit: [
182                     { name: 'Banana', color: 'yellow', price: '1.96' },
183                     { name: 'Orange', color: 'orange', price: '2.04' },
184                     { name: 'Eggplant', color: 'purple', price: '4.31' }
185                 ]
186             },
187             lastInventory: '2011-07-19'
188         };
190         response = Y.DataSchema.JSON.apply(schema, data);
192         // response.results[0] is { name: "Banana", color: "yellow" }
193         // response.meta.lastInventory is '2001-07-19'
196         // Use parsers
197         schema.resultFields = [
198             {
199                 key: 'name',
200                 parser: function (val) { return val.toUpperCase(); }
201             },
202             {
203                 key: 'price',
204                 parser: 'number' // Uses Y.Parsers.number
205             }
206         ];
208         response = Y.DataSchema.JSON.apply(schema, data);
210         // Note price was converted from a numeric string to a number
211         // response.results[0] looks like { fruit: "BANANA", price: 1.96 }
212      
213     @method apply
214     @param {Object} [schema] Schema to apply.  Supported configuration
215         properties are:
216       @param {String} [schema.resultListLocator] Path locator for the
217           location of the array of records to flatten into `response.results`
218       @param {Array} [schema.resultFields] Field identifiers to
219           locate/assign values in the response records. See above for
220           details.
221       @param {Array} [schema.metaFields] Path locators to extract extra
222           non-record related information from the data object.
223     @param {Object|Array|String} data JSON data or its string serialization.
224     @return {Object} An Object with properties `results` and `meta`
225     @static
226     **/
227     apply: function(schema, data) {
228         var data_in = data,
229             data_out = { results: [], meta: {} };
231         // Convert incoming JSON strings
232         if (!isObject(data)) {
233             try {
234                 data_in = Y.JSON.parse(data);
235             }
236             catch(e) {
237                 data_out.error = e;
238                 return data_out;
239             }
240         }
242         if (isObject(data_in) && schema) {
243             // Parse results data
244             data_out = SchemaJSON._parseResults.call(this, schema, data_in, data_out);
246             // Parse meta data
247             if (schema.metaFields !== undefined) {
248                 data_out = SchemaJSON._parseMeta(schema.metaFields, data_in, data_out);
249             }
250         }
251         else {
252             data_out.error = new Error("JSON schema parse failure");
253         }
255         return data_out;
256     },
258     /**
259      * Schema-parsed list of results from full data
260      *
261      * @method _parseResults
262      * @param schema {Object} Schema to parse against.
263      * @param json_in {Object} JSON to parse.
264      * @param data_out {Object} In-progress parsed data to update.
265      * @return {Object} Parsed data object.
266      * @static
267      * @protected
268      */
269     _parseResults: function(schema, json_in, data_out) {
270         var getPath  = SchemaJSON.getPath,
271             getValue = SchemaJSON.getLocationValue,
272             path     = getPath(schema.resultListLocator),
273             results  = path ?
274                         (getValue(path, json_in) ||
275                          // Fall back to treat resultListLocator as a simple key
276                             json_in[schema.resultListLocator]) :
277                         // Or if no resultListLocator is supplied, use the input
278                         json_in;
280         if (isArray(results)) {
281             // if no result fields are passed in, then just take
282             // the results array whole-hog Sometimes you're getting
283             // an array of strings, or want the whole object, so
284             // resultFields don't make sense.
285             if (isArray(schema.resultFields)) {
286                 data_out = SchemaJSON._getFieldValues.call(this, schema.resultFields, results, data_out);
287             } else {
288                 data_out.results = results;
289             }
290         } else if (schema.resultListLocator) {
291             data_out.results = [];
292             data_out.error = new Error("JSON results retrieval failure");
293         }
295         return data_out;
296     },
298     /**
299      * Get field data values out of list of full results
300      *
301      * @method _getFieldValues
302      * @param fields {Array} Fields to find.
303      * @param array_in {Array} Results to parse.
304      * @param data_out {Object} In-progress parsed data to update.
305      * @return {Object} Parsed data object.
306      * @static
307      * @protected
308      */
309     _getFieldValues: function(fields, array_in, data_out) {
310         var results = [],
311             len = fields.length,
312             i, j,
313             field, key, locator, path, parser, val,
314             simplePaths = [], complexPaths = [], fieldParsers = [],
315             result, record;
317         // First collect hashes of simple paths, complex paths, and parsers
318         for (i=0; i<len; i++) {
319             field = fields[i]; // A field can be a simple string or a hash
320             key = field.key || field; // Find the key
321             locator = field.locator || key; // Find the locator
323             // Validate and store locators for later
324             path = SchemaJSON.getPath(locator);
325             if (path) {
326                 if (path.length === 1) {
327                     simplePaths.push({
328                         key : key,
329                         path: path[0]
330                     });
331                 } else {
332                     complexPaths.push({
333                         key    : key,
334                         path   : path,
335                         locator: locator
336                     });
337                 }
338             } else {
339             }
341             // Validate and store parsers for later
342             //TODO: use Y.DataSchema.parse?
343             parser = (isFunction(field.parser)) ?
344                         field.parser :
345                         Y.Parsers[field.parser + ''];
347             if (parser) {
348                 fieldParsers.push({
349                     key   : key,
350                     parser: parser
351                 });
352             }
353         }
355         // Traverse list of array_in, creating records of simple fields,
356         // complex fields, and applying parsers as necessary
357         for (i=array_in.length-1; i>=0; --i) {
358             record = {};
359             result = array_in[i];
360             if(result) {
361                 // Cycle through complexLocators
362                 for (j=complexPaths.length - 1; j>=0; --j) {
363                     path = complexPaths[j];
364                     val = SchemaJSON.getLocationValue(path.path, result);
365                     if (val === undefined) {
366                         val = SchemaJSON.getLocationValue([path.locator], result);
367                         // Fail over keys like "foo.bar" from nested parsing
368                         // to single token parsing if a value is found in
369                         // results["foo.bar"]
370                         if (val !== undefined) {
371                             simplePaths.push({
372                                 key:  path.key,
373                                 path: path.locator
374                             });
375                             // Don't try to process the path as complex
376                             // for further results
377                             complexPaths.splice(i,1);
378                             continue;
379                         }
380                     }
382                     record[path.key] = Base.parse.call(this,
383                         (SchemaJSON.getLocationValue(path.path, result)), path);
384                 }
386                 // Cycle through simpleLocators
387                 for (j = simplePaths.length - 1; j >= 0; --j) {
388                     path = simplePaths[j];
389                     // Bug 1777850: The result might be an array instead of object
390                     record[path.key] = Base.parse.call(this,
391                             ((result[path.path] === undefined) ?
392                             result[j] : result[path.path]), path);
393                 }
395                 // Cycle through fieldParsers
396                 for (j=fieldParsers.length-1; j>=0; --j) {
397                     key = fieldParsers[j].key;
398                     record[key] = fieldParsers[j].parser.call(this, record[key]);
399                     // Safety net
400                     if (record[key] === undefined) {
401                         record[key] = null;
402                     }
403                 }
404                 results[i] = record;
405             }
406         }
407         data_out.results = results;
408         return data_out;
409     },
411     /**
412      * Parses results data according to schema
413      *
414      * @method _parseMeta
415      * @param metaFields {Object} Metafields definitions.
416      * @param json_in {Object} JSON to parse.
417      * @param data_out {Object} In-progress parsed data to update.
418      * @return {Object} Schema-parsed meta data.
419      * @static
420      * @protected
421      */
422     _parseMeta: function(metaFields, json_in, data_out) {
423         if (isObject(metaFields)) {
424             var key, path;
425             for(key in metaFields) {
426                 if (metaFields.hasOwnProperty(key)) {
427                     path = SchemaJSON.getPath(metaFields[key]);
428                     if (path && json_in) {
429                         data_out.meta[key] = SchemaJSON.getLocationValue(path, json_in);
430                     }
431                 }
432             }
433         }
434         else {
435             data_out.error = new Error("JSON meta data retrieval failure");
436         }
437         return data_out;
438     }
441 // TODO: Y.Object + mix() might be better here
442 Y.DataSchema.JSON = Y.mix(SchemaJSON, Base);
445 }, '3.5.1' ,{requires:['dataschema-base','json']});