Skip to content

Commit

Permalink
Merge pull request #30 from talyssonoc/allow-circular-reference
Browse files Browse the repository at this point in the history
Allow circular reference
  • Loading branch information
Fleury committed Feb 1, 2017
2 parents 2847b4d + df54598 commit 896a908
Show file tree
Hide file tree
Showing 30 changed files with 647 additions and 60 deletions.
7 changes: 6 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
"mocha": true
},
"extends": "eslint:recommended",
"plugins": [
"mocha"
],
"rules": {
"comma-spacing": [2, { "before": false, "after": true }],
"indent": [2, 2],
"linebreak-style": [2, "unix"],
"quotes": [2, "single"],
"semi": [2, "always"]
"semi": [2, "always"],
"mocha/no-exclusive-tests": 2,
"mocha/no-skipped-tests": 2
}
}
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ node_js:
- "6.1"
- "6.0"
script:
- npm run lint
- npm test
- npm run coveralls
69 changes: 68 additions & 1 deletion benchmark/coercion.bm.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class FantasyBooksCollection extends Array { }
class FriendsCollection extends Array { }
class Item { }

var User = attributes({
const User = attributes({
name: String,
item: Item,
favoriteBook: Book,
Expand All @@ -26,6 +26,8 @@ var User = attributes({
}
})(class User { });

const CircularUser = require('../test/fixtures/CircularUser');

exports.name = 'Coercion';

exports.cases = [
Expand Down Expand Up @@ -67,6 +69,71 @@ exports.cases = [
});
}
},
{
name: 'Primitive coercion with dynamic types',
fn() {
new CircularUser({
name: 50
});
}
},
{
name: 'Nested coercion with dynamic types [x1]',
fn() {
new CircularUser({
favoriteBook: {
name: 'A Study in Scarlet'
}
});
}
},
{
name: 'Nested coercion with dynamic types [x2]',
fn() {
new CircularUser({
favoriteBook: {
name: 1984
}
});
}
},
{
name: 'Nested coercion with dynamic types [x3]',
fn() {
new CircularUser({
friends: [
new CircularUser(),
new CircularUser()
]
});
}
},
{
name: 'Nested coercion with dynamic types [x4]',
fn() {
new CircularUser({
friends: [
{},
{}
]
});
}
},
{
name: 'Nested coercion with dynamic types [x5]',
fn() {
new CircularUser({
friends: [
{
favoriteBook: {}
},
{
favoriteBook: {}
}
]
});
}
},
{
name: 'Array coercion [1x]',
fn() {
Expand Down
41 changes: 41 additions & 0 deletions benchmark/instantiation.bm.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const Product = attributes({
}
})(class Product { });

const CircularUser = require('../test/fixtures/CircularUser');
const CircularBook = require('../test/fixtures/CircularBook');

exports.name = 'Instantiation';

exports.cases = [
Expand Down Expand Up @@ -49,5 +52,43 @@ exports.cases = [
]
});
}
},
{
name: 'Simple instantiation with dynamic types',
fn() {
new CircularUser({
name: 'A name'
});
}
},
{
name: 'Complex instantiation with dynamic types [x1]',
fn() {
new CircularUser({
name: 'A name',
favoriteBook: new CircularBook({
name: 'A book'
})
});
}
},
{
name: 'Complex instantiation with dynamic types [x2]',
fn() {
new CircularUser({
name: 'A name',
friends: [
new CircularUser({
name: 'A friend'
}),
new CircularUser({
name: 'Another friend'
})
],
favoriteBook: new CircularBook({
name: 'A book'
})
});
}
}
];
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"chai": "^3.5.0",
"coveralls": "^2.11.15",
"eslint": "^3.8.1",
"eslint-plugin-mocha": "^4.8.0",
"istanbul": "^0.4.5",
"joi-browser": "^10.0.6",
"karma": "^1.3.0",
Expand Down
8 changes: 4 additions & 4 deletions src/attributesDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const {

const define = Object.defineProperty;

function attributesDecorator(declaredSchema) {
if(arguments.length > 1) {
const ErroneousPassedClass = arguments[1];
function attributesDecorator(declaredSchema, schemaOptions = {}) {
if(typeof schemaOptions !== 'object') {
const ErroneousPassedClass = schemaOptions;

const errorMessage = `You passed the structure class as the second parameter of attributes(). The expected usage is \`attributes(schema)(${ ErroneousPassedClass.name || 'StructureClass' })\`.`;

Expand All @@ -34,7 +34,7 @@ function attributesDecorator(declaredSchema) {
}
});

declaredSchema = normalizeSchema(declaredSchema);
declaredSchema = normalizeSchema(declaredSchema, schemaOptions);

if(WrapperClass[SCHEMA]) {
declaredSchema = Object.assign({}, WrapperClass[SCHEMA], declaredSchema);
Expand Down
53 changes: 28 additions & 25 deletions src/schemaNormalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@ const { coercionFor } = require('./typeCoercion');
const { validationForAttribute, validationForSchema } = require('./validation');
const { VALIDATE } = require('./symbols');

function normalizeAttribute(attribute, attributeName) {
function normalizeSchema(rawSchema, schemaOptions) {
const schema = Object.create(null);

Object.keys(rawSchema).forEach((attributeName) => {
schema[attributeName] = normalizeAttribute(schemaOptions, rawSchema[attributeName], attributeName);
});

const schemaValidation = validationForSchema(schema);

Object.defineProperty(schema, VALIDATE, {
value: schemaValidation
});

return schema;
}

function normalizeAttribute(schemaOptions, attribute, attributeName) {
switch(typeof attribute) {
case 'object':
if(!attribute.type) {
throw new Error(`Missing type for attribute: ${ attributeName }.`);
}

if(typeof attribute.type !== 'function') {
if (typeof attribute.type === 'string') {
if(!schemaOptions.dynamics || !schemaOptions.dynamics[attribute.type]) {
throw new Error(`There is no dynamic type for attribute: ${ attributeName }.`);
}

attribute.getType = schemaOptions.dynamics[attribute.type];
attribute.dynamicType = true;
} else if(typeof attribute.type !== 'function') {
throw new TypeError(`Attribute type must be a constructor: ${ attributeName }.`);
}

if(attribute.itemType) {
attribute.itemType = normalizeAttribute(attribute.itemType, 'itemType');
attribute.itemType = normalizeAttribute(schemaOptions, attribute.itemType, 'itemType');
}

return Object.assign({}, attribute, {
Expand All @@ -23,33 +46,13 @@ function normalizeAttribute(attribute, attributeName) {
});

case 'function':
var normalizedType = { type: attribute };
normalizedType.coerce = coercionFor(normalizedType);
normalizedType.validation = validationForAttribute(normalizedType);

return normalizedType;
case 'string':
return normalizeAttribute(schemaOptions, { type: attribute }, attributeName);

default:
throw new TypeError(`Invalid type for attribute: ${ attributeName }.`);
}
}

function normalizeSchema(rawSchema) {
const schema = Object.create(null);

Object.keys(rawSchema).forEach((attributeName) => {
schema[attributeName] = normalizeAttribute(rawSchema[attributeName], attributeName);
});

const schemaValidation = validationForSchema(schema);

Object.defineProperty(schema, VALIDATE, {
value: schemaValidation
});


return schema;
}

exports.normalizeSchema = normalizeSchema;
exports.VALIDATE = VALIDATE;
9 changes: 7 additions & 2 deletions src/serialization.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { SCHEMA } = require('./symbols');
const { getType } = require('./typeResolver');

function serialize(structure) {
if(structure === undefined) {
Expand All @@ -17,9 +18,9 @@ function serialize(structure) {

let serializedValue;

if(schema[attrName].itemType && schema[attrName].itemType.type[SCHEMA] !== undefined) {
if(schema[attrName].itemType && getTypeSchema(schema[attrName].itemType)) {
serializedValue = attribute.map(serialize);
} else if(schema[attrName].type[SCHEMA] !== undefined) {
} else if(getTypeSchema(schema[attrName])) {
serializedValue = serialize(attribute);
} else {
serializedValue = attribute;
Expand All @@ -31,4 +32,8 @@ function serialize(structure) {
return serializedStructure;
}

function getTypeSchema(typeDescriptor) {
return getType(typeDescriptor)[SCHEMA];
}

exports.serialize = serialize;
4 changes: 3 additions & 1 deletion src/typeCoercion/array.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { ARRAY_OR_ITERABLE } = require('../errorMessages');
const { getType } = require('../typeResolver');

module.exports = function arrayCoercionFor(typeDescriptor, itemTypeDescriptor) {
return function coerceArray(value) {
Expand All @@ -14,7 +15,8 @@ module.exports = function arrayCoercionFor(typeDescriptor, itemTypeDescriptor) {
value = Array(...value);
}

const coercedValue = new typeDescriptor.type();
const type = getType(typeDescriptor);
const coercedValue = new type();

for(let i = 0; i < value.length; i++) {
coercedValue.push(itemTypeDescriptor.coerce(value[i]));
Expand Down
8 changes: 6 additions & 2 deletions src/typeCoercion/generic.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
const { getType } = require('../typeResolver');

module.exports = function genericCoercionFor(typeDescriptor) {
return function coerce(value) {
if(value === undefined) {
return;
}

if(value instanceof typeDescriptor.type) {
const type = getType(typeDescriptor);

if(value instanceof type) {
return value;
}

return new typeDescriptor.type(value);
return new type(value);
};
};
7 changes: 7 additions & 0 deletions src/typeResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
exports.getType = function getType(typeDescriptor) {
if(typeDescriptor.dynamicType) {
return typeDescriptor.getType();
}

return typeDescriptor.type;
};

0 comments on commit 896a908

Please sign in to comment.