Skip to content

Commit

Permalink
Finished collection factory
Browse files Browse the repository at this point in the history
  • Loading branch information
Henrique Barcelos committed Aug 28, 2017
1 parent 495854a commit d1d72c2
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 66 deletions.
8 changes: 3 additions & 5 deletions src/build-schema.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { allowAny } from './default-validators'
import { omit, pick } from './common'

export default function buildSchema (definition) {
const { $contexts = {}, ...propertyDefinition } = definition
export default function buildSchema (definition, contexts) {
const defaultSchema = fillDefaultValidators(definition)

const defaultSchema = fillDefaultValidators(propertyDefinition)

const contextSchemas = Object.entries($contexts)
const contextSchemas = Object.entries(contexts)
.reduce(
(acc, [ contextName, contextDefinition ]) => ({
[contextName]: applyOperators(contextDefinition, defaultSchema, contextName)
Expand Down
49 changes: 33 additions & 16 deletions src/build-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,102 +6,119 @@ import { omit, pick } from './common'
import * as fixtures from './build-schema.fixture'

test('[Structure] Given schema definition, when final schema is built, then it should have created the default schema', t => {
const result = subject(fixtures.schemaWithoutCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithoutCustomValidators
const result = subject(definition, $contexts)

t.truthy(result.default)
})

test('[Structure] Given schema definition with no contexts, when final schema is built, then it should be no extra keys in it', t => {
const expectedKeys = ['default']

const result = subject(fixtures.schemaWithoutCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithoutCustomValidators
const result = subject(definition, $contexts)

t.deepEqual(Object.keys(result), expectedKeys)
})

test('[Structure] Given schema definition with no contexts, when final schema is built, then it should be no extra keys in it', t => {
const expectedKeys = ['default']

const result = subject(fixtures.schemaWithoutCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithoutCustomValidators
const result = subject(definition, $contexts)

t.deepEqual(Object.keys(result), expectedKeys)
})

test('[Structure] Given schema definition with no contexts, when final schema is built, then it should have created the default schema with all keys from definition', t => {
const expectedKeys = Object.keys(fixtures.schemaWithoutCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithoutCustomValidators

const result = subject(fixtures.schemaWithoutCustomValidators)
const expectedKeys = Object.keys(definition)

const result = subject(definition, $contexts)

t.deepEqual(Object.keys(result.default), expectedKeys)
})

test('[Structure] Given schema definition with context, when final schema is built, then it should create a context schema', t => {
const expectedKeys = ['default', 'myContext']

const result = subject(fixtures.schemaWithContext)
const { $contexts = {}, ...definition } = fixtures.schemaWithContext
const result = subject(definition, $contexts)

t.deepEqual(Object.keys(result), expectedKeys)
})

test('[Structure] Given schema definition with context, when final schema is built, then it should not have a `$contexts` property on default schema', t => {
const result = subject(fixtures.schemaWithContext)
const { $contexts = {}, ...definition } = fixtures.schemaWithContext
const result = subject(definition, $contexts)

t.false(result.default.hasOwnProperty('$contexts'))
})

test('[Validators] Given schema definition with no validators, when final schema is built, then it should use the default `allowAny` validator for leaf fields', t => {
const result = subject(fixtures.schemaWithoutCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithoutCustomValidators
const result = subject(definition, $contexts)

t.is(result.default.field1.validator, allowAny)
t.is(result.default.childEntity.validator, allowAny)
})

test('[Validators] Given schema definition with validators, when final schema is built, then it should not override the validator from definition', t => {
const result = subject(fixtures.schemaWithCustomValidators)
const { $contexts = {}, ...definition } = fixtures.schemaWithCustomValidators
const result = subject(definition, $contexts)

t.is(result.default.field1.validator, fixtures.schemaWithCustomValidators.field1.validator)
t.is(result.default.childEntity.validator, fixtures.schemaWithCustomValidators.childEntity.validator)
})

test('[Contexts] Given schema definition with a context using `$exclude`, when final schema is built, then it should have a context schema excluding the properties declared', t => {
const result = subject(fixtures.schemaWithContextExclude)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextExclude
const result = subject(definition, $contexts)

t.deepEqual(result.contextWithExclude, omit(result.default, ['field1']))
})

test('[Contexts] Given schema definition with a context using `$include`, when final schema is built, then it should have a context schema including only the properties declared', t => {
const result = subject(fixtures.schemaWithContextInclude)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextInclude
const result = subject(definition, $contexts)

t.deepEqual(result.contextWithInclude, pick(result.default, ['field1']))
})

test('[Contexts] Given schema definition with a context using both `$include` and `$exclue`, when final schema is built, then it should have a context schema including only the properties declared in `$include`', t => {
const result = subject(fixtures.schemaWithContextIncludeAndExclude)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextIncludeAndExclude
const result = subject(definition, $contexts)

t.deepEqual(result.contextWithIncludeAndExclude, pick(result.default, ['field1']))
})

test('[Contexts] Given schema definition with a context using `$modify`, when final schema is built, then it should have a context schema changing only the declared validators', t => {
const result = subject(fixtures.schemaWithContextModify)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextModify
const result = subject(definition, $contexts)

t.deepEqual(omit(result.contextWithModify, ['field1']), omit(result.default, ['field1']))
t.is(result.contextWithModify.field1.validator, forbidAny)
})

test('[Contexts] Given schema definition with a context using `$modify` with extra properties, when final schema is built, then it should not create new properties in the result', t => {
const result = subject(fixtures.schemaWithContextModifyUnspecifiedProp)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextModifyUnspecifiedProp
const result = subject(definition, $contexts)

t.is(result.contextWithModifyUnspecifiedProp.unespecifiedField1, undefined)
})

test('[Contexts] Given schema definition with a context using `$include` and `$modify`, when final schema is built, then it should modify only the properties in `$include`', t => {
const result = subject(fixtures.schemaWithContextIncludeAndModify)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextIncludeAndModify
const result = subject(definition, $contexts)

t.is(result.contextWithIncludeAndModify.field1.validator, forbidAny)
t.is(result.contextWithIncludeAndModify.field2, undefined)
})

test('[Contexts] Given schema definition with a context using `$exclude` and `$modify`, when final schema is built, then it should modify only the properties not in `$exclude`', t => {
const result = subject(fixtures.schemaWithContextExcludeAndModify)
const { $contexts = {}, ...definition } = fixtures.schemaWithContextExcludeAndModify
const result = subject(definition, $contexts)

t.is(result.contextWithExcludeAndModify.field1, undefined)
t.is(result.contextWithExcludeAndModify.field2.validator, forbidAny)
Expand Down
42 changes: 42 additions & 0 deletions src/collection-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export default factory => {
return data => {
const instances = [...data]
.map(factory)

return Object.create(null, {
toJSON: {
value: function toJSON (context = 'default') {
return instances.map(item => item.toJSON(context))
}
},
validate: {
value: function validate (context = 'default') {
const errors = instances.reduce((acc, item) => {
try {
item.validate(context)
return acc
} catch (e) {
return acc.concat(e)
}
}, [])

if (errors.length > 0) {
throw errors
}

return this
}
},
at: {
value: function at (n) {
return instances[n]
}
},
[Symbol.iterator]: {
value: function * iterator () {
yield * instances[Symbol.iterator]()
}
}
})
}
}
58 changes: 57 additions & 1 deletion src/collection-factory.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { test } from 'ava'
import factoryFor from './factory'
import { forbidAny } from './default-validators'
import subject from './collection-factory'

test('Given a single object factory and an array of data, when collection factory is called, then it should return an entity that is a collection of entities', t => {
test('Given a single object factory and an array of data, when collection factory is called with raw data, then it should return an entity that is a collection of entities', t => {
const singleSchema = {
prop1: {},
prop2: {}
Expand All @@ -24,3 +25,58 @@ test('Given a single object factory and an array of data, when collection factor

t.deepEqual(result.toJSON(), validData)
})

test('Given a single object factory and an array of data, when collection factory is called with an instance of the collection, then it should return an entity that is a collection of entities', t => {
const singleSchema = {
prop1: {},
prop2: {}
}

const singleFactory = factoryFor(singleSchema)

const collectionFactory = subject(singleFactory)

const validDataWithInstance = collectionFactory([{
prop1: 'a',
prop2: 'b'
}, {
prop1: 'c',
prop2: 'd'
}])

const result = collectionFactory(validDataWithInstance)

t.deepEqual(result.toJSON(), validDataWithInstance.toJSON())
})

test('Given a single object factory and an array of invalid data, when `validate()` factory is called, then it should throw an error for each one of the invalid entity', t => {
const singleSchema = {
prop1: {
validator: forbidAny
},
prop2: {
validator: forbidAny
}
}

const singleFactory = factoryFor(singleSchema)

const collectionFactory = subject(singleFactory)

const validData = [{
prop1: 'a',
prop2: 'b'
}, {
prop1: 'c',
prop2: 'd'
}]

const result = collectionFactory(validData)

const errors = t.throws(() => result.validate())
t.true(errors.length === 2)
t.truthy(errors[0].details.prop1)
t.truthy(errors[0].details.prop2)
t.truthy(errors[1].details.prop1)
t.truthy(errors[1].details.prop2)
})
4 changes: 4 additions & 0 deletions src/default-validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export function delegate (entity) {
entity.validate()
return { data: entity }
} catch (e) {
if (Array.isArray(e)) {
return { error: e.map(e => e.details) }
}

return { error: e.details }
}
}
63 changes: 38 additions & 25 deletions src/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,40 @@ import validate from './validate'
import toJSON from './to-json'
import buildSchema from './build-schema'

const buildDescriptors = (schema, factory) => ({
$schema: {
value: schema
},
$factory: {
value: factory
},
toJSON: {
value: function (context = 'default') {
checkContext(this.$schema, context)

return toJSON(this.$schema[context], this)
}
},
validate: {
value: function (context = 'default') {
checkContext(this.$schema, context)
const buildDescriptors = (schema, factory, methods) => {
const methodDescriptors = Object.entries(methods)
.reduce(
(acc, [ methodName, method ]) => ({
...acc,
[methodName]: { value: method }
}),
{}
)

return {
...methodDescriptors,
$schema: {
value: schema
},
$factory: {
value: factory
},
toJSON: {
value: function (context = 'default') {
checkContext(this.$schema, context)

return toJSON(this.$schema[context], this)
}
},
validate: {
value: function (context = 'default') {
checkContext(this.$schema, context)

return validate(this.$schema[context], this)
return validate(this.$schema[context], this)
}
}
}
})
}

function checkContext (schema, context) {
if (schema[context] === undefined) {
Expand All @@ -33,20 +45,21 @@ function checkContext (schema, context) {
}

export default schemaDefinition => {
const schemaKeys = Object.keys(schemaDefinition)
const { $methods = {}, $contexts = {}, ...definition } = schemaDefinition

const schema = buildSchema(schemaDefinition)
const schema = buildSchema(definition, $contexts)

const schemaKeys = Object.keys(schemaDefinition)
const factory = data => {
const allowedData = pick(data, schemaKeys)

return Object.assign(
Object.create(null, buildDescriptors(schema, factory)),
Object.keys(allowedData).reduce(
(acc, currentKey) =>
Object.create(null, buildDescriptors(schema, factory, $methods)),
Object.entries(allowedData).reduce(
(acc, [ key, value ]) =>
Object.assign(
acc,
{ [currentKey]: (schemaDefinition[currentKey].factory || identity)(allowedData[currentKey]) }
{ [key]: (schemaDefinition[key].factory || identity)(value) }
),
{}
)
Expand Down
Loading

0 comments on commit d1d72c2

Please sign in to comment.