| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- /*
- Copyright (c) 2010, Yahoo! Inc. All rights reserved.
- Code licensed under the BSD License:
- http://developer.yahoo.com/yui/license.html
- version: 3.4.0
- build: nightly
- */
- YUI.add('dataschema-json', function(Y) {
-
- /**
- Provides a DataSchema implementation which can be used to work with JSON data.
-
- @module dataschema
- @submodule dataschema-json
- **/
-
- /**
- Provides a DataSchema implementation which can be used to work with JSON data.
-
- See the `apply` method for usage.
-
- @class DataSchema.JSON
- @extends DataSchema.Base
- @static
- **/
- var LANG = Y.Lang,
- isFunction = LANG.isFunction,
- isObject = LANG.isObject,
- isArray = LANG.isArray,
- // TODO: I don't think the calls to Base.* need to be done via Base since
- // Base is mixed into SchemaJSON. Investigate for later.
- Base = Y.DataSchema.Base,
-
- SchemaJSON;
-
- SchemaJSON = {
-
- /////////////////////////////////////////////////////////////////////////////
- //
- // DataSchema.JSON static methods
- //
- /////////////////////////////////////////////////////////////////////////////
- /**
- * Utility function converts JSON locator strings into walkable paths
- *
- * @method getPath
- * @param locator {String} JSON value locator.
- * @return {String[]} Walkable path to data value.
- * @static
- */
- getPath: function(locator) {
- var path = null,
- keys = [],
- i = 0;
-
- if (locator) {
- // Strip the ["string keys"] and [1] array indexes
- // TODO: the first two steps can probably be reduced to one with
- // /\[\s*(['"])?(.*?)\1\s*\]/g, but the array indices would be
- // stored as strings. This is not likely an issue.
- locator = locator.
- replace(/\[\s*(['"])(.*?)\1\s*\]/g,
- function (x,$1,$2) {keys[i]=$2;return '.@'+(i++);}).
- replace(/\[(\d+)\]/g,
- function (x,$1) {keys[i]=parseInt($1,10)|0;return '.@'+(i++);}).
- replace(/^\./,''); // remove leading dot
-
- // Validate against problematic characters.
- // commented out because the path isn't sent to eval, so it
- // should be safe. I'm not sure what makes a locator invalid.
- //if (!/[^\w\.\$@]/.test(locator)) {
- path = locator.split('.');
- for (i=path.length-1; i >= 0; --i) {
- if (path[i].charAt(0) === '@') {
- path[i] = keys[parseInt(path[i].substr(1),10)];
- }
- }
- /*}
- else {
- }
- */
- }
- return path;
- },
-
- /**
- * Utility function to walk a path and return the value located there.
- *
- * @method getLocationValue
- * @param path {String[]} Locator path.
- * @param data {String} Data to traverse.
- * @return {Object} Data value at location.
- * @static
- */
- getLocationValue: function (path, data) {
- var i = 0,
- len = path.length;
- for (;i<len;i++) {
- if (isObject(data) && (path[i] in data)) {
- data = data[path[i]];
- } else {
- data = undefined;
- break;
- }
- }
- return data;
- },
-
- /**
- Applies a schema to an array of data located in a JSON structure, returning
- a normalized object with results in the `results` property. Additional
- information can be parsed out of the JSON for inclusion in the `meta`
- property of the response object. If an error is encountered during
- processing, an `error` property will be added.
-
- The input _data_ is expected to be an object or array. If it is a string,
- it will be passed through `Y.JSON.parse()`.
-
- If _data_ contains an array of data records to normalize, specify the
- _schema.resultListLocator_ as a dot separated path string just as you would
- reference it in JavaScript. So if your _data_ object has a record array at
- _data.response.results_, use _schema.resultListLocator_ =
- "response.results". Bracket notation can also be used for array indices or
- object properties (e.g. "response['results']"); This is called a "path
- locator"
-
- Field data in the result list is extracted with field identifiers in
- _schema.resultFields_. Field identifiers are objects with the following
- properties:
-
- * `key` : <strong>(required)</strong> The path locator (String)
- * `parser`: A function or the name of a function on `Y.Parsers` used
- to convert the input value into a normalized type. Parser
- functions are passed the value as input and are expected to
- return a value.
-
- If no value parsing is needed, you can use path locators (strings)
- instead of field identifiers (objects) -- see example below.
-
- If no processing of the result list array is needed, _schema.resultFields_
- can be omitted; the `response.results` will point directly to the array.
-
- If the result list contains arrays, `response.results` will contain an
- array of objects with key:value pairs assuming the fields in
- _schema.resultFields_ are ordered in accordance with the data array
- values.
-
- If the result list contains objects, the identified _schema.resultFields_
- will be used to extract a value from those objects for the output result.
-
- To extract additional information from the JSON, include an array of
- path locators in _schema.metaFields_. The collected values will be
- stored in `response.meta`.
-
-
- @example
- // Process array of arrays
- var schema = {
- resultListLocator: 'produce.fruit',
- resultFields: [ 'name', 'color' ]
- },
- data = {
- produce: {
- fruit: [
- [ 'Banana', 'yellow' ],
- [ 'Orange', 'orange' ],
- [ 'Eggplant', 'purple' ]
- ]
- }
- };
-
- var response = Y.DataSchema.JSON.apply(schema, data);
-
- // response.results[0] is { name: "Banana", color: "yellow" }
-
-
- // Process array of objects + some metadata
- schema.metaFields = [ 'lastInventory' ];
-
- data = {
- produce: {
- fruit: [
- { name: 'Banana', color: 'yellow', price: '1.96' },
- { name: 'Orange', color: 'orange', price: '2.04' },
- { name: 'Eggplant', color: 'purple', price: '4.31' }
- ]
- },
- lastInventory: '2011-07-19'
- };
-
- response = Y.DataSchema.JSON.apply(schema, data);
-
- // response.results[0] is { name: "Banana", color: "yellow" }
- // response.meta.lastInventory is '2001-07-19'
-
-
- // Use parsers
- schema.resultFields = [
- {
- key: 'name',
- parser: function (val) { return val.toUpperCase(); }
- },
- {
- key: 'price',
- parser: 'number' // Uses Y.Parsers.number
- }
- ];
-
- response = Y.DataSchema.JSON.apply(schema, data);
-
- // Note price was converted from a numeric string to a number
- // response.results[0] looks like { fruit: "BANANA", price: 1.96 }
-
- @method apply
- @param {Object} [schema] Schema to apply. Supported configuration
- properties are:
- @param {String} [schema.resultListLocator] Path locator for the
- location of the array of records to flatten into `response.results`
- @param {Array} [schema.resultFields] Field identifiers to
- locate/assign values in the response records. See above for
- details.
- @param {Array} [schema.metaFields] Path locators to extract extra
- non-record related information from the data object.
- @param {Object|Array|String} data JSON data or its string serialization.
- @return {Object} An Object with properties `results` and `meta`
- @static
- **/
- apply: function(schema, data) {
- var data_in = data,
- data_out = { results: [], meta: {} };
-
- // Convert incoming JSON strings
- if (!isObject(data)) {
- try {
- data_in = Y.JSON.parse(data);
- }
- catch(e) {
- data_out.error = e;
- return data_out;
- }
- }
-
- if (isObject(data_in) && schema) {
- // Parse results data
- data_out = SchemaJSON._parseResults.call(this, schema, data_in, data_out);
-
- // Parse meta data
- if (schema.metaFields !== undefined) {
- data_out = SchemaJSON._parseMeta(schema.metaFields, data_in, data_out);
- }
- }
- else {
- data_out.error = new Error("JSON schema parse failure");
- }
-
- return data_out;
- },
-
- /**
- * Schema-parsed list of results from full data
- *
- * @method _parseResults
- * @param schema {Object} Schema to parse against.
- * @param json_in {Object} JSON to parse.
- * @param data_out {Object} In-progress parsed data to update.
- * @return {Object} Parsed data object.
- * @static
- * @protected
- */
- _parseResults: function(schema, json_in, data_out) {
- var getPath = SchemaJSON.getPath,
- getValue = SchemaJSON.getLocationValue,
- path = getPath(schema.resultListLocator),
- results = path ?
- (getValue(path, json_in) ||
- // Fall back to treat resultListLocator as a simple key
- json_in[schema.resultListLocator]) :
- // Or if no resultListLocator is supplied, use the input
- json_in;
-
- if (isArray(results)) {
- // if no result fields are passed in, then just take
- // the results array whole-hog Sometimes you're getting
- // an array of strings, or want the whole object, so
- // resultFields don't make sense.
- if (isArray(schema.resultFields)) {
- data_out = SchemaJSON._getFieldValues.call(this, schema.resultFields, results, data_out);
- } else {
- data_out.results = results;
- }
- } else if (schema.resultListLocator) {
- data_out.results = [];
- data_out.error = new Error("JSON results retrieval failure");
- }
-
- return data_out;
- },
-
- /**
- * Get field data values out of list of full results
- *
- * @method _getFieldValues
- * @param fields {Array} Fields to find.
- * @param array_in {Array} Results to parse.
- * @param data_out {Object} In-progress parsed data to update.
- * @return {Object} Parsed data object.
- * @static
- * @protected
- */
- _getFieldValues: function(fields, array_in, data_out) {
- var results = [],
- len = fields.length,
- i, j,
- field, key, locator, path, parser, val,
- simplePaths = [], complexPaths = [], fieldParsers = [],
- result, record;
-
- // First collect hashes of simple paths, complex paths, and parsers
- for (i=0; i<len; i++) {
- field = fields[i]; // A field can be a simple string or a hash
- key = field.key || field; // Find the key
- locator = field.locator || key; // Find the locator
-
- // Validate and store locators for later
- path = SchemaJSON.getPath(locator);
- if (path) {
- if (path.length === 1) {
- simplePaths.push({
- key : key,
- path: path[0]
- });
- } else {
- complexPaths.push({
- key : key,
- path : path,
- locator: locator
- });
- }
- } else {
- }
-
- // Validate and store parsers for later
- //TODO: use Y.DataSchema.parse?
- parser = (isFunction(field.parser)) ?
- field.parser :
- Y.Parsers[field.parser + ''];
-
- if (parser) {
- fieldParsers.push({
- key : key,
- parser: parser
- });
- }
- }
-
- // Traverse list of array_in, creating records of simple fields,
- // complex fields, and applying parsers as necessary
- for (i=array_in.length-1; i>=0; --i) {
- record = {};
- result = array_in[i];
- if(result) {
- // Cycle through complexLocators
- for (j=complexPaths.length - 1; j>=0; --j) {
- path = complexPaths[j];
- val = SchemaJSON.getLocationValue(path.path, result);
- if (val === undefined) {
- val = SchemaJSON.getLocationValue([path.locator], result);
- // Fail over keys like "foo.bar" from nested parsing
- // to single token parsing if a value is found in
- // results["foo.bar"]
- if (val !== undefined) {
- simplePaths.push({
- key: path.key,
- path: path.locator
- });
- // Don't try to process the path as complex
- // for further results
- complexPaths.splice(i,1);
- continue;
- }
- }
-
- record[path.key] = Base.parse.call(this,
- (SchemaJSON.getLocationValue(path.path, result)), path);
- }
-
- // Cycle through simpleLocators
- for (j = simplePaths.length - 1; j >= 0; --j) {
- path = simplePaths[j];
- // Bug 1777850: The result might be an array instead of object
- record[path.key] = Base.parse.call(this,
- ((result[path.path] === undefined) ?
- result[j] : result[path.path]), path);
- }
-
- // Cycle through fieldParsers
- for (j=fieldParsers.length-1; j>=0; --j) {
- key = fieldParsers[j].key;
- record[key] = fieldParsers[j].parser.call(this, record[key]);
- // Safety net
- if (record[key] === undefined) {
- record[key] = null;
- }
- }
- results[i] = record;
- }
- }
- data_out.results = results;
- return data_out;
- },
-
- /**
- * Parses results data according to schema
- *
- * @method _parseMeta
- * @param metaFields {Object} Metafields definitions.
- * @param json_in {Object} JSON to parse.
- * @param data_out {Object} In-progress parsed data to update.
- * @return {Object} Schema-parsed meta data.
- * @static
- * @protected
- */
- _parseMeta: function(metaFields, json_in, data_out) {
- if (isObject(metaFields)) {
- var key, path;
- for(key in metaFields) {
- if (metaFields.hasOwnProperty(key)) {
- path = SchemaJSON.getPath(metaFields[key]);
- if (path && json_in) {
- data_out.meta[key] = SchemaJSON.getLocationValue(path, json_in);
- }
- }
- }
- }
- else {
- data_out.error = new Error("JSON meta data retrieval failure");
- }
- return data_out;
- }
- };
-
- // TODO: Y.Object + mix() might be better here
- Y.DataSchema.JSON = Y.mix(SchemaJSON, Base);
-
-
- }, '3.4.0' ,{requires:['dataschema-base','json']});
|