diff --git a/features/functions.feature b/features/functions.feature new file mode 100644 index 0000000..c32591f --- /dev/null +++ b/features/functions.feature @@ -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 diff --git a/features/stepDefinitions/steps.js b/features/stepDefinitions/steps.js index 9f9ffae..9f2cc34 100644 --- a/features/stepDefinitions/steps.js +++ b/features/stepDefinitions/steps.js @@ -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 }), @@ -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, @@ -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`) @@ -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) { @@ -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]) +}) diff --git a/package.json b/package.json index 0007fce..069d4b8 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/functions.js b/src/functions.js new file mode 100644 index 0000000..444384d --- /dev/null +++ b/src/functions.js @@ -0,0 +1,7 @@ +const Function = method => wrapped => () => { + return method(wrapped) +} + +module.exports = { + Function, +} diff --git a/src/index.js b/src/index.js index a1d5cd4..9592d92 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ module.exports = { ...require('./properties'), ...require('./models'), + ...require('./functions'), validation: require('./validation'), serialization: require('./serialization'), } diff --git a/src/models.js b/src/models.js index a0cfb6d..edec289 100644 --- a/src/models.js +++ b/src/models.js @@ -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 @@ -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] } @@ -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 ) @@ -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, diff --git a/test/src/functions.test.js b/test/src/functions.test.js new file mode 100644 index 0000000..98a7a44 --- /dev/null +++ b/test/src/functions.test.js @@ -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) + }) + }) +}) diff --git a/test/src/models.test.js b/test/src/models.test.js index 75f5de6..b075ed2 100644 --- a/test/src/models.test.js +++ b/test/src/models.test.js @@ -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) })