3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('dataschema-json', function(Y) {
10 Provides a DataSchema implementation which can be used to work with JSON data.
13 @submodule dataschema-json
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
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,
37 /////////////////////////////////////////////////////////////////////////////
39 // DataSchema.JSON static methods
41 /////////////////////////////////////////////////////////////////////////////
43 * Utility function converts JSON locator strings into walkable paths
46 * @param locator {String} JSON value locator.
47 * @return {String[]} Walkable path to data value.
50 getPath: function(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.
61 replace(/\[\s*(['"])(.*?)\1\s*\]/g,
62 function (x,$1,$2) {keys[i]=$2;return '.@'+(i++);}).
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)];
86 * Utility function to walk a path and return the value located there.
88 * @method getLocationValue
89 * @param path {String[]} Locator path.
90 * @param data {String} Data to traverse.
91 * @return {Object} Data value at location.
94 getLocationValue: function (path, data) {
98 if (isObject(data) && (path[i] in data)) {
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
126 Field data in the result list is extracted with field identifiers in
127 _schema.resultFields_. Field identifiers are objects with the following
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
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
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`.
156 // Process array of arrays
158 resultListLocator: 'produce.fruit',
159 resultFields: [ 'name', 'color' ]
164 [ 'Banana', 'yellow' ],
165 [ 'Orange', 'orange' ],
166 [ 'Eggplant', 'purple' ]
171 var response = Y.DataSchema.JSON.apply(schema, data);
173 // response.results[0] is { name: "Banana", color: "yellow" }
176 // Process array of objects + some metadata
177 schema.metaFields = [ 'lastInventory' ];
182 { name: 'Banana', color: 'yellow', price: '1.96' },
183 { name: 'Orange', color: 'orange', price: '2.04' },
184 { name: 'Eggplant', color: 'purple', price: '4.31' }
187 lastInventory: '2011-07-19'
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'
197 schema.resultFields = [
200 parser: function (val) { return val.toUpperCase(); }
204 parser: 'number' // Uses Y.Parsers.number
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 }
214 @param {Object} [schema] Schema to apply. Supported configuration
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
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`
227 apply: function(schema, data) {
229 data_out = { results: [], meta: {} };
231 // Convert incoming JSON strings
232 if (!isObject(data)) {
234 data_in = Y.JSON.parse(data);
242 if (isObject(data_in) && schema) {
243 // Parse results data
244 data_out = SchemaJSON._parseResults.call(this, schema, data_in, data_out);
247 if (schema.metaFields !== undefined) {
248 data_out = SchemaJSON._parseMeta(schema.metaFields, data_in, data_out);
252 data_out.error = new Error("JSON schema parse failure");
259 * Schema-parsed list of results from full data
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.
269 _parseResults: function(schema, json_in, data_out) {
270 var getPath = SchemaJSON.getPath,
271 getValue = SchemaJSON.getLocationValue,
272 path = getPath(schema.resultListLocator),
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
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);
288 data_out.results = results;
290 } else if (schema.resultListLocator) {
291 data_out.results = [];
292 data_out.error = new Error("JSON results retrieval failure");
299 * Get field data values out of list of full results
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.
309 _getFieldValues: function(fields, array_in, data_out) {
313 field, key, locator, path, parser, val,
314 simplePaths = [], complexPaths = [], fieldParsers = [],
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);
326 if (path.length === 1) {
341 // Validate and store parsers for later
342 //TODO: use Y.DataSchema.parse?
343 parser = (isFunction(field.parser)) ?
345 Y.Parsers[field.parser + ''];
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) {
359 result = array_in[i];
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) {
375 // Don't try to process the path as complex
376 // for further results
377 complexPaths.splice(i,1);
382 record[path.key] = Base.parse.call(this,
383 (SchemaJSON.getLocationValue(path.path, result)), path);
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);
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]);
400 if (record[key] === undefined) {
407 data_out.results = results;
412 * Parses results data according to schema
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.
422 _parseMeta: function(metaFields, json_in, data_out) {
423 if (isObject(metaFields)) {
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);
435 data_out.error = new Error("JSON meta data retrieval failure");
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']});