3
Copyright 2011 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)];
79
Y.log("Invalid locator: " + locator, "error", "dataschema-json");
87
* Utility function to walk a path and return the value located there.
89
* @method getLocationValue
90
* @param path {String[]} Locator path.
91
* @param data {String} Data to traverse.
92
* @return {Object} Data value at location.
95
getLocationValue: function (path, data) {
99
if (isObject(data) && (path[i] in data)) {
100
data = data[path[i]];
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.
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()`.
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
127
Field data in the result list is extracted with field identifiers in
128
_schema.resultFields_. Field identifiers are objects with the following
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
137
If no value parsing is needed, you can use path locators (strings)
138
instead of field identifiers (objects) -- see example below.
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.
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
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.
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`.
157
// Process array of arrays
159
resultListLocator: 'produce.fruit',
160
resultFields: [ 'name', 'color' ]
165
[ 'Banana', 'yellow' ],
166
[ 'Orange', 'orange' ],
167
[ 'Eggplant', 'purple' ]
172
var response = Y.DataSchema.JSON.apply(schema, data);
174
// response.results[0] is { name: "Banana", color: "yellow" }
177
// Process array of objects + some metadata
178
schema.metaFields = [ 'lastInventory' ];
183
{ name: 'Banana', color: 'yellow', price: '1.96' },
184
{ name: 'Orange', color: 'orange', price: '2.04' },
185
{ name: 'Eggplant', color: 'purple', price: '4.31' }
188
lastInventory: '2011-07-19'
191
response = Y.DataSchema.JSON.apply(schema, data);
193
// response.results[0] is { name: "Banana", color: "yellow" }
194
// response.meta.lastInventory is '2001-07-19'
198
schema.resultFields = [
201
parser: function (val) { return val.toUpperCase(); }
205
parser: 'number' // Uses Y.Parsers.number
209
response = Y.DataSchema.JSON.apply(schema, data);
211
// Note price was converted from a numeric string to a number
212
// response.results[0] looks like { fruit: "BANANA", price: 1.96 }
215
@param {Object} [schema] Schema to apply. Supported configuration
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
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`
228
apply: function(schema, data) {
230
data_out = { results: [], meta: {} };
232
// Convert incoming JSON strings
233
if (!isObject(data)) {
235
data_in = Y.JSON.parse(data);
243
if (isObject(data_in) && schema) {
244
// Parse results data
245
data_out = SchemaJSON._parseResults.call(this, schema, data_in, data_out);
248
if (schema.metaFields !== undefined) {
249
data_out = SchemaJSON._parseMeta(schema.metaFields, data_in, data_out);
253
Y.log("JSON data could not be schema-parsed: " + Y.dump(data) + " " + Y.dump(data), "error", "dataschema-json");
254
data_out.error = new Error("JSON schema parse failure");
261
* Schema-parsed list of results from full data
263
* @method _parseResults
264
* @param schema {Object} Schema to parse against.
265
* @param json_in {Object} JSON to parse.
266
* @param data_out {Object} In-progress parsed data to update.
267
* @return {Object} Parsed data object.
271
_parseResults: function(schema, json_in, data_out) {
272
var getPath = SchemaJSON.getPath,
273
getValue = SchemaJSON.getLocationValue,
274
path = getPath(schema.resultListLocator),
276
(getValue(path, json_in) ||
277
// Fall back to treat resultListLocator as a simple key
278
json_in[schema.resultListLocator]) :
279
// Or if no resultListLocator is supplied, use the input
282
if (isArray(results)) {
283
// if no result fields are passed in, then just take
284
// the results array whole-hog Sometimes you're getting
285
// an array of strings, or want the whole object, so
286
// resultFields don't make sense.
287
if (isArray(schema.resultFields)) {
288
data_out = SchemaJSON._getFieldValues.call(this, schema.resultFields, results, data_out);
290
data_out.results = results;
292
} else if (schema.resultListLocator) {
293
data_out.results = [];
294
data_out.error = new Error("JSON results retrieval failure");
295
Y.log("JSON data could not be parsed: " + Y.dump(json_in), "error", "dataschema-json");
302
* Get field data values out of list of full results
304
* @method _getFieldValues
305
* @param fields {Array} Fields to find.
306
* @param array_in {Array} Results to parse.
307
* @param data_out {Object} In-progress parsed data to update.
308
* @return {Object} Parsed data object.
312
_getFieldValues: function(fields, array_in, data_out) {
316
field, key, locator, path, parser, val,
317
simplePaths = [], complexPaths = [], fieldParsers = [],
320
// First collect hashes of simple paths, complex paths, and parsers
321
for (i=0; i<len; i++) {
322
field = fields[i]; // A field can be a simple string or a hash
323
key = field.key || field; // Find the key
324
locator = field.locator || key; // Find the locator
326
// Validate and store locators for later
327
path = SchemaJSON.getPath(locator);
329
if (path.length === 1) {
342
Y.log("Invalid key syntax: " + key, "warn", "dataschema-json");
345
// Validate and store parsers for later
346
//TODO: use Y.DataSchema.parse?
347
parser = (isFunction(field.parser)) ?
349
Y.Parsers[field.parser + ''];
359
// Traverse list of array_in, creating records of simple fields,
360
// complex fields, and applying parsers as necessary
361
for (i=array_in.length-1; i>=0; --i) {
363
result = array_in[i];
365
// Cycle through complexLocators
366
for (j=complexPaths.length - 1; j>=0; --j) {
367
path = complexPaths[j];
368
val = SchemaJSON.getLocationValue(path.path, result);
369
if (val === undefined) {
370
val = SchemaJSON.getLocationValue([path.locator], result);
371
// Fail over keys like "foo.bar" from nested parsing
372
// to single token parsing if a value is found in
373
// results["foo.bar"]
374
if (val !== undefined) {
379
// Don't try to process the path as complex
380
// for further results
381
complexPaths.splice(i,1);
386
record[path.key] = Base.parse.call(this,
387
(SchemaJSON.getLocationValue(path.path, result)), path);
390
// Cycle through simpleLocators
391
for (j = simplePaths.length - 1; j >= 0; --j) {
392
path = simplePaths[j];
393
// Bug 1777850: The result might be an array instead of object
394
record[path.key] = Base.parse.call(this,
395
((result[path.path] === undefined) ?
396
result[j] : result[path.path]), path);
399
// Cycle through fieldParsers
400
for (j=fieldParsers.length-1; j>=0; --j) {
401
key = fieldParsers[j].key;
402
record[key] = fieldParsers[j].parser.call(this, record[key]);
404
if (record[key] === undefined) {
411
data_out.results = results;
416
* Parses results data according to schema
419
* @param metaFields {Object} Metafields definitions.
420
* @param json_in {Object} JSON to parse.
421
* @param data_out {Object} In-progress parsed data to update.
422
* @return {Object} Schema-parsed meta data.
426
_parseMeta: function(metaFields, json_in, data_out) {
427
if (isObject(metaFields)) {
429
for(key in metaFields) {
430
if (metaFields.hasOwnProperty(key)) {
431
path = SchemaJSON.getPath(metaFields[key]);
432
if (path && json_in) {
433
data_out.meta[key] = SchemaJSON.getLocationValue(path, json_in);
439
data_out.error = new Error("JSON meta data retrieval failure");
445
// TODO: Y.Object + mix() might be better here
446
Y.DataSchema.JSON = Y.mix(SchemaJSON, Base);
449
}, '3.4.1' ,{requires:['dataschema-base','json']});