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