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