From 57c181c8b9a26760b221d2085604afda4e27a63d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 26 Jul 2013 13:06:43 -0700 Subject: [PATCH] Bring up json object introspection to build models --- lib/introspection.js | 59 ++++++++++++++++++++++ lib/model-builder.js | 18 ++----- lib/model.js | 30 +++++++++-- lib/types.js | 6 +-- package.json | 3 +- test/introspection.test.js | 101 +++++++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 lib/introspection.js create mode 100644 test/introspection.test.js diff --git a/lib/introspection.js b/lib/introspection.js new file mode 100644 index 00000000..80e29f8a --- /dev/null +++ b/lib/introspection.js @@ -0,0 +1,59 @@ +var ModelBuilder = require('./model-builder').ModelBuilder; + +function introspectType(value) { + + // Unknown type, using Any + if (value === null || value === undefined) { + return ModelBuilder.Any; + } + + // Check registered schemaTypes + for (var t in ModelBuilder.schemaTypes) { + var st = ModelBuilder.schemaTypes[t]; + if (st !== Object && st !== Array && (value instanceof st)) { + return t; + } + } + + var type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean') { + return type; + } + + if (value instanceof Date) { + return 'date'; + } + + if (Array.isArray(value)) { + for (var i = 0; i < value.length; i++) { + if (value[i] === null || value[i] === undefined) { + continue; + } + var itemType = introspectType(value[i]); + if (itemType) { + return [itemType]; + } + } + return 'array'; + } + + if (type === 'function') { + return value.constructor.name; + } + + var properties = {}; + for (var p in value) { + var itemType = introspectType(value[p]); + if (itemType) { + properties[p] = itemType; + } + } + if(Object.keys(properties).length === 0) { + return 'object'; + } + return properties; +} + +module.exports = introspectType; + + diff --git a/lib/model-builder.js b/lib/model-builder.js index f5b0902e..41d08a9c 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -192,21 +192,13 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett if(!DataType) { throw new Error('Invalid type for property ' + attr); } - if (Array.isArray(DataType)) { + if (Array.isArray(DataType) || DataType === Array) { DataType = List; } else if (DataType.name === 'Date') { var OrigDate = Date; DataType = function Date(arg) { return new OrigDate(arg); }; - } else if (DataType.name === 'JSON' || DataType === JSON) { - DataType = function JSON(s) { - return s; - }; - } else if (DataType.name === 'Text' || DataType === ModelBuilder.Text) { - DataType = function Text(s) { - return s; - }; } else if(typeof DataType === 'string') { DataType = dataSource.getSchemaType(DataType); } @@ -271,10 +263,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett function standartize(properties, settings) { Object.keys(properties).forEach(function (key) { var v = properties[key]; - if ( - typeof v === 'function' || - typeof v === 'object' && v && v.constructor.name === 'Array' - ) { + if (typeof v === 'function' || Array.isArray(v)) { properties[key] = { type: v }; } }); @@ -420,8 +409,9 @@ ModelBuilder.prototype.getSchemaType = function(type) { return this.getSchemaType(type.type); } else { if(!this.anonymousTypesCount) { - this.anonymousTypesCount = 1; + this.anonymousTypesCount = 0; } + this.anonymousTypesCount++; return this.define('AnonymousType' + this.anonymousTypesCount, type, {idInjection: false}); /* console.error(type); diff --git a/lib/model.js b/lib/model.js index 8960032f..77cd1100 100644 --- a/lib/model.js +++ b/lib/model.js @@ -8,6 +8,7 @@ module.exports = ModelBaseClass; */ var util = require('util'); +var traverse = require('traverse'); var jutil = require('./jutil'); var List = require('./list'); var Hookable = require('./hooks'); @@ -30,6 +31,23 @@ function ModelBaseClass(data) { this._initProperties(data, true); } +// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing +function clone(data) { + /* + if(!(data instanceof ModelBaseClass)) { + if(data && (Array.isArray(data) || 'object' === typeof data)) { + return traverse(data).clone(); + } + } + */ + return data; +} +/** + * Initialize properties + * @param data + * @param applySetters + * @private + */ ModelBaseClass.prototype._initProperties = function (data, applySetters) { var self = this; var ctor = this.constructor; @@ -67,13 +85,13 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { for (var i in data) { if (i in properties) { - this.__data[i] = this.__dataWas[i] = data[i]; + this.__data[i] = this.__dataWas[i] = clone(data[i]); } else if (i in ctor.relations) { this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; this.__cachedRelations[i] = data[i]; } else { if(strict === false) { - this.__data[i] = this.__dataWas[i] = data[i]; + this.__data[i] = this.__dataWas[i] = clone(data[i]); } else if(strict === 'throw') { throw new Error('Unknown property: ' + i); } @@ -83,7 +101,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { if (applySetters === true) { Object.keys(data).forEach(function (attr) { if((attr in properties) || (attr in ctor.relations) || strict === false) { - self[attr] = data[attr]; + self[attr] = self.__data[attr] || data[attr]; } }); } @@ -110,8 +128,10 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { self.__data[attr] = String(self.__data[attr]); } } - if (type.name === 'Array' || typeof type === 'object' && type.constructor.name === 'Array') { - self.__data[attr] = new List(self.__data[attr], type, self); + if (type.name === 'Array' || Array.isArray(type)) { + if(!(self.__data[attr] instanceof List)) { + self.__data[attr] = new List(self.__data[attr], type, self); + } } } diff --git a/lib/types.js b/lib/types.js index aea86d02..8967d4c0 100644 --- a/lib/types.js +++ b/lib/types.js @@ -8,7 +8,7 @@ module.exports = function (Types) { */ Types.Text = function Text(value) { if (!(this instanceof Text)) { - return new Text(value); + return value; } this.value = value; }; // Text type @@ -19,7 +19,7 @@ module.exports = function (Types) { Types.JSON = function JSON(value) { if (!(this instanceof JSON)) { - return new JSON(value); + return value; } this.value = value; }; // JSON Object @@ -29,7 +29,7 @@ module.exports = function (Types) { Types.Any = function Any(value) { if (!(this instanceof Any)) { - return new Any(value); + return value; } this.value = value; }; // Any Type diff --git a/package.json b/package.json index 3a008088..15f0e67b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "async": "latest", - "inflection": "~1.2.6" + "inflection": "~1.2.6", + "traverse": "latest" } } diff --git a/test/introspection.test.js b/test/introspection.test.js new file mode 100644 index 00000000..ac3a0e17 --- /dev/null +++ b/test/introspection.test.js @@ -0,0 +1,101 @@ +var assert = require('assert'); +var ModelBuilder = require('../lib/model-builder').ModelBuilder; +var introspectType = require('../lib/introspection'); +var traverse = require('traverse'); + +describe('Introspection of model definitions from JSON', function() { + + it('should handle simple types', function() { + assert.equal(introspectType('123'), 'string'); + assert.equal(introspectType(true), 'boolean'); + assert.equal(introspectType(false), 'boolean'); + assert.equal(introspectType(12), 'number'); + assert.equal(introspectType(new Date()), 'date'); + }); + + it('should handle array types', function() { + var type = introspectType(['123']); + assert.deepEqual(type, ['string'], 'type should be ["string"]'); + type = introspectType([1]); + assert.deepEqual(type, ['number'], 'type should be ["number"]'); + // Stop at first known type + type = introspectType([1, '123']); + assert.deepEqual(type, ['number'], 'type should be ["number"]'); + type = introspectType([null, '123']); + assert.deepEqual(type, ['string'], 'type should be ["string"]'); + + type = introspectType([]); + assert.equal(type, 'array'); + }); + + it('should return Any for null or undefined', function() { + assert.equal(introspectType(null), ModelBuilder.Any); + assert.equal(introspectType(undefined), ModelBuilder.Any); + }); + + it('should return a schema for object', function() { + var json = {a: 'str', b: 0, c: true}; + var type = introspectType(json); + assert.equal(type.a, 'string'); + assert.equal(type.b, 'number'); + assert.equal(type.c, 'boolean'); + }); + + it('should handle nesting objects', function() { + var json = {a: 'str', b: 0, c: true, d: {x: 10, y: 5}}; + var type = introspectType(json); + assert.equal(type.a, 'string'); + assert.equal(type.b, 'number'); + assert.equal(type.c, 'boolean'); + assert.equal(type.d.x, 'number'); + assert.equal(type.d.y, 'number'); + }); + + it('should handle nesting arrays', function() { + var json = {a: 'str', b: 0, c: true, d: [1, 2]}; + var type = introspectType(json); + assert.equal(type.a, 'string'); + assert.equal(type.b, 'number'); + assert.equal(type.c, 'boolean'); + assert.deepEqual(type.d, ['number']); + }); + + it('should build a model from the introspected schema', function(done) { + + var json = { + name: 'Joe', + age: 30, + birthday: new Date(), + vip: true, + address: { + street: '1 Main St', + city: 'San Jose', + state: 'CA', + zipcode: '95131', + country: 'US' + }, + friends: ['John', 'Mary'], + emails: [ + {label: 'work', id: 'x@sample.com'}, + {label: 'home', id: 'x@home.com'} + ], + tags: [] + }; + + var copy = traverse(json).clone(); + + var schema = introspectType(json); + + var builder = new ModelBuilder(); + var Model = builder.define('MyModel', schema, {idInjection: false}); + + // FIXME: [rfeng] The constructor mutates the arguments + var obj = new Model(json); + + obj = obj.toObject(); + + assert.deepEqual(obj, copy); + done(); + }); +}); +