diff --git a/features/model.feature b/features/model.feature new file mode 100644 index 0000000..0ee8d52 --- /dev/null +++ b/features/model.feature @@ -0,0 +1,7 @@ +Feature: Models + + Scenario: A Model With a 4 fields + Given TestModel1 is used + When TestModel1b data is inserted + Then TestModel1b expected fields are found + diff --git a/features/stepDefinitions/steps.js b/features/stepDefinitions/steps.js index 7c3ceab..bed1cb9 100644 --- a/features/stepDefinitions/steps.js +++ b/features/stepDefinitions/steps.js @@ -2,15 +2,14 @@ const assert = require('chai').assert const flatMap = require('lodash/flatMap') const { Given, When, Then } = require('@cucumber/cucumber') -const { smartObject, property, named, typed } = require('../../index') +const { createModel, field } = require('../../index') const MODEL_DEFINITIONS = { - TestModel1: ({ name, type, flag }) => - smartObject([ - named({ required: true })(name), - typed({ required: true, isString: 'true' })(type), - property('flag', { required: true, isNumber: true })(flag), - ]), + TestModel1: createModel({ + name: field({ required: true }), + type: field({ required: true, isString: true }), + flag: field({ required: true, isNumber: true }), + }), } const MODEL_INPUT_VALUES = { @@ -26,6 +25,10 @@ const MODEL_INPUT_VALUES = { }, } +const EXPECTED_FIELDS = { + TestModel1b: ['getName', 'getType', 'getFlag', 'meta', 'functions'], +} + Given( 'the {word} has been created, with {word} inputs provided', function (modelDefinition, modelInputValues) { @@ -42,7 +45,7 @@ Given( ) When('functions.validate is called', function () { - return this.instance.functions.validate.object().then(x => { + return this.instance.functions.validate.model().then(x => { this.errors = x }) }) @@ -54,3 +57,31 @@ Then('an array of {int} errors is shown', function (errorCount) { } assert.equal(errors.length, errorCount) }) + +Given('{word} is used', function (modelDefinition) { + const def = MODEL_DEFINITIONS[modelDefinition] + if (!def) { + throw new Error(`${modelDefinition} did not result in a definition`) + } + this.modelDefinition = def +}) + +When('{word} data is inserted', function (modelInputValues) { + const input = MODEL_INPUT_VALUES[modelInputValues] + if (!input) { + throw new Error(`${modelInputValues} did not result in an input`) + } + this.instance = this.modelDefinition(input) +}) + +Then('{word} expected fields are found', function (fields) { + const propertyArray = EXPECTED_FIELDS[fields] + if (!propertyArray) { + throw new Error(`${fields} did not result in fields`) + } + propertyArray.forEach(key => { + if (!(key in this.instance)) { + throw new Error(`Did not find ${key} in model`) + } + }) +}) diff --git a/package.json b/package.json index 36e0516..434b9c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "functional-models", - "version": "1.0.2", + "version": "1.0.5", "description": "A library for creating JavaScript function based models.", "main": "index.js", "scripts": { diff --git a/src/dates.js b/src/dates.js deleted file mode 100644 index 4a09cdd..0000000 --- a/src/dates.js +++ /dev/null @@ -1,13 +0,0 @@ -const { property } = require('./properties') - -const _autonowDate = ({ key }) => (date = null, ...args) => { - const theDate = date ? date : new Date() - return property(key, ...args)(theDate) -} -const lastModifiedProperty = _autonowDate({ key: 'lastModified' }) -const lastUpdatedProperty = _autonowDate({ key: 'lastUpdated' }) - -module.exports = { - lastModifiedProperty, - lastUpdatedProperty, -} diff --git a/src/fields.js b/src/fields.js new file mode 100644 index 0000000..2e7b59a --- /dev/null +++ b/src/fields.js @@ -0,0 +1,106 @@ +const identity = require('lodash/identity') +const { createFieldValidator } = require('./validation') +const { createUuid } = require('./utils') +const { lazyValue } = require('./lazy') + +const field = (config = {}) => { + const value = config.value || undefined + const defaultValue = config.defaultValue || undefined + const lazyLoadMethod = config.lazyLoadMethod || false + const valueSelector = config.valueSelector || identity + if (typeof valueSelector !== 'function') { + throw new Error(`valueSelector must be a function`) + } + + return { + createGetter: instanceValue => { + if (value !== undefined) { + return () => value + } + if ( + defaultValue !== undefined && + (instanceValue === null || instanceValue === undefined) + ) { + return () => defaultValue + } + const method = lazyLoadMethod + ? lazyValue(lazyLoadMethod) + : typeof instanceValue === 'function' + ? instanceValue + : () => instanceValue + return async () => { + return valueSelector(await method(instanceValue)) + } + }, + getValidator: valueGetter => { + return async () => { + return createFieldValidator(config)(await valueGetter()) + } + }, + } +} + +const uniqueId = config => + field({ + ...config, + lazyLoadMethod: value => { + if (!value) { + return createUuid() + } + return value + }, + }) + +const dateField = config => + field({ + ...config, + lazyLoadMethod: value => { + if (!value && config.autoNow) { + return new Date() + } + return value + }, + }) + +const referenceField = config => { + return field({ + ...config, + lazyLoadMethod: async smartObj => { + const _getId = () => { + if (!smartObj) { + return null + } + return smartObj && smartObj.id + ? smartObj.id + : smartObj.getId + ? smartObj.getId() + : smartObj + } + const _getSmartObjReturn = objToUse => { + return { + ...objToUse, + functions: { + ...(objToUse.functions ? objToUse.functions : {}), + toJson: _getId, + }, + } + } + const valueIsSmartObj = smartObj && smartObj.functions + if (valueIsSmartObj) { + return _getSmartObjReturn(smartObj) + } + if (config.fetcher) { + const obj = await config.fetcher(smartObj) + return _getSmartObjReturn(obj) + } + return _getId(smartObj) + }, + }) +} + +module.exports = { + field, + uniqueId, + dateField, + referenceField, +} diff --git a/src/index.js b/src/index.js index 8cf436c..9041215 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,6 @@ module.exports = { - ...require('./dates'), - ...require('./lazy'), - ...require('./objects'), - ...require('./properties'), - ...require('./references'), + ...require('./fields'), + ...require('./models'), + validation: require('./validation'), serialization: require('./serialization'), } diff --git a/src/lazy.js b/src/lazy.js index 63835fa..5addd11 100644 --- a/src/lazy.js +++ b/src/lazy.js @@ -1,16 +1,19 @@ -const { lazyValue, createPropertyTitle } = require('./utils') +const lazyValue = method => { + /* eslint-disable functional/no-let */ + let value = undefined + let called = false + return async (...args) => { + if (!called) { + value = await method(...args) + // eslint-disable-next-line require-atomic-updates + called = true + } -const lazyProperty = (key, method, { selector = null } = {}) => { - const lazy = lazyValue(method) - const propertyKey = createPropertyTitle(key) - return { - [propertyKey]: async () => { - const value = await lazy() - return selector ? selector(value) : value - }, + return value } + /* eslint-enable functional/no-let */ } module.exports = { - lazyProperty, + lazyValue, } diff --git a/src/models.js b/src/models.js new file mode 100644 index 0000000..a9e70e4 --- /dev/null +++ b/src/models.js @@ -0,0 +1,55 @@ +const merge = require('lodash/merge') +const get = require('lodash/get') +const { toJson } = require('./serialization') +const { createPropertyTitle } = require('./utils') +const { createModelValidator } = require('./validation') + +const SYSTEM_KEYS = ['meta', 'functions'] + +const PROTECTED_KEYS = ['model'] + +const createModel = keyToField => { + PROTECTED_KEYS.forEach(key => { + if (key in keyToField) { + throw new Error(`Cannot use ${key}. This is a protected value.`) + } + }) + const systemProperties = SYSTEM_KEYS.reduce((acc, key) => { + const value = get(keyToField, key, {}) + return { ...acc, [key]: value } + }, {}) + const nonSystemProperties = Object.entries(keyToField).filter( + ([key, _]) => !(key in SYSTEM_KEYS) + ) + + return instanceValues => { + const loadedInternals = nonSystemProperties.reduce((acc, [key, field]) => { + const fieldGetter = field.createGetter(instanceValues[key]) + const fieldValidator = field.getValidator(fieldGetter) + const getFieldKey = createPropertyTitle(key) + const fleshedOutField = { + [getFieldKey]: fieldGetter, + functions: { + validate: { + [key]: fieldValidator, + }, + }, + } + return merge(acc, fleshedOutField) + }, {}) + const allUserData = merge(systemProperties, loadedInternals) + const internalFunctions = { + functions: { + toJson: toJson(loadedInternals), + validate: { + model: createModelValidator(loadedInternals), + }, + }, + } + return merge(allUserData, internalFunctions) + } +} + +module.exports = { + createModel, +} diff --git a/src/objects.js b/src/objects.js deleted file mode 100644 index d54fc49..0000000 --- a/src/objects.js +++ /dev/null @@ -1,49 +0,0 @@ -const merge = require('lodash/merge') -const get = require('lodash/get') -const { toJson } = require('./serialization') - -const findValidateFunctions = smartObject => { - return Object.entries(get(smartObject, 'functions.validate', {})) -} - -const smartObject = ( - internals, - { metaProperties = {}, functions = {} } = {} -) => { - const realInternals = Array.isArray(internals) - ? internals.reduce((acc, obj) => merge(acc, obj), {}) - : internals - - const passedInData = merge( - metaProperties ? { meta: { ...metaProperties } } : {}, - realInternals, - { functions } - ) - const internalFunctions = { - functions: { - ...functions, - toJson: toJson(realInternals), - validate: { - object: async () => { - const keysAndfunctions = findValidateFunctions(realInternals) - const data = await Promise.all( - keysAndfunctions.map(async ([key, validator]) => { - return [key, await validator()] - }) - ) - return data - .filter(([_, errors]) => Boolean(errors) && errors.length > 0) - .reduce((acc, [key, errors]) => { - return { ...acc, [key]: errors } - }, {}) - }, - }, - }, - } - return merge(passedInData, internalFunctions) -} - -module.exports = { - smartObject, - findValidateFunctions, -} diff --git a/src/properties.js b/src/properties.js deleted file mode 100644 index 0123287..0000000 --- a/src/properties.js +++ /dev/null @@ -1,23 +0,0 @@ -const { createPropertyTitle, createUuid } = require('./utils') -const { createPropertyValidate } = require('./validation') - -const property = (key, config = {}) => arg => { - const method = typeof arg === 'function' ? arg : () => arg - const propertyKey = createPropertyTitle(key) - return { - [propertyKey]: method, - ...createPropertyValidate(key, config)(arg), - } -} - -const named = config => property('Name', config) -const typed = config => property('Type', config) -const uniqueId = config => (id = null) => - property('id', config)(id || createUuid(), config) - -module.exports = { - property, - named, - typed, - uniqueId, -} diff --git a/src/references.js b/src/references.js deleted file mode 100644 index c438291..0000000 --- a/src/references.js +++ /dev/null @@ -1,38 +0,0 @@ -const { lazyProperty } = require('./lazy') - -const smartObjectReference = ({ fetcher = null }) => (key, smartObj) => { - return lazyProperty(key, async () => { - const _getId = () => { - if (!smartObj) { - return null - } - return smartObj && smartObj.id - ? smartObj.id - : smartObj.getId - ? smartObj.getId() - : smartObj - } - const _getSmartObjReturn = objToUse => { - return { - ...objToUse, - functions: { - ...(objToUse.functions ? objToUse.functions : {}), - toJson: _getId, - }, - } - } - const valueIsSmartObj = smartObj && smartObj.functions - if (valueIsSmartObj) { - return _getSmartObjReturn(smartObj) - } - if (fetcher) { - const obj = await fetcher(smartObj) - return _getSmartObjReturn(obj) - } - return _getId(smartObj) - }) -} - -module.exports = { - smartObjectReference, -} diff --git a/src/utils.js b/src/utils.js index 1f67b3e..25d90a7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,22 +11,6 @@ const createPropertyTitle = key => { return `get${goodName}` } -const lazyValue = method => { - /* eslint-disable functional/no-let */ - let value = undefined - let called = false - return async () => { - if (!called) { - value = await method() - // eslint-disable-next-line require-atomic-updates - called = true - } - - return value - } - /* eslint-enable functional/no-let */ -} - const getCryptoRandomValues = () => { if (typeof window !== 'undefined') { return (window.crypto || window.msCrypto).getRandomValues @@ -53,7 +37,6 @@ const loweredTitleCase = string => { module.exports = { createUuid, loweredTitleCase, - lazyValue, createPropertyTitle, toTitleCase, } diff --git a/src/validation.js b/src/validation.js index a601c3b..a459c34 100644 --- a/src/validation.js +++ b/src/validation.js @@ -1,5 +1,6 @@ const isEmpty = require('lodash/isEmpty') const flatMap = require('lodash/flatMap') +const get = require('lodash/get') const _trueOrError = (method, error) => value => { if (method(value) === false) { @@ -30,14 +31,12 @@ const isInteger = _trueOrError(v => { const isBoolean = isType('boolean') const isString = isType('string') -const meetsRegex = ( - regex, - flags, - errorMessage = 'Format was invalid' -) => value => { - const reg = new RegExp(regex, flags) - return _trueOrError(v => reg.test(v), errorMessage)(value) -} +const meetsRegex = + (regex, flags, errorMessage = 'Format was invalid') => + value => { + const reg = new RegExp(regex, flags) + return _trueOrError(v => reg.test(v), errorMessage)(value) + } const choices = choiceArray => value => { if (choiceArray.includes(value) === false) { @@ -125,7 +124,7 @@ const CONFIG_TO_VALIDATE_METHOD = { isString: _boolChoice(isString), } -const createPropertyValidate = (key, config) => value => { +const createFieldValidator = config => { const validators = [ ...Object.entries(config).map(([key, value]) => { return (CONFIG_TO_VALIDATE_METHOD[key] || (() => undefined))(value) @@ -134,18 +133,26 @@ const createPropertyValidate = (key, config) => value => { ].filter(x => x) const validator = validators.length > 0 ? aggregateValidator(validators) : emptyValidator - return { - functions: { - validate: { - [key]: async () => { - const errors = await validator(value) - return flatMap(errors) - }, - }, - }, + return async value => { + const errors = await validator(value) + return flatMap(errors) } } +const createModelValidator = fields => async () => { + const keysAndFunctions = Object.entries(get(fields, 'functions.validate', {})) + const data = await Promise.all( + keysAndFunctions.map(async ([key, validator]) => { + return [key, await validator()] + }) + ) + return data + .filter(([_, errors]) => Boolean(errors) && errors.length > 0) + .reduce((acc, [key, errors]) => { + return { ...acc, [key]: errors } + }, {}) +} + module.exports = { isNumber, isBoolean, @@ -161,5 +168,6 @@ module.exports = { meetsRegex, aggregateValidator, emptyValidator, - createPropertyValidate, + createFieldValidator, + createModelValidator, } diff --git a/test/src/dates.test.js b/test/src/dates.test.js deleted file mode 100644 index 8840492..0000000 --- a/test/src/dates.test.js +++ /dev/null @@ -1,33 +0,0 @@ -const assert = require('chai').assert -const { lastModifiedProperty, lastUpdatedProperty } = require('../../src/dates') - -describe('/src/dates.js', () => { - describe('#lastModifiedProperty()', () => { - it('should return the "2021-09-16T21:51:56.039Z" when getLastModified() is called with this date', async () => { - const date = new Date('2021-09-16T21:51:56.039Z') - const property = lastModifiedProperty(date) - const actual = await property.getLastModified().toISOString() - const expected = '2021-09-16T21:51:56.039Z' - assert.deepEqual(actual, expected) - }) - it('should return a date for getLastModified() even if none is passed in', async () => { - const property = lastModifiedProperty() - const actual = await property.getLastModified().toISOString() - assert.isOk(actual) - }) - }) - describe('#lastUpdatedProperty()', () => { - it('should return the "2021-09-16T21:51:56.039Z" when getLastUpdated() is called with this date', async () => { - const date = new Date('2021-09-16T21:51:56.039Z') - const property = lastUpdatedProperty(date) - const actual = await property.getLastUpdated().toISOString() - const expected = '2021-09-16T21:51:56.039Z' - assert.deepEqual(actual, expected) - }) - it('should return a date for getLastUpdated() even if none is passed in', async () => { - const property = lastUpdatedProperty() - const actual = await property.getLastUpdated().toISOString() - assert.isOk(actual) - }) - }) -}) diff --git a/test/src/fields.test.js b/test/src/fields.test.js new file mode 100644 index 0000000..9a173e7 --- /dev/null +++ b/test/src/fields.test.js @@ -0,0 +1,160 @@ +const assert = require('chai').assert +const { + uniqueId, + field, + dateField, + referenceField, +} = require('../../src/fields') +const { createModel } = require('../../src/models') + +describe('/src/fields.js', () => { + describe('#field()', () => { + it('should throw an exception if config.valueSelector is not a function but is set', () => { + assert.throws(() => { + field({ valueSelector: 'blah' }) + }) + }) + it('should not throw an exception if config.valueSelector is a function', () => { + assert.doesNotThrow(() => { + field({ valueSelector: () => ({}) }) + }) + }) + describe('#createGetter()', () => { + it('should return a function even if config.value is set to a value', () => { + const instance = field({ value: 'my-value' }) + const actual = instance.createGetter('not-my-value') + assert.isFunction(actual) + }) + it('should return the value passed into config.value regardless of what is passed into the createGetter', async () => { + const instance = field({ value: 'my-value' }) + const actual = await instance.createGetter('not-my-value')() + const expected = 'my-value' + assert.deepEqual(actual, expected) + }) + it('should return the value passed into createGetter when config.value is not set', async () => { + const instance = field() + const actual = await instance.createGetter('my-value')() + const expected = 'my-value' + assert.deepEqual(actual, expected) + }) + it('should return the value of the function passed into createGetter when config.value is not set', async () => { + const instance = field() + const actual = await instance.createGetter(() => 'my-value')() + const expected = 'my-value' + assert.deepEqual(actual, expected) + }) + }) + }) + describe('#uniqueId()', () => { + describe('#createGetter()', () => { + it('should call createUuid only once even if called twice', async () => { + const uniqueField = uniqueId({}) + const getter = uniqueField.createGetter() + const first = await getter() + const second = await getter() + assert.deepEqual(first, second) + }) + it('should use the uuid passed in', async () => { + const uniqueField = uniqueId({}) + const getter = uniqueField.createGetter('my-uuid') + const actual = await getter() + const expected = 'my-uuid' + assert.deepEqual(actual, expected) + }) + }) + }) + describe('#dateField()', () => { + it('should create a new date once when config.autoNow=true and called multiple times', async () => { + const proto = dateField({ autoNow: true }) + const instance = proto.createGetter() + const first = await instance() + const second = await instance() + const third = await instance() + assert.deepEqual(first, second) + assert.deepEqual(first, third) + }) + it('should use the date passed in', async () => { + const proto = dateField({ autoNow: true }) + const date = new Date() + const instance = proto.createGetter(date) + const actual = await instance() + const expected = date + assert.deepEqual(actual, expected) + }) + }) + + describe('#referenceField()', () => { + describe('#createGetter()', () => { + it('should return "obj-id" when no fetcher is used', async () => { + const input = ['obj-id'] + const actual = await referenceField({}).createGetter(...input)() + const expected = 'obj-id' + assert.equal(actual, expected) + }) + it('should allow null as the input', async () => { + const input = [null] + const actual = await referenceField({}).createGetter(...input)() + const expected = null + assert.equal(actual, expected) + }) + it('should return "obj-id" from {}.id when no fetcher is used', async () => { + const input = [{ id: 'obj-id' }] + const actual = await referenceField({}).createGetter(...input)() + const expected = 'obj-id' + assert.equal(actual, expected) + }) + it('should return prop: "switch-a-roo" when switch-a-roo fetcher is used', async () => { + const input = ['obj-id'] + const actual = await referenceField({ + fetcher: () => ({ id: 'obj-id', prop: 'switch-a-roo' }), + }).createGetter(...input)() + const expected = 'switch-a-roo' + assert.deepEqual(actual.prop, expected) + }) + it('should combine functions when switch-a-roo fetcher is used', async () => { + const input = ['obj-id'] + const instance = await referenceField({ + fetcher: () => ({ + id: 'obj-id', + prop: 'switch-a-roo', + functions: { myfunc: 'ok' }, + }), + }).createGetter(...input)() + const actual = instance.functions.myfunc + const expected = 'ok' + assert.deepEqual(actual, expected) + }) + it('should take the smartObject as a value', async () => { + const proto = createModel({ + id: uniqueId({ value: 'obj-id' }), + }) + const input = [proto({ id: 'obj-id' })] + const instance = await referenceField({}).createGetter(...input)() + const actual = await instance.getId() + const expected = 'obj-id' + assert.deepEqual(actual, expected) + }) + describe('#functions.toJson()', () => { + it('should use the getId of the smartObject passed in when toJson is called', async () => { + const proto = createModel({ + id: uniqueId({ value: 'obj-id' }), + }) + const input = [proto({ id: 'obj-id' })] + const instance = await referenceField({}).createGetter(...input)() + const actual = await instance.functions.toJson() + const expected = 'obj-id' + assert.deepEqual(actual, expected) + }) + it('should return "obj-id" when switch-a-roo fetcher is used and toJson is called', async () => { + const input = ['obj-id'] + const instance = await referenceField({ + fetcher: () => ({ id: 'obj-id', prop: 'switch-a-roo' }), + }).createGetter(...input)() + const actual = await instance.functions.toJson() + const expected = 'obj-id' + assert.deepEqual(actual, expected) + }) + }) + }) + }) +}) diff --git a/test/src/lazy.test.js b/test/src/lazy.test.js index 67b68e9..84a7c43 100644 --- a/test/src/lazy.test.js +++ b/test/src/lazy.test.js @@ -1,25 +1,15 @@ const assert = require('chai').assert -const { lazyProperty } = require('../../src/lazy') +const sinon = require('sinon') +const { lazyValue } = require('../../src/lazy') describe('/src/lazy.js', () => { - describe('#lazyProperty()', () => { - it('should call the selector that is passed in', async () => { - const inputs = [ - 'lazy', - () => 'hello world', - { selector: value => value.slice(6) }, - ] - const obj = lazyProperty(...inputs) - const actual = await obj.getLazy() - const expected = 'world' - assert.deepEqual(actual, expected) - }) - it('should return the lazy value that is passed in', async () => { - const inputs = ['lazy', () => 'hello world'] - const obj = lazyProperty(...inputs) - const actual = await obj.getLazy() - const expected = 'hello world' - assert.deepEqual(actual, expected) + describe('#lazyValue()', () => { + it('should only call the method passed in once even after two calls', async () => { + const method = sinon.stub().returns('hello-world') + const instance = lazyValue(method) + await instance() + await instance() + sinon.assert.calledOnce(method) }) }) }) diff --git a/test/src/models.test.js b/test/src/models.test.js new file mode 100644 index 0000000..e2ef08d --- /dev/null +++ b/test/src/models.test.js @@ -0,0 +1,97 @@ +const assert = require('chai').assert +const { createModel } = require('../../src/models') +const { field } = require('../../src/fields') + +describe('/src/models.js', () => { + describe('#createModel()', () => { + it('should return a function when called once with valid data', () => { + const actual = createModel({}) + const expected = 'function' + assert.isFunction(actual) + }) + describe('#()', () => { + it('should use the value passed in when field.defaultValue and field.value are not set', async () => { + const input = { + myField: field({ required: true }), + } + const model = createModel(input) + const instance = model({ myField: 'passed-in' }) + const actual = await instance.getMyField() + const expected = 'passed-in' + assert.deepEqual(actual, expected) + }) + it('should use the value for field.value when even if field.defaultValue is set and a value is passed in', async () => { + const input = { + myField: field({ value: 'value', defaultValue: 'default-value' }), + } + const model = createModel(input) + const instance = model({ myField: 'passed-in' }) + const actual = await instance.getMyField() + const expected = 'value' + assert.deepEqual(actual, expected) + }) + it('should use the value for field.value when even if field.defaultValue is not set and a value is passed in', async () => { + const input = { + myField: field({ value: 'value' }), + } + const model = createModel(input) + const instance = model({ myField: 'passed-in' }) + const actual = await instance.getMyField() + const expected = 'value' + assert.deepEqual(actual, expected) + }) + it('should use the value for field.defaultValue when field.value is not set and no value is passed in', async () => { + const input = { + myField: field({ defaultValue: 'defaultValue' }), + } + const model = createModel(input) + const instance = model({}) + const actual = await instance.getMyField() + const expected = 'defaultValue' + assert.deepEqual(actual, expected) + }) + it('should use the value for field.defaultValue when field.value is not set and null is passed as a value', async () => { + const input = { + myField: field({ defaultValue: 'defaultValue' }), + } + const model = createModel(input) + const instance = model({ myField: null }) + const actual = await instance.getMyField() + const expected = 'defaultValue' + assert.deepEqual(actual, expected) + }) + it('should return a model with getId and getType for the provided valid keyToField', () => { + const input = { + id: field({ required: true }), + type: field(), + } + const model = createModel(input) + const actual = model({ id: 'my-id', type: 'my-type' }) + console.log(actual) + assert.isOk(actual.getId) + assert.isOk(actual.getType) + }) + it('should return a model where validate returns one error for id', async () => { + const input = { + id: field({ required: true }), + type: field(), + } + const model = createModel(input) + const instance = model({ type: 'my-type' }) + const actual = await instance.functions.validate.model() + const expected = 1 + console.log(actual) + assert.equal(Object.values(actual).length, expected) + }) + }) + it('should return a function when called once with valid data', () => { + const actual = createModel({}) + assert.isFunction(actual) + }) + it('should throw an exception if a key "model" is passed in', () => { + assert.throws(() => { + createModel({ model: 'weeee' }) + }) + }) + }) +}) diff --git a/test/src/objects.test.js b/test/src/objects.test.js deleted file mode 100644 index 93115df..0000000 --- a/test/src/objects.test.js +++ /dev/null @@ -1,70 +0,0 @@ -const assert = require('chai').assert -const { smartObject } = require('../../src/objects') - -describe('/src/objects.js', () => { - describe('#smartObject()', () => { - it('should use the functions.validation of two objects correctly', async () => { - const instance = smartObject([ - { functions: { validate: { property1: () => ['failed1'] } } }, - { functions: { validate: { property2: () => ['failed2'] } } }, - ]) - const actual = await instance.functions.validate.object() - const expected = { - property1: ['failed1'], - property2: ['failed2'], - } - assert.deepEqual(actual, expected) - }) - it('should combine functions.validate of two objects correctly', () => { - const instance = smartObject([ - { functions: { validate: { property1: () => {} } } }, - { functions: { validate: { property2: () => {} } } }, - ]) - assert.isOk(instance.functions.validate.property1) - assert.isOk(instance.functions.validate.property2) - }) - it('should allow a single value for internals', async () => { - const instance = smartObject({ - key: 'value', - }) - const actual = await instance.key - const expected = 'value' - assert.deepEqual(actual, expected) - }) - it('should merge metaProperties', () => { - const instance = smartObject( - { - key: 'value', - }, - { metaProperties: { test: 'me' } } - ) - const actual = instance.meta.test - const expected = 'me' - assert.deepEqual(actual, expected) - }) - it('should allow a null metaProperties passed in', () => { - const instance = smartObject( - { - key: 'value', - }, - { metaProperties: null } - ) - const actual = instance.meta - const expected = undefined - assert.deepEqual(actual, expected) - }) - it('should have a "functions" property', () => { - const actual = smartObject([{ key: 'value' }, { key2: 'value2' }]) - .functions - assert.isOk(actual) - }) - it('should combine an array of objects', () => { - const actual = smartObject([{ key: 'value' }, { key2: 'value2' }]) - const expected = { - key: 'value', - key2: 'value2', - } - assert.include(actual, expected) - }) - }) -}) diff --git a/test/src/properties.test.js b/test/src/properties.test.js deleted file mode 100644 index 3e8faf9..0000000 --- a/test/src/properties.test.js +++ /dev/null @@ -1,52 +0,0 @@ -const assert = require('chai').assert -const { property, named, typed, uniqueId } = require('../../src/properties') - -describe('/src/properties.js', () => { - describe('#uniqueId()', () => { - describe('#getId()', () => { - it('should use the id passed in', () => { - const actual = uniqueId()('passed-in').getId() - const expected = 'passed-in' - assert.equal(actual, expected) - }) - it('should create an id if null is passed in', () => { - const actual = uniqueId()().getId() - assert.isOk(actual) - }) - }) - }) - describe('#property()', () => { - it('should take an arg that is a function', async () => { - const instance = property('property')(() => 'hello world') - const actual = await instance.getProperty() - }) - it('should return an object that has a key "getKey" that returns "value" when "Key" is passed in', () => { - const actual = property('Key')('value') - const [actualKey, actualValue] = Object.entries(actual)[0] - const expectedKey = 'getKey' - const expectedValue = 'value' - assert.equal(actualKey, expectedKey) - assert.equal(actualValue(), expectedValue) - }) - }) - describe('#named()', () => { - it('should return an object with a key "getName" that returns "name" when "name" is passed in', () => { - const actual = named()('name') - const [actualKey, actualValue] = Object.entries(actual)[0] - const expectedKey = 'getName' - const expectedValue = 'name' - assert.equal(actualKey, expectedKey) - assert.equal(actualValue(), expectedValue) - }) - }) - describe('#typed()', () => { - it('should return an object with a key "getType" that returns "theType" when "theType" is passed in', () => { - const actual = typed()('theType') - const [actualKey, actualValue] = Object.entries(actual)[0] - const expectedKey = 'getType' - const expectedValue = 'theType' - assert.equal(actualKey, expectedKey) - assert.equal(actualValue(), expectedValue) - }) - }) -}) diff --git a/test/src/references.test.js b/test/src/references.test.js deleted file mode 100644 index 0500fbb..0000000 --- a/test/src/references.test.js +++ /dev/null @@ -1,84 +0,0 @@ -const assert = require('chai').assert -const { smartObject } = require('../../src/objects') -const { uniqueId } = require('../../src/properties') -const { smartObjectReference } = require('../../src/references') - -describe('/src/references.js', () => { - describe('#smartObjectReference()', () => { - it('should return an object with getMyObject as a property for a key of "MyObject"', async () => { - const input = ['MyObject', 'obj-id'] - const instance = smartObjectReference({})(...input) - const actual = instance.getMyObject - assert.isOk(actual) - }) - it('should return "obj-id" when no fetcher is used', async () => { - const input = ['MyObject', 'obj-id'] - const instance = smartObjectReference({})(...input) - const actual = await instance.getMyObject() - const expected = 'obj-id' - assert.equal(actual, expected) - }) - it('should allow null as the input', async () => { - const input = ['MyObject', null] - const instance = smartObjectReference({})(...input) - const actual = await instance.getMyObject() - const expected = null - assert.equal(actual, expected) - }) - it('should return "obj-id" from {}.id when no fetcher is used', async () => { - const input = ['MyObject', { id: 'obj-id' }] - const instance = smartObjectReference({})(...input) - const actual = await instance.getMyObject() - const expected = 'obj-id' - assert.equal(actual, expected) - }) - it('should return prop: "switch-a-roo" when switch-a-roo fetcher is used', async () => { - const input = ['MyObject', 'obj-id'] - const instance = smartObjectReference({ - fetcher: () => ({ id: 'obj-id', prop: 'switch-a-roo' }), - })(...input) - const actual = await instance.getMyObject() - const expected = 'switch-a-roo' - assert.deepEqual(actual.prop, expected) - }) - it('should combine functions when switch-a-roo fetcher is used', async () => { - const input = ['MyObject', 'obj-id'] - const instance = smartObjectReference({ - fetcher: () => ({ - id: 'obj-id', - prop: 'switch-a-roo', - functions: { myfunc: 'ok' }, - }), - })(...input) - const actual = (await instance.getMyObject()).functions.myfunc - const expected = 'ok' - assert.deepEqual(actual, expected) - }) - it('should take the smartObject as a value', async () => { - const input = ['MyObject', smartObject([uniqueId()('obj-id')])] - const instance = smartObjectReference({})(...input) - const classThing = await instance.getMyObject() - const actual = await (await instance.getMyObject()).getId() - const expected = 'obj-id' - assert.deepEqual(actual, expected) - }) - describe('#functions.toJson()', () => { - it('should use the getId of the smartObject passed in when toJson is called', async () => { - const input = ['MyObject', smartObject([uniqueId()('obj-id')])] - const instance = smartObjectReference({})(...input) - const actual = await (await instance.getMyObject()).functions.toJson() - const expected = 'obj-id' - assert.deepEqual(actual, expected) - }) - it('should return "obj-id" when switch-a-roo fetcher is used and toJson is called', async () => { - const input = ['MyObject', 'obj-id'] - const instance = smartObjectReference({ - fetcher: () => ({ id: 'obj-id', prop: 'switch-a-roo' }), - })(...input) - const actual = await (await instance.getMyObject()).functions.toJson() - const expected = 'obj-id' - assert.deepEqual(actual, expected) - }) - }) - }) -}) diff --git a/test/src/utils.test.js b/test/src/utils.test.js index 2a0c597..d2f52a5 100644 --- a/test/src/utils.test.js +++ b/test/src/utils.test.js @@ -1,18 +1,9 @@ const assert = require('chai').assert const sinon = require('sinon') const proxyquire = require('proxyquire') -const { loweredTitleCase, createUuid, lazyValue } = require('../../src/utils') +const { loweredTitleCase, createUuid } = require('../../src/utils') describe('/src/utils.js', () => { - describe('#lazyValue()', () => { - it('should only call the method passed in once even after two calls', async () => { - const method = sinon.stub().returns('hello-world') - const instance = lazyValue(method) - await instance() - await instance() - sinon.assert.calledOnce(method) - }) - }) describe('#loweredTitleCase()', () => { it('should turn TitleCase into titleCase', () => { const actual = loweredTitleCase('TitleCase') diff --git a/test/src/validation.test.js b/test/src/validation.test.js index 1f018c1..7554cca 100644 --- a/test/src/validation.test.js +++ b/test/src/validation.test.js @@ -1,4 +1,5 @@ const assert = require('chai').assert +const sinon = require('sinon') const { isNumber, isBoolean, @@ -13,7 +14,8 @@ const { meetsRegex, aggregateValidator, emptyValidator, - createPropertyValidate, + createModelValidator, + createFieldValidator, } = require('../../src/validation') describe('/src/validation.js', () => { @@ -201,36 +203,6 @@ describe('/src/validation.js', () => { assert.equal(actual, expected) }) }) - describe('#createPropertyValidate()', () => { - it('should result in {}.functions.validate.myProperty when key="myProperty"', () => { - const actual = createPropertyValidate('myProperty', {})('value') - assert.isOk(actual.functions.validate.myProperty) - }) - it('should create a isRequired validator when config contains isRequired=true', async () => { - const property = createPropertyValidate('myProperty', { required: true })( - null - ) - const actual = (await property.functions.validate.myProperty()).length - const expected = 1 - assert.equal(actual, expected) - }) - it('should not use isRequired validator when config contains isRequired=false', async () => { - const property = createPropertyValidate('myProperty', { - required: false, - })(null) - const actual = (await property.functions.validate.myProperty()).length - const expected = 0 - assert.equal(actual, expected) - }) - it('should use the validators passed in', async () => { - const property = createPropertyValidate('myProperty', { - validators: [maxTextLength(5)], - })('hello world') - const actual = (await property.functions.validate.myProperty()).length - const expected = 1 - assert.equal(actual, expected) - }) - }) describe('#emptyValidator()', () => { it('should return an empty array with a value of 1', () => { const actual = emptyValidator(1).length @@ -268,4 +240,79 @@ describe('/src/validation.js', () => { assert.isUndefined(actual) }) }) + describe('#createModelValidator()', () => { + it('should use both functions.validate for two objects', async () => { + const fields = { + functions: { + validate: { + id: sinon.stub().returns(undefined), + type: sinon.stub().returns(undefined), + }, + }, + } + const validator = createModelValidator(fields) + await validator() + sinon.assert.calledOnce(fields.functions.validate.id) + sinon.assert.calledOnce(fields.functions.validate.type) + }) + it('should combine results for both functions.validate for two objects that error', async () => { + const fields = { + functions: { + validate: { + id: sinon.stub().returns('error1'), + type: sinon.stub().returns('error2'), + }, + }, + } + const validator = createModelValidator(fields) + const actual = await validator() + const expected = { + id: 'error1', + type: 'error2', + } + assert.deepEqual(actual, expected) + }) + it('should take the error of the one of two functions', async () => { + const fields = { + functions: { + validate: { + id: sinon.stub().returns(undefined), + type: sinon.stub().returns('error2'), + }, + }, + } + const validator = createModelValidator(fields) + const actual = await validator() + const expected = { + type: 'error2', + } + assert.deepEqual(actual, expected) + }) + }) + describe('#createFieldValidator()', () => { + it('should not include isRequired if required=false, returning []', async () => { + const validator = createFieldValidator({ required: false }) + const actual = await validator(null) + const expected = [] + assert.deepEqual(actual, expected) + }) + it('should return [] if no configs are provided', async () => { + const validator = createFieldValidator({}) + const actual = await validator(null) + const expected = [] + assert.deepEqual(actual, expected) + }) + it('should use isRequired if required=false, returning one error', async () => { + const validator = createFieldValidator({ required: true }) + const actual = await validator(null) + const expected = 1 + assert.equal(actual.length, expected) + }) + it('should use validators.isRequired returning one error', async () => { + const validator = createFieldValidator({ validators: [isRequired] }) + const actual = await validator(null) + const expected = 1 + assert.equal(actual.length, expected) + }) + }) })