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