2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('dataschema-base', function(Y) {
11 * The DataSchema utility provides a common configurable interface for widgets to
12 * apply a given schema to a variety of data.
18 * Provides the base DataSchema implementation, which can be extended to
19 * create DataSchemas for specific data formats, such XML, JSON, text and
23 * @submodule dataschema-base
28 * Base class for the YUI DataSchema Utility.
29 * @class DataSchema.Base
34 * Overridable method returns data as-is.
37 * @param schema {Object} Schema to apply.
38 * @param data {Object} Data.
39 * @return {Object} Schema-parsed data.
42 apply: function(schema, data) {
47 * Applies field parser, if defined
50 * @param value {Object} Original value.
51 * @param field {Object} Field.
52 * @return {Object} Type-converted value.
54 parse: function(value, field) {
56 var parser = (LANG.isFunction(field.parser)) ?
57 field.parser : Y.Parsers[field.parser+''];
59 value = parser.call(this, value);
68 Y.namespace("DataSchema").Base = SchemaBase;
69 Y.namespace("Parsers");
73 }, '3.3.0' ,{requires:['base']});
75 YUI.add('dataschema-json', function(Y) {
78 * Provides a DataSchema implementation which can be used to work with JSON data.
81 * @submodule dataschema-json
85 * JSON subclass for the DataSchema Utility.
86 * @class DataSchema.JSON
87 * @extends DataSchema.Base
94 /////////////////////////////////////////////////////////////////////////////
96 // DataSchema.JSON static methods
98 /////////////////////////////////////////////////////////////////////////////
100 * Utility function converts JSON locator strings into walkable paths
102 * @method DataSchema.JSON.getPath
103 * @param locator {String} JSON value locator.
104 * @return {String[]} Walkable path to data value.
107 getPath: function(locator) {
113 // Strip the ["string keys"] and [1] array indexes
115 replace(/\[(['"])(.*?)\1\]/g,
116 function (x,$1,$2) {keys[i]=$2;return '.@'+(i++);}).
117 replace(/\[(\d+)\]/g,
118 function (x,$1) {keys[i]=parseInt($1,10)|0;return '.@'+(i++);}).
119 replace(/^\./,''); // remove leading dot
121 // Validate against problematic characters.
122 if (!/[^\w\.\$@]/.test(locator)) {
123 path = locator.split('.');
124 for (i=path.length-1; i >= 0; --i) {
125 if (path[i].charAt(0) === '@') {
126 path[i] = keys[parseInt(path[i].substr(1),10)];
137 * Utility function to walk a path and return the value located there.
139 * @method DataSchema.JSON.getLocationValue
140 * @param path {String[]} Locator path.
141 * @param data {String} Data to traverse.
142 * @return {Object} Data value at location.
145 getLocationValue: function (path, data) {
150 LANG.isObject(data) &&
153 data = data[path[i]];
164 * Applies a given schema to given JSON data.
167 * @param schema {Object} Schema to apply.
168 * @param data {Object} JSON data.
169 * @return {Object} Schema-parsed data.
172 apply: function(schema, data) {
174 data_out = {results:[],meta:{}};
176 // Convert incoming JSON strings
177 if(!LANG.isObject(data)) {
179 data_in = Y.JSON.parse(data);
187 if(LANG.isObject(data_in) && schema) {
188 // Parse results data
189 if(!LANG.isUndefined(schema.resultListLocator)) {
190 data_out = SchemaJSON._parseResults.call(this, schema, data_in, data_out);
194 if(!LANG.isUndefined(schema.metaFields)) {
195 data_out = SchemaJSON._parseMeta(schema.metaFields, data_in, data_out);
199 data_out.error = new Error("JSON schema parse failure");
206 * Schema-parsed list of results from full data
208 * @method _parseResults
209 * @param schema {Object} Schema to parse against.
210 * @param json_in {Object} JSON to parse.
211 * @param data_out {Object} In-progress parsed data to update.
212 * @return {Object} Parsed data object.
216 _parseResults: function(schema, json_in, data_out) {
221 if(schema.resultListLocator) {
222 path = SchemaJSON.getPath(schema.resultListLocator);
224 results = SchemaJSON.getLocationValue(path, json_in);
225 if (results === undefined) {
226 data_out.results = [];
227 error = new Error("JSON results retrieval failure");
230 if(LANG.isArray(results)) {
231 // if no result fields are passed in, then just take the results array whole-hog
232 // Sometimes you're getting an array of strings, or want the whole object,
233 // so resultFields don't make sense.
234 if (LANG.isArray(schema.resultFields)) {
235 data_out = SchemaJSON._getFieldValues.call(this, schema.resultFields, results, data_out);
238 data_out.results = results;
242 data_out.results = [];
243 error = new Error("JSON Schema fields retrieval failure");
248 error = new Error("JSON Schema results locator failure");
252 data_out.error = error;
260 * Get field data values out of list of full results
262 * @method _getFieldValues
263 * @param fields {Array} Fields to find.
264 * @param array_in {Array} Results to parse.
265 * @param data_out {Object} In-progress parsed data to update.
266 * @return {Object} Parsed data object.
270 _getFieldValues: function(fields, array_in, data_out) {
274 field, key, locator, path, parser,
275 simplePaths = [], complexPaths = [], fieldParsers = [],
278 // First collect hashes of simple paths, complex paths, and parsers
279 for (i=0; i<len; i++) {
280 field = fields[i]; // A field can be a simple string or a hash
281 key = field.key || field; // Find the key
282 locator = field.locator || key; // Find the locator
284 // Validate and store locators for later
285 path = SchemaJSON.getPath(locator);
287 if (path.length === 1) {
288 simplePaths[simplePaths.length] = {key:key, path:path[0]};
290 complexPaths[complexPaths.length] = {key:key, path:path};
295 // Validate and store parsers for later
296 //TODO: use Y.DataSchema.parse?
297 parser = (LANG.isFunction(field.parser)) ? field.parser : Y.Parsers[field.parser+''];
299 fieldParsers[fieldParsers.length] = {key:key, parser:parser};
303 // Traverse list of array_in, creating records of simple fields,
304 // complex fields, and applying parsers as necessary
305 for (i=array_in.length-1; i>=0; --i) {
307 result = array_in[i];
309 // Cycle through simpleLocators
310 for (j=simplePaths.length-1; j>=0; --j) {
311 // Bug 1777850: The result might be an array instead of object
312 record[simplePaths[j].key] = Y.DataSchema.Base.parse.call(this,
313 (LANG.isUndefined(result[simplePaths[j].path]) ?
314 result[j] : result[simplePaths[j].path]), simplePaths[j]);
317 // Cycle through complexLocators
318 for (j=complexPaths.length - 1; j>=0; --j) {
319 record[complexPaths[j].key] = Y.DataSchema.Base.parse.call(this,
320 (SchemaJSON.getLocationValue(complexPaths[j].path, result)), complexPaths[j] );
323 // Cycle through fieldParsers
324 for (j=fieldParsers.length-1; j>=0; --j) {
325 key = fieldParsers[j].key;
326 record[key] = fieldParsers[j].parser.call(this, record[key]);
328 if (LANG.isUndefined(record[key])) {
335 data_out.results = results;
340 * Parses results data according to schema
343 * @param metaFields {Object} Metafields definitions.
344 * @param json_in {Object} JSON to parse.
345 * @param data_out {Object} In-progress parsed data to update.
346 * @return {Object} Schema-parsed meta data.
350 _parseMeta: function(metaFields, json_in, data_out) {
351 if(LANG.isObject(metaFields)) {
353 for(key in metaFields) {
354 if (metaFields.hasOwnProperty(key)) {
355 path = SchemaJSON.getPath(metaFields[key]);
356 if (path && json_in) {
357 data_out.meta[key] = SchemaJSON.getLocationValue(path, json_in);
363 data_out.error = new Error("JSON meta data retrieval failure");
369 Y.DataSchema.JSON = Y.mix(SchemaJSON, Y.DataSchema.Base);
373 }, '3.3.0' ,{requires:['dataschema-base','json']});
375 YUI.add('dataschema-xml', function(Y) {
378 * Provides a DataSchema implementation which can be used to work with XML data.
381 * @submodule dataschema-xml
386 * XML subclass for the DataSchema Utility.
387 * @class DataSchema.XML
388 * @extends DataSchema.Base
393 /////////////////////////////////////////////////////////////////////////////
395 // DataSchema.XML static methods
397 /////////////////////////////////////////////////////////////////////////////
399 * Applies a given schema to given XML data.
402 * @param schema {Object} Schema to apply.
403 * @param data {XMLDoc} XML document.
404 * @return {Object} Schema-parsed data.
407 apply: function(schema, data) {
408 var xmldoc = data, // unnecessary variables
409 data_out = {results:[],meta:{}};
411 if(xmldoc && xmldoc.nodeType && (9 === xmldoc.nodeType || 1 === xmldoc.nodeType || 11 === xmldoc.nodeType) && schema) {
412 // Parse results data
413 data_out = SchemaXML._parseResults.call(this, schema, xmldoc, data_out);
416 data_out = SchemaXML._parseMeta.call(this, schema.metaFields, xmldoc, data_out);
419 data_out.error = new Error("XML schema parse failure");
426 * Get an XPath-specified value for a given field from an XML node or document.
428 * @method _getLocationValue
429 * @param field {String | Object} Field definition.
430 * @param context {Object} XML node or document to search within.
431 * @return {Object} Data value or null.
435 _getLocationValue: function(field, context) {
436 var locator = field.locator || field.key || field,
437 xmldoc = context.ownerDocument || context,
438 result, res, value = null;
441 result = SchemaXML._getXPathResult(locator, context, xmldoc);
442 while(res = result.iterateNext()) {
443 value = res.textContent || res.value || res.text || res.innerHTML || null;
446 return Y.DataSchema.Base.parse.call(this, value, field);
455 * Fetches the XPath-specified result for a given location in an XML node or document.
457 * @param locator {String} The XPath location.
458 * @param context {Object} XML node or document to search within.
459 * @param xmldoc {Object} XML document to resolve namespace.
460 * @return {Object} Data collection or null.
464 _getXPathResult: function(locator, context, xmldoc) {
466 if (! LANG.isUndefined(xmldoc.evaluate)) {
467 return xmldoc.evaluate(locator, context, xmldoc.createNSResolver(context.ownerDocument ? context.ownerDocument.documentElement : context.documentElement), 0, null);
471 var values=[], locatorArray = locator.split(/\b\/\b/), i=0, l=locatorArray.length, location, subloc, m, isNth;
473 // XPath is supported
475 // this fixes the IE 5.5+ issue where childnode selectors begin at 0 instead of 1
476 xmldoc.setProperty("SelectionLanguage", "XPath");
477 values = context.selectNodes(locator);
479 // Fallback for DOM nodes and fragments
481 // Iterate over each locator piece
482 for (; i<l && context; i++) {
483 location = locatorArray[i];
486 if ((location.indexOf("[") > -1) && (location.indexOf("]") > -1)) {
487 subloc = location.slice(location.indexOf("[")+1, location.indexOf("]"));
488 //XPath is 1-based while DOM is 0-based
490 context = context.children[subloc];
493 // grab attribute value @
494 else if (location.indexOf("@") > -1) {
495 subloc = location.substr(location.indexOf("@"));
496 context = subloc ? context.getAttribute(subloc.replace('@', '')) : context;
498 // grab that last instance of tagName
499 else if (-1 < location.indexOf("//")) {
500 subloc = context.getElementsByTagName(location.substr(2));
501 context = subloc.length ? subloc[subloc.length - 1] : null;
503 // find the last matching location in children
504 else if (l != i + 1) {
505 for (m=context.childNodes.length-1; 0 <= m; m-=1) {
506 if (location === context.childNodes[m].tagName) {
507 context = context.childNodes[m];
516 if (LANG.isString(context)) {
517 values[0] = {value: context};
521 values[0] = {value: context.innerHTML};
525 values = Y.Array(context.childNodes, 0, true);
530 // returning a mock-standard object for IE
534 iterateNext: function() {
535 if (this.index >= this.values.length) {return undefined;}
536 var result = this.values[this.index];
547 * Schema-parsed result field.
549 * @method _parseField
550 * @param field {String | Object} Required. Field definition.
551 * @param result {Object} Required. Schema parsed data object.
552 * @param context {Object} Required. XML node or document to search within.
556 _parseField: function(field, result, context) {
558 result[field.key] = SchemaXML._parseResults.call(this, field.schema, context, {results:[],meta:{}}).results;
561 result[field.key || field] = SchemaXML._getLocationValue.call(this, field, context);
566 * Parses results data according to schema
569 * @param xmldoc_in {Object} XML document parse.
570 * @param data_out {Object} In-progress schema-parsed data to update.
571 * @return {Object} Schema-parsed data.
575 _parseMeta: function(metaFields, xmldoc_in, data_out) {
576 if(LANG.isObject(metaFields)) {
578 xmldoc = xmldoc_in.ownerDocument || xmldoc_in;
580 for(key in metaFields) {
581 if (metaFields.hasOwnProperty(key)) {
582 data_out.meta[key] = SchemaXML._getLocationValue.call(this, metaFields[key], xmldoc);
590 * Schema-parsed result to add to results list.
592 * @method _parseResult
593 * @param fields {Array} Required. A collection of field definition.
594 * @param context {Object} Required. XML node or document to search within.
595 * @return {Object} Schema-parsed data.
599 _parseResult: function(fields, context) {
602 // Find each field value
603 for (j=fields.length-1; 0 <= j; j--) {
604 SchemaXML._parseField.call(this, fields[j], result, context);
611 * Schema-parsed list of results from full data
613 * @method _parseResults
614 * @param schema {Object} Schema to parse against.
615 * @param context {Object} XML node or document to parse.
616 * @param data_out {Object} In-progress schema-parsed data to update.
617 * @return {Object} Schema-parsed data.
621 _parseResults: function(schema, context, data_out) {
622 if (schema.resultListLocator && LANG.isArray(schema.resultFields)) {
623 var xmldoc = context.ownerDocument || context,
624 fields = schema.resultFields,
626 node, result, nodeList, i=0;
628 if (schema.resultListLocator.match(/^[:\-\w]+$/)) {
629 nodeList = context.getElementsByTagName(schema.resultListLocator);
631 // loop through each result node
632 for (i=nodeList.length-1; 0 <= i; i--) {
633 results[i] = SchemaXML._parseResult.call(this, fields, nodeList[i]);
637 nodeList = SchemaXML._getXPathResult(schema.resultListLocator, context, xmldoc);
639 // loop through the nodelist
640 while (node = nodeList.iterateNext()) {
641 results[i] = SchemaXML._parseResult.call(this, fields, node);
646 if (results.length) {
647 data_out.results = results;
650 data_out.error = new Error("XML schema result nodes retrieval failure");
657 Y.DataSchema.XML = Y.mix(SchemaXML, Y.DataSchema.Base);
661 }, '3.3.0' ,{requires:['dataschema-base']});
663 YUI.add('dataschema-array', function(Y) {
666 * Provides a DataSchema implementation which can be used to work with data stored in arrays.
669 * @submodule dataschema-array
673 * Array subclass for the DataSchema Utility.
674 * @class DataSchema.Array
675 * @extends DataSchema.Base
682 /////////////////////////////////////////////////////////////////////////////
684 // DataSchema.Array static methods
686 /////////////////////////////////////////////////////////////////////////////
688 * Applies a given schema to given Array data.
691 * @param schema {Object} Schema to apply.
692 * @param data {Object} Array data.
693 * @return {Object} Schema-parsed data.
696 apply: function(schema, data) {
698 data_out = {results:[],meta:{}};
700 if(LANG.isArray(data_in)) {
701 if(LANG.isArray(schema.resultFields)) {
702 // Parse results data
703 data_out = SchemaArray._parseResults.call(this, schema.resultFields, data_in, data_out);
706 data_out.results = data_in;
710 data_out.error = new Error("Array schema parse failure");
717 * Schema-parsed list of results from full data
719 * @method _parseResults
720 * @param fields {Array} Schema to parse against.
721 * @param array_in {Array} Array to parse.
722 * @param data_out {Object} In-progress parsed data to update.
723 * @return {Object} Parsed data object.
727 _parseResults: function(fields, array_in, data_out) {
729 result, item, type, field, key, value, i, j;
731 for(i=array_in.length-1; i>-1; i--) {
734 type = (LANG.isObject(item) && !LANG.isFunction(item)) ? 2 : (LANG.isArray(item)) ? 1 : (LANG.isString(item)) ? 0 : -1;
736 for(j=fields.length-1; j>-1; j--) {
738 key = (!LANG.isUndefined(field.key)) ? field.key : field;
739 value = (!LANG.isUndefined(item[key])) ? item[key] : item[j];
740 result[key] = Y.DataSchema.Base.parse.call(this, value, field);
743 else if(type === 0) {
752 data_out.results = results;
758 Y.DataSchema.Array = Y.mix(SchemaArray, Y.DataSchema.Base);
762 }, '3.3.0' ,{requires:['dataschema-base']});
764 YUI.add('dataschema-text', function(Y) {
767 * Provides a DataSchema implementation which can be used to work with delimited text data.
770 * @submodule dataschema-text
774 * Text subclass for the DataSchema Utility.
775 * @class DataSchema.Text
776 * @extends DataSchema.Base
784 /////////////////////////////////////////////////////////////////////////////
786 // DataSchema.Text static methods
788 /////////////////////////////////////////////////////////////////////////////
790 * Applies a given schema to given delimited text data.
793 * @param schema {Object} Schema to apply.
794 * @param data {Object} Text data.
795 * @return {Object} Schema-parsed data.
798 apply: function(schema, data) {
800 data_out = {results:[],meta:{}};
802 if(LANG.isString(data_in) && LANG.isString(schema.resultDelimiter)) {
803 // Parse results data
804 data_out = SchemaText._parseResults.call(this, schema, data_in, data_out);
807 data_out.error = new Error("Text schema parse failure");
814 * Schema-parsed list of results from full data
816 * @method _parseResults
817 * @param schema {Array} Schema to parse against.
818 * @param text_in {String} Text to parse.
819 * @param data_out {Object} In-progress parsed data to update.
820 * @return {Object} Parsed data object.
824 _parseResults: function(schema, text_in, data_out) {
825 var resultDelim = schema.resultDelimiter,
827 results_in, fields_in, result, item, fields, field, key, value, i, j,
829 // Delete final delimiter at end of string if there
830 tmpLength = text_in.length-resultDelim.length;
831 if(text_in.substr(tmpLength) == resultDelim) {
832 text_in = text_in.substr(0, tmpLength);
835 // Split into results
836 results_in = text_in.split(schema.resultDelimiter);
838 for(i=results_in.length-1; i>-1; i--) {
840 item = results_in[i];
842 if(LANG.isString(schema.fieldDelimiter)) {
843 fields_in = item.split(schema.fieldDelimiter);
845 if(LANG.isArray(schema.resultFields)) {
846 fields = schema.resultFields;
847 for(j=fields.length-1; j>-1; j--) {
849 key = (!LANG.isUndefined(field.key)) ? field.key : field;
850 value = (!LANG.isUndefined(fields_in[key])) ? fields_in[key] : fields_in[j];
851 result[key] = Y.DataSchema.Base.parse.call(this, value, field);
862 data_out.results = results;
868 Y.DataSchema.Text = Y.mix(SchemaText, Y.DataSchema.Base);
872 }, '3.3.0' ,{requires:['dataschema-base']});
876 YUI.add('dataschema', function(Y){}, '3.3.0' ,{use:['dataschema-base','dataschema-json','dataschema-xml','dataschema-array','dataschema-text']});