Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions features/arrayFields.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Feature: Array Fields

Scenario: A model that has an array field is created holding an array of integers and is checked and validated
Given ArrayModel1 model is used
When ArrayModelData1 data is inserted
Then the getArrayField field is called on the model
Then the array values match
| array | [1,2,3,4,5]|
Then functions.validate is called
Then an array of 0 errors is shown

Scenario: A model that has an array field but has a non-array inserted into it fails validation
Given ArrayModel1 model is used
When ArrayModelData2 data is inserted
Then the getArrayField field is called on the model
Then functions.validate is called
Then an array of 1 errors is shown

Scenario: A model that has an array field for integers but an array of strings is inserted into it, it should fail validation
Given ArrayModel1 model is used
When ArrayModelData3 data is inserted
Then the getArrayField field is called on the model
Then functions.validate is called
Then an array of 1 errors is shown

Scenario: A model that has an array field that has mixed values it should not fail validation.
Given ArrayModel2 model is used
When ArrayModelData4 data is inserted
Then the getArrayField field is called on the model
Then functions.validate is called
Then an array of 0 errors is shown

Scenario: A model that uses the arrayField field that has mixed values it should not fail validation.
Given ArrayModel3 model is used
When ArrayModelData4 data is inserted
Then the getArrayField field is called on the model
Then functions.validate is called
Then an array of 0 errors is shown
2 changes: 1 addition & 1 deletion features/model.feature
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Feature: Models

Scenario: A Model With a 4 fields
Given TestModel1 is used
Given TestModel1 model is used
When TestModel1b data is inserted
Then TestModel1b expected fields are found

40 changes: 37 additions & 3 deletions features/stepDefinitions/steps.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
const assert = require('chai').assert
const flatMap = require('lodash/flatMap')
const { Given, When, Then } = require('@cucumber/cucumber')

const { createModel, field } = require('../../index')
const { createModel, field, arrayField, validation } = require('../../index')

const MODEL_DEFINITIONS = {
TestModel1: createModel({
name: field({ required: true }),
type: field({ required: true, isString: true }),
flag: field({ required: true, isNumber: true }),
}),
ArrayModel1: createModel({
arrayField: field({
isArray: true,
validators: [validation.arrayType(validation.TYPE_PRIMATIVES.integer)],
}),
}),
ArrayModel2: createModel({
arrayField: field({ isArray: true }),
}),
ArrayModel3: createModel({
arrayField: arrayField({}),
}),
}

const MODEL_INPUT_VALUES = {
Expand All @@ -23,6 +34,18 @@ const MODEL_INPUT_VALUES = {
type: 'a-type',
flag: 1,
},
ArrayModelData1: {
arrayField: [1, 2, 3, 4, 5],
},
ArrayModelData2: {
arrayField: 'a-string',
},
ArrayModelData3: {
arrayField: ['a-string', 'a-string2'],
},
ArrayModelData4: {
arrayField: ['a-string', 1, {}, true],
},
}

const EXPECTED_FIELDS = {
Expand Down Expand Up @@ -58,7 +81,7 @@ Then('an array of {int} errors is shown', function (errorCount) {
assert.equal(errors.length, errorCount)
})

Given('{word} is used', function (modelDefinition) {
Given('{word} model is used', function (modelDefinition) {
const def = MODEL_DEFINITIONS[modelDefinition]
if (!def) {
throw new Error(`${modelDefinition} did not result in a definition`)
Expand All @@ -85,3 +108,14 @@ Then('{word} expected fields are found', function (fields) {
}
})
})

Then('the {word} field is called on the model', function (field) {
return this.instance[field]().then(result => {
this.results = result
})
})

Then('the array values match', function (table) {
const expected = JSON.parse(table.rowsHash().array)
assert.deepEqual(this.results, expected)
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "functional-models",
"version": "1.0.5",
"version": "1.0.6",
"description": "A library for creating JavaScript function based models.",
"main": "index.js",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,17 @@ const referenceField = config => {
})
}

const arrayField = (config = {}) =>
field({
defaultValue: [],
...config,
isArray: true,
})

module.exports = {
field,
uniqueId,
dateField,
arrayField,
referenceField,
}
36 changes: 35 additions & 1 deletion src/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ const isEmpty = require('lodash/isEmpty')
const flatMap = require('lodash/flatMap')
const get = require('lodash/get')

const TYPE_PRIMATIVES = {
boolean: 'boolean',
string: 'string',
object: 'object',
number: 'number',
integer: 'integer',
}

const _trueOrError = (method, error) => value => {
if (method(value) === false) {
return error
Expand Down Expand Up @@ -30,6 +38,28 @@ const isInteger = _trueOrError(v => {

const isBoolean = isType('boolean')
const isString = isType('string')
const isArray = _trueOrError(v => Array.isArray(v), 'Value is not an array')

const PRIMATIVE_TO_SPECIAL_TYPE_VALIDATOR = {
[TYPE_PRIMATIVES.boolean]: isBoolean,
[TYPE_PRIMATIVES.string]: isString,
[TYPE_PRIMATIVES.integer]: isInteger,
[TYPE_PRIMATIVES.number]: isNumber,
}

const arrayType = type => value => {
const arrayError = isArray(value)
if (arrayError) {
return arrayError
}
const validator = PRIMATIVE_TO_SPECIAL_TYPE_VALIDATOR[type] || isType(type)
return value.reduce((acc, v) => {
if (acc) {
return acc
}
return validator(v)
}, undefined)
}

const meetsRegex =
(regex, flags, errorMessage = 'Format was invalid') =>
Expand Down Expand Up @@ -122,6 +152,7 @@ const CONFIG_TO_VALIDATE_METHOD = {
isInteger: _boolChoice(isInteger),
isNumber: _boolChoice(isNumber),
isString: _boolChoice(isString),
isArray: _boolChoice(isArray),
}

const createFieldValidator = config => {
Expand All @@ -135,7 +166,7 @@ const createFieldValidator = config => {
validators.length > 0 ? aggregateValidator(validators) : emptyValidator
return async value => {
const errors = await validator(value)
return flatMap(errors)
return [...new Set(flatMap(errors))]
}
}

Expand All @@ -159,6 +190,7 @@ module.exports = {
isString,
isInteger,
isType,
isArray,
isRequired,
maxNumber,
minNumber,
Expand All @@ -170,4 +202,6 @@ module.exports = {
emptyValidator,
createFieldValidator,
createModelValidator,
arrayType,
TYPE_PRIMATIVES,
}
43 changes: 43 additions & 0 deletions test/src/fields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,53 @@ const {
field,
dateField,
referenceField,
arrayField,
} = require('../../src/fields')
const { createModel } = require('../../src/models')

describe('/src/fields.js', () => {
describe('#arrayField()', () => {
describe('#createGetter()', () => {
it('should return an array passed in without issue', async () => {
const theField = arrayField({})
const getter = theField.createGetter([1, 2, 3])
const actual = await getter()
const expected = [1, 2, 3]
assert.deepEqual(actual, expected)
})
it('should return an array passed in without issue, even if no config is passed', async () => {
const theField = arrayField()
const getter = theField.createGetter([1, 2, 3])
const actual = await getter()
const expected = [1, 2, 3]
assert.deepEqual(actual, expected)
})
it('should return an empty array if defaultValue is not changed in config and null is passed', async () => {
const theField = arrayField()
const getter = theField.createGetter(null)
const actual = await getter()
const expected = []
assert.deepEqual(actual, expected)
})
it('should return the passed in defaultValue if set in config and null is passed', async () => {
const theField = arrayField({ defaultValue: [1, 2, 3] })
const getter = theField.createGetter(null)
const actual = await getter()
const expected = [1, 2, 3]
assert.deepEqual(actual, expected)
})
})
describe('#getValidator()', () => {
it('should validate an array passed in without issue', async () => {
const theField = arrayField({})
const getter = theField.createGetter([1, 2, 3])
const validator = theField.getValidator(getter)
const actual = await validator()
const expected = []
assert.deepEqual(actual, expected)
})
})
})
describe('#field()', () => {
it('should throw an exception if config.valueSelector is not a function but is set', () => {
assert.throws(() => {
Expand Down
71 changes: 71 additions & 0 deletions test/src/validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const {
isBoolean,
isInteger,
isString,
isArray,
arrayType,
isRequired,
maxNumber,
minNumber,
Expand All @@ -16,6 +18,7 @@ const {
emptyValidator,
createModelValidator,
createFieldValidator,
TYPE_PRIMATIVES,
} = require('../../src/validation')

describe('/src/validation.js', () => {
Expand Down Expand Up @@ -315,4 +318,72 @@ describe('/src/validation.js', () => {
assert.equal(actual.length, expected)
})
})
describe('#isArray()', () => {
it('should return an error for null', () => {
const actual = isArray(null)
assert.isOk(actual)
})
it('should return an error for undefined', () => {
const actual = isArray(undefined)
assert.isOk(actual)
})
it('should return an error for 1', () => {
const actual = isArray(1)
assert.isOk(actual)
})
it('should return an error for "1"', () => {
const actual = isArray('1')
assert.isOk(actual)
})
it('should return undefined for [1,2,3]', () => {
const actual = isArray([1, 2, 3])
assert.isUndefined(actual)
})
it('should return undefined for []', () => {
const actual = isArray([])
assert.isUndefined(actual)
})
})
describe('#arrayType()', () => {
describe('#(object)()', () => {
it('should return an error for null, even though its an object, its not an array', () => {
const actual = arrayType('object')(null)
assert.isOk(actual)
})
it('should return an error for 1', () => {
const actual = arrayType('object')(1)
assert.isOk(actual)
})
it('should return undefined for [{}]', () => {
const actual = arrayType('object')([{}])
assert.isUndefined(actual)
})
})
describe('#(integer)()', () => {
it('should return an error for null', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)(null)
assert.isOk(actual)
})
it('should return an error for undefined', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)(undefined)
assert.isOk(actual)
})
it('should return an error for 1', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)(1)
assert.isOk(actual)
})
it('should return an error for "1"', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)('1')
assert.isOk(actual)
})
it('should return undefined for [1,2,3]', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)([1, 2, 3])
assert.isUndefined(actual)
})
it('should return an error for [1,"2",3]', () => {
const actual = arrayType(TYPE_PRIMATIVES.integer)([1, '2', 3])
assert.isOk(actual)
})
})
})
})