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
11 changes: 11 additions & 0 deletions features/functions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Feature: Functions

Scenario: A model with 2 properties (name, id), 2 model functions (modelWrapper, toString), and 2 instance functions (toString, toJson)
Given FunctionModel1 model is used
When FunctionModelData1 data is inserted
Then getName property is found
And getId property is found
And toString instance function is found
And toJson instance function is found
And modelWrapper model function is found
And toString model function is found
62 changes: 61 additions & 1 deletion features/stepDefinitions/steps.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
const assert = require('chai').assert
const flatMap = require('lodash/flatMap')
const { Given, When, Then } = require('@cucumber/cucumber')
const { Model, Property, ArrayProperty, validation } = require('../../index')
const {
Model,
UniqueId,
TextProperty,
Function,
Property,
ArrayProperty,
validation,
} = require('../../index')

const instanceToString = Function(modelInstance => {
return `${modelInstance.getModel().getName()}-Instance`
})

const instanceToJson = Function(async modelInstance => {
return JSON.stringify(await modelInstance.functions.toObj())
})

const modelToString = Function(model => {
return `${model.getName()}-[${Object.keys(model.getProperties()).join(',')}]`
})

const modelWrapper = Function(model => {
return model
})

const MODEL_DEFINITIONS = {
FunctionModel1: Model(
'FunctionModel1',
{
id: UniqueId({ required: true }),
name: TextProperty({ required: true }),
},
{
modelFunctions: {
modelWrapper,
toString: modelToString,
},
instanceFunctions: {
toString: instanceToString,
toJson: instanceToJson,
},
}
),
TestModel1: Model('TestModel1', {
name: Property({ required: true }),
type: Property({ required: true, isString: true }),
Expand All @@ -30,6 +71,10 @@ const MODEL_DEFINITIONS = {
}

const MODEL_INPUT_VALUES = {
FunctionModelData1: {
id: 'my-id',
name: 'function-model-name',
},
TestModel1a: {
name: 'my-name',
type: 1,
Expand Down Expand Up @@ -68,6 +113,8 @@ Given(
'the {word} has been created, with {word} inputs provided',
function (modelDefinition, modelInputValues) {
const def = MODEL_DEFINITIONS[modelDefinition]
this.model = def

const input = MODEL_INPUT_VALUES[modelInputValues]
if (!def) {
throw new Error(`${modelDefinition} did not result in a definition`)
Expand Down Expand Up @@ -99,6 +146,7 @@ Given('{word} model is used', function (modelDefinition) {
throw new Error(`${modelDefinition} did not result in a definition`)
}
this.modelDefinition = def
this.model = def
})

When('{word} data is inserted', function (modelInputValues) {
Expand Down Expand Up @@ -131,3 +179,15 @@ Then('the array values match', function (table) {
const expected = JSON.parse(table.rowsHash().array)
assert.deepEqual(this.results, expected)
})

Then('{word} property is found', function (propertyKey) {
assert.isFunction(this.instance[propertyKey])
})

Then('{word} instance function is found', function (instanceFunctionKey) {
assert.isFunction(this.instance.functions[instanceFunctionKey])
})

Then('{word} model function is found', function (modelFunctionKey) {
assert.isFunction(this.model[modelFunctionKey])
})
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.14",
"version": "1.0.15",
"description": "A library for creating JavaScript function based models.",
"main": "index.js",
"scripts": {
Expand Down
7 changes: 7 additions & 0 deletions src/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Function = method => wrapped => () => {
return method(wrapped)
}

module.exports = {
Function,
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
...require('./properties'),
...require('./models'),
...require('./functions'),
validation: require('./validation'),
serialization: require('./serialization'),
}
32 changes: 28 additions & 4 deletions src/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ const PROTECTED_KEYS = ['model']
const Model = (
modelName,
keyToProperty,
modelExtensions = {},
{ instanceCreatedCallback = null } = {}
{
instanceCreatedCallback = null,
modelFunctions = {},
instanceFunctions = {},
} = {}
) => {
/*
* This non-functional approach is specifically used to
Expand Down Expand Up @@ -43,6 +46,8 @@ const Model = (
)

const create = (instanceValues = {}) => {
// eslint-disable-next-line functional/no-let
let instance = null
const specialInstanceProperties1 = MODEL_DEF_KEYS.reduce((acc, key) => {
if (key in instanceValues) {
return { ...acc, [key]: instanceValues[key] }
Expand Down Expand Up @@ -77,10 +82,20 @@ const Model = (
},
},
}
const instance = merge(
const fleshedOutInstanceFunctions = Object.entries(
instanceFunctions
).reduce((acc, [key, func]) => {
return merge(acc, {
functions: {
[key]: func(instance),
},
})
}, {})
instance = merge(
{},
loadedInternals,
specialProperties,
fleshedOutInstanceFunctions,
frameworkProperties,
specialInstanceProperties1
)
Expand All @@ -90,8 +105,17 @@ const Model = (
return instance
}

const fleshedOutModelFunctions = Object.entries(modelFunctions).reduce(
(acc, [key, func]) => {
return merge(acc, {
[key]: func(model),
})
},
{}
)

// This sets the model that is used by the instances later.
model = merge({}, modelExtensions, {
model = merge({}, fleshedOutModelFunctions, {
create,
getName: () => modelName,
getProperties: () => properties,
Expand Down
45 changes: 45 additions & 0 deletions test/src/functions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const assert = require('chai').assert
const sinon = require('sinon')
const { Function } = require('../../src/functions')

describe('/src/functions.js', () => {
describe('#Function()', () => {
it('should return "Hello-world" when passed in', () => {
const method = sinon.stub().callsFake(input => {
return `${input}-world`
})
const myFunction = Function(method)
const wrappedObj = 'Hello'
const wrappedFunc = myFunction(wrappedObj)
const actual = wrappedFunc()
const expected = 'Hello-world'
assert.equal(actual, expected)
})
it('should call the method when Function()()() called', () => {
const method = sinon.stub().callsFake(input => {
return `${input}-world`
})
const myFunction = Function(method)
const wrappedObj = 'Hello'
const wrappedFunc = myFunction(wrappedObj)
const result = wrappedFunc()
sinon.assert.calledOnce(method)
})
it('should not call the method when Function()() called', () => {
const method = sinon.stub().callsFake(input => {
return `${input}-world`
})
const myFunction = Function(method)
const wrappedObj = 'Hello'
const wrappedFunc = myFunction(wrappedObj)
sinon.assert.notCalled(method)
})
it('should not call the method when Function() called', () => {
const method = sinon.stub().callsFake(input => {
return `${input}-world`
})
const myFunction = Function(method)
sinon.assert.notCalled(method)
})
})
})
39 changes: 33 additions & 6 deletions test/src/models.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,45 @@ const { Property } = require('../../src/properties')

describe('/src/models.js', () => {
describe('#Model()', () => {
it('should find model.myString when modelExtension has myString function in it', () => {
const model = Model(
'ModelName',
{},
{
modelFunctions: {
myString: model => () => {
return 'To String'
},
},
}
)
console.log(model)
assert.isFunction(model.myString)
})
describe('#create()', () => {
it('should find instance.functions.toString when in instanceFunctions', () => {
const model = Model(
'ModelName',
{},
{
instanceFunctions: {
toString: instance => () => {
return 'An instance'
},
},
}
)
const instance = model.create({})
assert.isFunction(instance.functions.toString)
})
it('should call the instanceCreatedCallback function when create() is called', () => {
const input = {
myProperty: Property({ required: true }),
}
const callback = sinon.stub()
const model = Model(
'name',
input,
{},
{ instanceCreatedCallback: callback }
)
const model = Model('name', input, {
instanceCreatedCallback: callback,
})
model.create({ myProperty: 'value' })
sinon.assert.calledOnce(callback)
})
Expand Down