diff --git a/src/__tests__/buildStoreChunk.test.js b/src/__tests__/buildStoreChunk.test.js index 0dd0d0a..a87b720 100644 --- a/src/__tests__/buildStoreChunk.test.js +++ b/src/__tests__/buildStoreChunk.test.js @@ -1,164 +1,203 @@ -import { -buildStoreChunk, -} from '../buildStoreChunk'; -import { -Types, -} from '../structure'; -import { -createCombinedAction, -} from '../reducers/batchUpdates'; -import { -createStore, -combineReducers, -} from 'redux'; -import isFunction from 'lodash/isFunction'; +import { buildStoreChunk } from "../buildStoreChunk"; +import { Types } from "../structure"; +import { createCombinedAction } from "../reducers/batchUpdates"; +import { createStore, combineReducers } from "redux"; +import isFunction from "lodash/isFunction"; +describe("buildStoreChunk", () => { + it("Will throw error if a structure is not defined", () => { + expect(() => buildStoreChunk("toast")).toThrowError(/structure/); + }); + it("Will accept a single reducer (no nesting)", () => { + expect( + Object.keys(buildStoreChunk("toast", Types.reducer(Types.string()))) + ).toEqual(["reducers", "actions", "selectors"]); + }); + it("Will return an object containing reducers, actions, and selectors as the result", () => { + expect( + Object.keys( + buildStoreChunk("toast", { + example: Types.reducer(Types.string()) + }) + ) + ).toEqual(["reducers", "actions", "selectors"]); + }); -describe('buildStoreChunk', () => { - it('Will throw error if a structure is not defined', () => { - expect(() => buildStoreChunk('toast')).toThrowError(/structure/); + describe("Resulting chunk", () => { + const chunk = buildStoreChunk("example", { + nested1: Types.reducer(Types.string("foo")), + nested2: Types.reducer( + Types.shape({ + foo: Types.number(), + bar: Types.string() + }) + ), + nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])), + nested4: Types.reducer({ + innerNested1: Types.reducer(Types.string("bar")), + innerNested2: Types.reducer({ + innerNested3: Types.reducer(Types.string("baz")) + }) + }), + nested5: Types.reducer( + Types.shape({ + arrayExample: Types.arrayOf(Types.string()) + }) + ) }); - it('Will accept a single reducer (no nesting)', () => { - expect(Object.keys(buildStoreChunk('toast', Types.reducer(Types.string()) )) ) - .toEqual(['reducers', 'actions', 'selectors']); - }); - it('Will return an object containing reducers, actions, and selectors as the result', () => { - expect(Object.keys(buildStoreChunk('toast', { - example: Types.reducer(Types.string()), - }))).toEqual(['reducers', 'actions', 'selectors']); + const nonNestedChunk = buildStoreChunk( + "example2", + Types.reducer(Types.string("foo")) + ); + + describe("Selectors", () => { + const store = createStore( + combineReducers({ + ...chunk.reducers + }) + ); + + it("Selectors object has the correct top level structure for a nested chunk", () => { + expect(Object.keys(chunk.selectors)).toEqual([ + "nested1", + "nested2", + "nested3", + "nested4", + "nested5" + ]); + }); + it("Selectors object is a function for a non-nested chunk", () => { + expect(isFunction(nonNestedChunk.selectors)).toBe(true); + }); + it("Nested selectors object has the correct structure for a defined reducer", () => { + expect(Object.keys(chunk.selectors.nested4)).toEqual([ + "innerNested1", + "innerNested2" + ]); + }); + it("Selector returns correct value", () => { + expect(chunk.selectors.nested1(store.getState())).toEqual("foo"); + }); + it("Nested selector returns correct value", () => { + expect(chunk.selectors.nested4.innerNested1(store.getState())).toEqual( + "bar" + ); + }); }); - describe('Resulting chunk', () => { - const chunk = buildStoreChunk('example', { - nested1: Types.reducer(Types.string('foo')), - nested2: Types.reducer(Types.shape({ - foo: Types.number(), - bar: Types.string(), - })), - nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])), - nested4: Types.reducer({ - innerNested1: Types.reducer(Types.string('bar')), - innerNested2: Types.reducer({ - innerNested3: Types.reducer(Types.string('baz')), - }), - }), - nested5: Types.reducer(Types.shape({ - arrayExample: Types.arrayOf(Types.string()), - })), - }); - const nonNestedChunk = buildStoreChunk('example2', Types.reducer(Types.string('foo'))); - - describe('Selectors', () => { - const store = createStore(combineReducers({ - ...chunk.reducers, - })); - - it('Selectors object has the correct top level structure for a nested chunk', () => { - expect(Object.keys(chunk.selectors)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']); - }); - it('Selectors object is a function for a non-nested chunk', () => { - expect(isFunction(nonNestedChunk.selectors)).toBe(true); - }); - it('Nested selectors object has the correct structure for a defined reducer', () => { - expect(Object.keys(chunk.selectors.nested4)).toEqual(['innerNested1', 'innerNested2']); - }); - it('Selector returns correct value', () => { - expect(chunk.selectors.nested1(store.getState())).toEqual('foo'); - }); - it('Nested selector returns correct value', () => { - expect(chunk.selectors.nested4.innerNested1(store.getState())).toEqual('bar'); - }); - }); - - describe('Actions', () => { - it('Actions object has the correct top level structure for a nested chunk', () => { - expect(Object.keys(chunk.actions)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']); - }); - it('Actions object has the correct top level structure for a non nested chunk', () => { - expect(Object.keys(nonNestedChunk.actions)).toEqual(['replace', 'reset']); - }); - it('Nested actions object has the correct structure for a chunk', () => { - expect(Object.keys(chunk.actions.nested4)).toEqual(['innerNested1', 'innerNested2']); - }); - it('Replace actions return an object that contains a type and payload', () => { - expect(Object.keys(chunk.actions.nested1.replace('bar'))).toEqual(['type', 'payload']); - expect(Object.keys(chunk.actions.nested2.replace({}))).toEqual(['type', 'payload']); - expect(Object.keys(chunk.actions.nested3.replace([]))).toEqual(['type', 'payload', 'index']); - }); - }); - - describe('Combined actions and selectors (nested chunk)', () => { - const store = createStore(combineReducers({ - ...chunk.reducers, - ...nonNestedChunk.reducers, - })); - - it('Dispatching an action should correctly update the store', () => { - store.dispatch(chunk.actions.nested1.replace('bar')); - expect(chunk.selectors.nested1(store.getState())).toEqual('bar'); - - store.dispatch(chunk.actions.nested1.reset()); - expect(chunk.selectors.nested1(store.getState())).toEqual('foo'); - }); - - it('Dispatching an empty array property should replace existing array', () => { - store.dispatch(chunk.actions.nested5.replace({ arrayExample: ['2'] })); - store.dispatch(chunk.actions.nested5.update({ arrayExample: [] })); - expect(chunk.selectors.nested5(store.getState())).toEqual({ - arrayExample: [], - }); - }); - }); - - describe('Combined actions and selectors (non nested chunk)', () => { - const store = createStore(combineReducers({ - ...chunk.reducers, - ...nonNestedChunk.reducers, - })); - - it('Dispatching an action should correctly update the store', () => { - store.dispatch(nonNestedChunk.actions.replace('bar')); - expect(nonNestedChunk.selectors(store.getState())).toEqual('bar'); - - store.dispatch(nonNestedChunk.actions.reset()); - expect(nonNestedChunk.selectors(store.getState())).toEqual('foo'); - }); - }); - - - describe('Combined actions', () => { - const store = createStore(combineReducers({ - ...chunk.reducers, - ...nonNestedChunk.reducers, - })); - - it('Dispatching a createCombinedAction updates the store correctly', () => { - store.dispatch(createCombinedAction({ - name: 'batchUpdateFunsies', - actions: [ - nonNestedChunk.actions.replace('bar'), - chunk.actions.nested2.update({ - foo: 4, - }), - chunk.actions.nested2.update({ - bar: 'boop!', - }), - chunk.actions.nested3.replace([ - 4,5,6 - ]), - chunk.actions.nested3.removeAtIndex(1), - ], - })); - expect(nonNestedChunk.selectors(store.getState())).toEqual('bar'); - expect(chunk.selectors.nested2(store.getState())).toEqual({ - foo: 4, - bar: 'boop!', - }); - expect(chunk.selectors.nested3(store.getState())).toEqual([ - 4,6, - ]); - }); - }); + describe("Actions", () => { + it("Actions object has the correct top level structure for a nested chunk", () => { + expect(Object.keys(chunk.actions)).toEqual([ + "nested1", + "nested2", + "nested3", + "nested4", + "nested5" + ]); + }); + it("Actions object has the correct top level structure for a non nested chunk", () => { + expect(Object.keys(nonNestedChunk.actions)).toEqual([ + "replace", + "reset" + ]); + }); + it("Nested actions object has the correct structure for a chunk", () => { + expect(Object.keys(chunk.actions.nested4)).toEqual([ + "innerNested1", + "innerNested2" + ]); + }); + it("Replace actions return an object that contains a type and payload", () => { + expect(Object.keys(chunk.actions.nested1.replace("bar"))).toEqual([ + "type", + "payload" + ]); + expect(Object.keys(chunk.actions.nested2.replace({}))).toEqual([ + "type", + "payload" + ]); + expect(Object.keys(chunk.actions.nested3.replace([]))).toEqual([ + "type", + "payload", + "index" + ]); + }); }); + describe("Combined actions and selectors (nested chunk)", () => { + const store = createStore( + combineReducers({ + ...chunk.reducers, + ...nonNestedChunk.reducers + }) + ); + + it("Dispatching an action should correctly update the store", () => { + store.dispatch(chunk.actions.nested1.replace("bar")); + expect(chunk.selectors.nested1(store.getState())).toEqual("bar"); + + store.dispatch(chunk.actions.nested1.reset()); + expect(chunk.selectors.nested1(store.getState())).toEqual("foo"); + }); + + it("Dispatching an empty array property should replace existing array", () => { + store.dispatch(chunk.actions.nested5.replace({ arrayExample: ["2"] })); + store.dispatch(chunk.actions.nested5.update({ arrayExample: [] })); + expect(chunk.selectors.nested5(store.getState())).toEqual({ + arrayExample: [] + }); + }); + }); + + describe("Combined actions and selectors (non nested chunk)", () => { + const store = createStore( + combineReducers({ + ...chunk.reducers, + ...nonNestedChunk.reducers + }) + ); + + it("Dispatching an action should correctly update the store", () => { + store.dispatch(nonNestedChunk.actions.replace("bar")); + expect(nonNestedChunk.selectors(store.getState())).toEqual("bar"); + + store.dispatch(nonNestedChunk.actions.reset()); + expect(nonNestedChunk.selectors(store.getState())).toEqual("foo"); + }); + }); + + describe("Combined actions", () => { + const store = createStore( + combineReducers({ + ...chunk.reducers, + ...nonNestedChunk.reducers + }) + ); + + it("Dispatching a createCombinedAction updates the store correctly", () => { + store.dispatch( + createCombinedAction({ + name: "batchUpdateFunsies", + actions: [ + nonNestedChunk.actions.replace("bar"), + chunk.actions.nested2.update({ + foo: 4 + }), + chunk.actions.nested2.update({ + bar: "boop!" + }), + chunk.actions.nested3.replace([4, 5, 6]), + chunk.actions.nested3.removeAtIndex(1) + ] + }) + ); + expect(nonNestedChunk.selectors(store.getState())).toEqual("bar"); + expect(chunk.selectors.nested2(store.getState())).toEqual({ + foo: 4, + bar: "boop!" + }); + expect(chunk.selectors.nested3(store.getState())).toEqual([4, 6]); + }); + }); + }); }); diff --git a/src/__tests__/reducers.test.js b/src/__tests__/reducers.test.js index 4d79c7e..8248b60 100644 --- a/src/__tests__/reducers.test.js +++ b/src/__tests__/reducers.test.js @@ -1,115 +1,138 @@ //@flow +import { Types, PROP_TYPES } from "../structure"; import { - Types, - PROP_TYPES, -} from '../structure'; -import { - calculateDefaults, - determineReducerType, - callReducer, - createReducerBehaviors, - REDUCER_CREATOR_MAPPING, -} from '../reducers'; -import forEach from 'lodash/forEach'; -import omit from 'lodash/omit'; + calculateDefaults, + determineReducerType, + callReducer, + createReducerBehaviors, + REDUCER_CREATOR_MAPPING +} from "../reducers"; +import forEach from "lodash/forEach"; +import omit from "lodash/omit"; -describe('reducers', () => { - - describe('calculateDefaults', () => { - it('Should provide correct default values for a given primitive type', () => { - expect(calculateDefaults(Types.string('toast'))).toBe('toast'); - expect(calculateDefaults(Types.number(3))).toBe(3); - expect(calculateDefaults(Types.string())).toBe(''); - expect(calculateDefaults(Types.number())).toBe(0); - }); - - it('Should provide correct default values for an object', () => { - const objectStructure = Types.shape({ - test1: Types.string(), - test2: Types.number(), - }); - expect(calculateDefaults(objectStructure)).toEqual({ test1: '', test2: 0 }); - }); - - it('Should provide correct default values for nested object', () => { - const objectStructure = Types.shape({ - test1: Types.string(), - test2: Types.number(), - test3: Types.shape({ - test4: Types.string('foo'), - }) - }); - expect(calculateDefaults(objectStructure)).toEqual({ - test1: '', - test2: 0, - test3: { - test4: 'foo' - } - }); - }); +describe("reducers", () => { + describe("calculateDefaults", () => { + it("Should provide correct default values for a given primitive type", () => { + expect(calculateDefaults(Types.string("toast"))).toBe("toast"); + expect(calculateDefaults(Types.number(3))).toBe(3); + expect(calculateDefaults(Types.string())).toBe(""); + expect(calculateDefaults(Types.number())).toBe(0); }); - describe('determineReducerType', () => { - it('should return the correct creator function for the default mapping', () => { - forEach({ custom: Types.custom(), ...omit(Types, 'reducer', 'wildcardKey', 'custom') }, structureType => { - const returnVal = determineReducerType(Types.reducer(structureType()), { - name: 'toast', - locationString: 'toasty', - }); - - expect({ - ...returnVal, - reducerFn: returnVal.reducerFn.name, - reducerStructureDescriptor: returnVal.reducerStructureDescriptor.name, - }).toEqual({ - name: 'toast', - reducerFn: REDUCER_CREATOR_MAPPING[structureType()().type].name, - reducerStructureDescriptor: '', //The internal functions should be anonymous - locationString: 'toasty', - }); - }); - }); - - it('should throw an error if the type provided does not match any in the mapping', () => { - expect(() => determineReducerType(Types.reducer(Types.string()), { - name: 'toast', - locationString: 'toasty', - reducerCreatorMapping: omit(REDUCER_CREATOR_MAPPING, PROP_TYPES._string), - })).toThrowError(/createReducer/) - }); + it("Should provide correct default values for an object", () => { + const objectStructure = Types.shape({ + test1: Types.string(), + test2: Types.number() + }); + expect(calculateDefaults(objectStructure)).toEqual({ + test1: "", + test2: 0 + }); }); - describe('callReducer', () => { - it('should call the provided reducer with the structure description, location string, and name', () => { - expect(callReducer({ - reducerStructureDescriptor: 'foo', - name: 'toast', - locationString: 'toasty', - reducerFn: (reducerStructureDescriptor, { locationString, name }) => ({ reducerStructureDescriptor, locationString, name }) - })).toEqual({ - reducerStructureDescriptor: 'foo', - locationString: 'toasty', - name: 'toast', - }) - }); + it("Should provide correct default values for nested object", () => { + const objectStructure = Types.shape({ + test1: Types.string(), + test2: Types.number(), + test3: Types.shape({ + test4: Types.string("foo") + }) + }); + expect(calculateDefaults(objectStructure)).toEqual({ + test1: "", + test2: 0, + test3: { + test4: "foo" + } + }); + }); + }); + + describe("determineReducerType", () => { + it("should return the correct creator function for the default mapping", () => { + forEach( + { + custom: Types.custom(), + ...omit(Types, "reducer", "wildcardKey", "custom") + }, + structureType => { + const returnVal = determineReducerType( + Types.reducer(structureType()), + { + name: "toast", + locationString: "toasty" + } + ); + + expect({ + ...returnVal, + reducerFn: returnVal.reducerFn.name, + reducerStructureDescriptor: + returnVal.reducerStructureDescriptor.name + }).toEqual({ + name: "toast", + reducerFn: REDUCER_CREATOR_MAPPING[structureType()().type].name, + reducerStructureDescriptor: "", //The internal functions should be anonymous + locationString: "toasty" + }); + } + ); }); - describe('createReducerBehaviors', () => { - it('Should return only the reducers of the behavior config and prepend the locationString', () => { - expect(createReducerBehaviors({ - 'toast': { - reducer: 'foo', - action: 'bar', - validate: true, - } - }, 'location')).toEqual({ - 'location.toast': { - reducer: 'foo', - action: 'bar', - validate: true, - }, - }) - }); + it("should throw an error if the type provided does not match any in the mapping", () => { + expect(() => + determineReducerType(Types.reducer(Types.string()), { + name: "toast", + locationString: "toasty", + reducerCreatorMapping: omit( + REDUCER_CREATOR_MAPPING, + PROP_TYPES._string + ) + }) + ).toThrowError(/createReducer/); }); + }); -}); \ No newline at end of file + describe("callReducer", () => { + it("should call the provided reducer with the structure description, location string, and name", () => { + expect( + callReducer({ + reducerStructureDescriptor: "foo", + name: "toast", + locationString: "toasty", + reducerFn: ( + reducerStructureDescriptor, + { locationString, name } + ) => ({ reducerStructureDescriptor, locationString, name }) + }) + ).toEqual({ + reducerStructureDescriptor: "foo", + locationString: "toasty", + name: "toast" + }); + }); + }); + + describe("createReducerBehaviors", () => { + it("Should return only the reducers of the behavior config and prepend the locationString", () => { + expect( + createReducerBehaviors( + { + toast: { + reducer: "foo", + action: "bar", + validate: true + } + }, + "location" + ) + ).toEqual({ + "location.toast": { + reducer: "foo", + action: "bar", + validate: true + } + }); + }); + }); +}); diff --git a/src/__tests__/structure.test.js b/src/__tests__/structure.test.js index fe0799b..1843006 100644 --- a/src/__tests__/structure.test.js +++ b/src/__tests__/structure.test.js @@ -1,64 +1,68 @@ -import { Types, PROP_TYPES } from '../structure'; +import { Types, PROP_TYPES } from "../structure"; function hasType(object, type) { - return object.type === type; + return object.type === type; } -describe('Type descriptions', () => { - it('will return a function which subsequently returns an object with the correct type', () => { - expect(hasType(Types.string()(), PROP_TYPES._string)).toBeTruthy(); - expect(hasType(Types.number()(), PROP_TYPES._number)).toBeTruthy(); - expect(hasType(Types.boolean()(), PROP_TYPES._boolean)).toBeTruthy(); - expect(hasType(Types.arrayOf()(), PROP_TYPES._array)).toBeTruthy(); - expect(hasType(Types.reducer()(), PROP_TYPES._reducer)).toBeTruthy(); - expect(hasType(Types.shape()(), PROP_TYPES._shape)).toBeTruthy(); - }); +describe("Type descriptions", () => { + it("will return a function which subsequently returns an object with the correct type", () => { + expect(hasType(Types.string()(), PROP_TYPES._string)).toBeTruthy(); + expect(hasType(Types.number()(), PROP_TYPES._number)).toBeTruthy(); + expect(hasType(Types.boolean()(), PROP_TYPES._boolean)).toBeTruthy(); + expect(hasType(Types.arrayOf()(), PROP_TYPES._array)).toBeTruthy(); + expect(hasType(Types.reducer()(), PROP_TYPES._reducer)).toBeTruthy(); + expect(hasType(Types.shape()(), PROP_TYPES._shape)).toBeTruthy(); + }); - it('will return the standard default values when none provided', () => { - expect(Types.string()().defaultValue).toBe(''); - expect(Types.number()().defaultValue).toBe(0); - expect(Types.boolean()().defaultValue).toBe(false); - expect(Types.arrayOf()().defaultValue).toEqual([]); - expect(Types.custom()()().defaultValue).toBeUndefined(); - }); + it("will return the standard default values when none provided", () => { + expect(Types.string()().defaultValue).toBe(""); + expect(Types.number()().defaultValue).toBe(0); + expect(Types.boolean()().defaultValue).toBe(false); + expect(Types.arrayOf()().defaultValue).toEqual([]); + expect(Types.custom()()().defaultValue).toBeUndefined(); + }); - it('will return the default value provided (except for reducer and shape)', () => { - expect(Types.string('foo')().defaultValue).toBe('foo'); - expect(Types.number(5)().defaultValue).toBe(5); - expect(Types.boolean(true)().defaultValue).toBe(true); - expect(Types.arrayOf(Types.number(), [1, 2, 3])().defaultValue).toEqual([1, 2, 3]); - expect(Types.custom()('foo')().defaultValue).toBe('foo'); - }); + it("will return the default value provided (except for reducer and shape)", () => { + expect(Types.string("foo")().defaultValue).toBe("foo"); + expect(Types.number(5)().defaultValue).toBe(5); + expect(Types.boolean(true)().defaultValue).toBe(true); + expect(Types.arrayOf(Types.number(), [1, 2, 3])().defaultValue).toEqual([ + 1, + 2, + 3 + ]); + expect(Types.custom()("foo")().defaultValue).toBe("foo"); + }); - it('will return the correct typeofValue (for string, number, and boolean)', () => { - expect(Types.string()().typeofValue).toBe('string'); - expect(Types.number()().typeofValue).toBe('number'); - expect(Types.boolean()().typeofValue).toBe('boolean'); - }); + it("will return the correct typeofValue (for string, number, and boolean)", () => { + expect(Types.string()().typeofValue).toBe("string"); + expect(Types.number()().typeofValue).toBe("number"); + expect(Types.boolean()().typeofValue).toBe("boolean"); + }); - it('will return the correct structure (for arrayOf, reducer, and shape)', () => { - const structureTest = Types.string(); - expect(Types.shape(structureTest)().structure).toEqual(structureTest); - expect(Types.arrayOf(structureTest)().structure).toEqual(structureTest); - expect(Types.reducer(structureTest)().structure).toEqual(structureTest); - }); + it("will return the correct structure (for arrayOf, reducer, and shape)", () => { + const structureTest = Types.string(); + expect(Types.shape(structureTest)().structure).toEqual(structureTest); + expect(Types.arrayOf(structureTest)().structure).toEqual(structureTest); + expect(Types.reducer(structureTest)().structure).toEqual(structureTest); + }); - it('custom value correctly exposes the validator and validation error message properties', () => { - const customValidator = () => false; - const customErrorMessage = () => 'Hai!'; - const customType = Types.custom({ - validator: customValidator, - validationErrorMessage: customErrorMessage, - })()(); + it("custom value correctly exposes the validator and validation error message properties", () => { + const customValidator = () => false; + const customErrorMessage = () => "Hai!"; + const customType = Types.custom({ + validator: customValidator, + validationErrorMessage: customErrorMessage + })()(); - expect(customType.validator).toEqual(customValidator); - expect(customType.validationErrorMessage).toEqual(customErrorMessage); - }); + expect(customType.validator).toEqual(customValidator); + expect(customType.validationErrorMessage).toEqual(customErrorMessage); + }); - it('custom value acts the same as any when no configuration provided', () => { - const unconfiguredCustom = Types.custom()()(); - expect(unconfiguredCustom.validator('toasty')).toBe(true); - expect(unconfiguredCustom.validator()).toBe(true); - expect(unconfiguredCustom.validator(null)).toBe(true); - }); + it("custom value acts the same as any when no configuration provided", () => { + const unconfiguredCustom = Types.custom()()(); + expect(unconfiguredCustom.validator("toasty")).toBe(true); + expect(unconfiguredCustom.validator()).toBe(true); + expect(unconfiguredCustom.validator(null)).toBe(true); + }); }); diff --git a/src/__tests__/validatePayload.test.js b/src/__tests__/validatePayload.test.js index c001129..1b12061 100644 --- a/src/__tests__/validatePayload.test.js +++ b/src/__tests__/validatePayload.test.js @@ -1,208 +1,245 @@ -import { Types } from '../structure'; +import { Types } from "../structure"; import { - validateValue, - validateShape, - validateArray, - getTypeValidation, - hasWildcardKey, - getValueType, -} from '../validatePayload'; + validateValue, + validateShape, + validateArray, + getTypeValidation, + hasWildcardKey, + getValueType +} from "../validatePayload"; -describe('Validation functionality', () => { +describe("Validation functionality", () => { + describe("Primitives/Custom", () => { + const customType = Types.custom({ + validator: value => value === 3, + validationErrorMessage: value => `Oh noes! ${value}` + })(); - describe('Primitives/Custom', () => { - const customType = Types.custom({ - validator: value => value === 3, - validationErrorMessage: value => `Oh noes! ${ value }`, - })(); + it("Number primitive should allow for numbers", () => { + expect(validateValue(Types.number(), 3)).toBe(3); + }); + it("String primitive should allow for string", () => { + expect(validateValue(Types.string(), "toast")).toBe("toast"); + }); + it("Boolean primitive should allow for string", () => { + expect(validateValue(Types.boolean(), true)).toBe(true); + }); + it("Any should allow for anything", () => { + const date = new Date(); + expect(validateValue(Types.any(), date)).toEqual(date); + }); + it("should validate custom values using the custom validator", () => { + expect(validateValue(customType, 3)).toBe(3); + }); + it("should return undefined from custom validators which failed", () => { + expect(validateValue(customType, 4)).toBeUndefined(); + }); + }); - it('Number primitive should allow for numbers', () => { - expect(validateValue(Types.number(), 3)).toBe(3); - }); - it('String primitive should allow for string', () => { - expect(validateValue(Types.string(), 'toast')).toBe('toast'); - }); - it('Boolean primitive should allow for string', () => { - expect(validateValue(Types.boolean(), true)).toBe(true); - }); - it('Any should allow for anything', () => { - const date = new Date(); - expect(validateValue(Types.any(), date)).toEqual(date); - }); - it('should validate custom values using the custom validator', () => { - expect(validateValue(customType, 3)).toBe(3); - }); - it('should return undefined from custom validators which failed', () => { - expect(validateValue(customType, 4)).toBeUndefined(); - }); + describe("Arrays", () => { + const testArrayStructure = Types.arrayOf(Types.string()); + it("Arrays should allow for primitives", () => { + expect(validateArray(testArrayStructure, ["a", "b", "c", "d"])).toEqual([ + "a", + "b", + "c", + "d" + ]); + }); + it("Arrays should strip values for primitives which fail the test", () => { + expect(validateArray(testArrayStructure, ["a", "b", 3, "d"])).toEqual([ + "a", + "b", + "d" + ]); }); - describe('Arrays', () => { - const testArrayStructure = Types.arrayOf(Types.string()); - it('Arrays should allow for primitives', () => { - expect(validateArray(testArrayStructure, ['a','b','c','d'])) - .toEqual(['a','b','c','d']); - }); - it('Arrays should strip values for primitives which fail the test', () => { - expect(validateArray(testArrayStructure, ['a','b',3,'d'])) - .toEqual(['a','b','d']); - }); + const testArrayStructure2 = Types.arrayOf( + Types.shape({ + test1: Types.number() + }) + ); + it("Arrays should allow for complex objects", () => { + expect( + validateArray(testArrayStructure2, [{ test1: 3 }, { test1: 4 }]) + ).toEqual([{ test1: 3 }, { test1: 4 }]); + }); + const testArrayStructure3 = Types.arrayOf( + Types.shape({ + test1: Types.arrayOf(Types.number()), + test2: Types.custom({ validator: value => value === "foo" })() + }) + ); + it("Arrays should allow for complex objects - test 2", () => { + expect( + validateArray(testArrayStructure3, [{ test1: [3, 4, 5], test2: "foo" }]) + ).toEqual([{ test1: [3, 4, 5], test2: "foo" }]); + }); + it("Array should return an empty array if a non-array is passed", () => { + expect(validateArray("foo")).toEqual([]); + }); + it("Array should allow an empty array", () => { + expect(validateArray([])).toEqual([]); + }); + }); - const testArrayStructure2 = Types.arrayOf(Types.shape({ - test1: Types.number() - })); - it('Arrays should allow for complex objects', () => { - expect(validateArray(testArrayStructure2, [{test1: 3},{test1: 4}])) - .toEqual([{test1: 3},{test1: 4}]); - }); - const testArrayStructure3 = Types.arrayOf(Types.shape({ - test1: Types.arrayOf(Types.number()), - test2: Types.custom({ validator: value => value === 'foo' })(), - })); - it('Arrays should allow for complex objects - test 2', () => { - expect(validateArray(testArrayStructure3, [{test1: [3,4,5], test2: 'foo' }])) - .toEqual([{test1: [3,4,5], test2: 'foo' }]); - }); - it('Array should return an empty array if a non-array is passed', () => { - expect(validateArray('foo')).toEqual([]); - }); - it('Array should allow an empty array', () => { - expect(validateArray([])).toEqual([]); - }); + describe("Objects", () => { + const testObjectStructure = Types.shape({ + test1: Types.string(), + test2: Types.number(), + test3: Types.custom({ validator: value => value !== 4 })() + }); + it("Object of primitives should allow all props present in the structure", () => { + expect( + validateShape(testObjectStructure, { + test1: "toast", + test2: 3, + test3: 1 + }) + ).toEqual({ test1: "toast", test2: 3, test3: 1 }); + }); + it("Object of primitives should only allow for props with values which match their config", () => { + expect( + validateShape(testObjectStructure, { test1: 5, test2: 3, test3: 4 }) + ).toEqual({ test2: 3 }); + }); + it("Object of primitives should strip any properties not part of the config", () => { + expect( + validateShape(testObjectStructure, { + test1: "toast", + test2: 3, + toast: "bar" + }) + ).toEqual({ test1: "toast", test2: 3 }); }); - describe('Objects', () => { - const testObjectStructure = Types.shape({ - test1: Types.string(), - test2: Types.number(), - test3: Types.custom({ validator: value => value !== 4 })(), - }); - it('Object of primitives should allow all props present in the structure', () => { - expect(validateShape(testObjectStructure, { test1: 'toast', test2: 3, test3: 1 })) - .toEqual({ test1: 'toast', test2: 3, test3: 1 }); - }); - it('Object of primitives should only allow for props with values which match their config', () => { - expect(validateShape(testObjectStructure, { test1: 5, test2: 3, test3: 4 })) - .toEqual({ test2: 3 }); - }); - it('Object of primitives should strip any properties not part of the config', () => { - expect(validateShape(testObjectStructure, { test1: 'toast', test2: 3, toast: 'bar' })) - .toEqual({ test1: 'toast', test2: 3 }); - }); - - const testObjectStructure2 = Types.shape({ - test1: testObjectStructure, - }); - it('Objects should allow for arbitrary nesting of objects', () => { - expect(validateShape(testObjectStructure2, { test1: { test1: 'toast', test2: 3 } })) - .toEqual({ test1: { test1: 'toast', test2: 3 } }); - }); - - const testObjectStructure3 = Types.shape({ - test1: Types.shape({ - test2: Types.string(), - }), - test2: Types.string(), - }); - it('Objects containing objects should properly check if an object is provided', () => { - expect(validateShape(testObjectStructure3, { test1: 'foo', test2: 'bar' })).toEqual({ - test1: {}, - test2: 'bar', - }); - }); - - - const testObjectStructure4 = Types.shape({ - test2: Types.string(), - [Types.wildcardKey()]: Types.string(), - }); - const testObjectStructure5 = Types.shape({ - test2: Types.string(), - [Types.wildcardKey()]: Types.any(), - }); - it('Should, if a key is not specified, see if the key matches the wildcard type, and apply if true', () => { - expect(validateShape(testObjectStructure4, { test1: 'foo', test2: 'bar' })).toEqual({ - test1: 'foo', - test2: 'bar', - }); - - expect(validateShape(testObjectStructure5, { test1: 0, test2: 'bar' })).toEqual({ - test1: 0, - test2: 'bar', - }); - }); - - const testObjectStructure6 = Types.shape({ - test2: Types.string(), - [Types.wildcardKey()]: Types.string(), - }); - it('Should, if a key is not specified, and does not match the wildcardKey, strip it out', () => { - expect(validateShape(testObjectStructure6, { test1: 0, test2: 'bar' })).toEqual({ - test2: 'bar', - }); - }); - - const testObjectStructure7 = Types.shape({ - test1: Types.arrayOf(Types.string()), - }); - it('Should allow an empty array to be passed for an array property', () => { - expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({ - test1: [], - }); - }); - + const testObjectStructure2 = Types.shape({ + test1: testObjectStructure + }); + it("Objects should allow for arbitrary nesting of objects", () => { + expect( + validateShape(testObjectStructure2, { + test1: { test1: "toast", test2: 3 } + }) + ).toEqual({ test1: { test1: "toast", test2: 3 } }); }); - describe('Non covered types', () => { - it('A type with no associated validation should throw an error', () => { - expect(() => getTypeValidation('toast')).toThrowError(/validation/); - }); + const testObjectStructure3 = Types.shape({ + test1: Types.shape({ + test2: Types.string() + }), + test2: Types.string() }); - - describe('Has wildcard value', () => { - const testObjectStructure = Types.shape({ - test1: Types.string(), - test2: Types.number(), - [Types.wildcardKey()]: Types.any(), - }); - - const testObjectStructure2 = Types.shape({ - test1: Types.string(), - test2: Types.number(), - }); - - it('should return true if the objectStructure passed in has a wildcard key', () => { - expect(hasWildcardKey(testObjectStructure)).toBe(true); - }); - - it('should return false if no wildcard key passed in', () => { - expect(hasWildcardKey(testObjectStructure2)).toBe(false); - }); - }); - - describe('GetValueType', () => { - const testObjectStructure = Types.shape({ - test1: Types.string(), - test2: Types.number(), - [Types.wildcardKey()]: Types.number(), - }); - - const testObjectStructure2 = Types.shape({ - test1: Types.string(), - test2: Types.number(), - }); - - it('should return the correct type for a key that is present, if no wildcard present', () => { - expect(getValueType(testObjectStructure, 'test1', false)().type).toEqual(Types.string()().type); - }); - - it('should return the wildcard value if key not present and wildcard is', () => { - expect(getValueType(testObjectStructure, 'test3', true)().type).toEqual(Types.number()().type); - }); - - it('should return undefined if no wildcard or matching key', () => { - expect(getValueType(testObjectStructure, 'test3', false)).toEqual(undefined); + it("Objects containing objects should properly check if an object is provided", () => { + expect( + validateShape(testObjectStructure3, { test1: "foo", test2: "bar" }) + ).toEqual({ + test1: {}, + test2: "bar" }); }); + const testObjectStructure4 = Types.shape({ + test2: Types.string(), + [Types.wildcardKey()]: Types.string() + }); + const testObjectStructure5 = Types.shape({ + test2: Types.string(), + [Types.wildcardKey()]: Types.any() + }); + it("Should, if a key is not specified, see if the key matches the wildcard type, and apply if true", () => { + expect( + validateShape(testObjectStructure4, { test1: "foo", test2: "bar" }) + ).toEqual({ + test1: "foo", + test2: "bar" + }); + + expect( + validateShape(testObjectStructure5, { test1: 0, test2: "bar" }) + ).toEqual({ + test1: 0, + test2: "bar" + }); + }); + + const testObjectStructure6 = Types.shape({ + test2: Types.string(), + [Types.wildcardKey()]: Types.string() + }); + it("Should, if a key is not specified, and does not match the wildcardKey, strip it out", () => { + expect( + validateShape(testObjectStructure6, { test1: 0, test2: "bar" }) + ).toEqual({ + test2: "bar" + }); + }); + + const testObjectStructure7 = Types.shape({ + test1: Types.arrayOf(Types.string()) + }); + it("Should allow an empty array to be passed for an array property", () => { + expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({ + test1: [] + }); + }); + }); + + describe("Non covered types", () => { + it("A type with no associated validation should throw an error", () => { + expect(() => getTypeValidation("toast")).toThrowError(/validation/); + }); + }); + + describe("Has wildcard value", () => { + const testObjectStructure = Types.shape({ + test1: Types.string(), + test2: Types.number(), + [Types.wildcardKey()]: Types.any() + }); + + const testObjectStructure2 = Types.shape({ + test1: Types.string(), + test2: Types.number() + }); + + it("should return true if the objectStructure passed in has a wildcard key", () => { + expect(hasWildcardKey(testObjectStructure)).toBe(true); + }); + + it("should return false if no wildcard key passed in", () => { + expect(hasWildcardKey(testObjectStructure2)).toBe(false); + }); + }); + + describe("GetValueType", () => { + const testObjectStructure = Types.shape({ + test1: Types.string(), + test2: Types.number(), + [Types.wildcardKey()]: Types.number() + }); + + const testObjectStructure2 = Types.shape({ + test1: Types.string(), + test2: Types.number() + }); + + it("should return the correct type for a key that is present, if no wildcard present", () => { + expect(getValueType(testObjectStructure, "test1", false)().type).toEqual( + Types.string()().type + ); + }); + + it("should return the wildcard value if key not present and wildcard is", () => { + expect(getValueType(testObjectStructure, "test3", true)().type).toEqual( + Types.number()().type + ); + }); + + it("should return undefined if no wildcard or matching key", () => { + expect(getValueType(testObjectStructure, "test3", false)).toEqual( + undefined + ); + }); + }); }); diff --git a/src/buildStoreChunk.js b/src/buildStoreChunk.js index 0016b66..73cfb3b 100644 --- a/src/buildStoreChunk.js +++ b/src/buildStoreChunk.js @@ -2,127 +2,149 @@ //============================== // Flow imports //============================== -import type { StructureType, PrimitiveType } from './structure'; -import type { PartialStoreChunk } from './reducers'; +import type { StructureType, PrimitiveType } from "./structure"; +import type { PartialStoreChunk } from "./reducers"; //============================== // JS imports //============================== -import { combineReducers } from 'redux'; -import reduce from 'lodash/reduce'; -import find from 'lodash/find'; -import omit from 'lodash/omit'; -import isFunction from 'lodash/isFunction'; -import { createReducer } from './reducers'; -import { PROP_TYPES } from './structure'; +import { combineReducers } from "redux"; +import reduce from "lodash/reduce"; +import find from "lodash/find"; +import omit from "lodash/omit"; +import isFunction from "lodash/isFunction"; +import { createReducer } from "./reducers"; +import { PROP_TYPES } from "./structure"; // Build a chunk of the eventual store. The selectors and actions // generated will specifically operate on the store chunk generated. Selectors will be // relative to the baseSelector provided or, if not specified, the root of the store, using // the name of the chunk as the base property. -export function buildStoreChunk(name: string, structure: any, { +export function buildStoreChunk( + name: string, + structure: any, + { baseSelector = state => state[name], - locationString = name, -}: { + locationString = name + }: { baseSelector: any, - locationString: string, -} = {}): PartialStoreChunk { - if (!structure) throw new Error(`The structure must be defined for a reducer! LocationString: ${ locationString }`); - const initialMemo: PartialStoreChunk = { - reducers: { - [name]: {}, - }, - actions: {}, - selectors: {}, - baseSelector, - locationString, - name, - }; + locationString: string + } = {} +): PartialStoreChunk { + if (!structure) + throw new Error( + `The structure must be defined for a reducer! LocationString: ${locationString}` + ); + const initialMemo: PartialStoreChunk = { + reducers: { + [name]: {} + }, + actions: {}, + selectors: {}, + baseSelector, + locationString, + name + }; - //Build up the reducers, actions, and selectors for this level. Due to recursion, - //these objects will be assigned to a property in the parent object, or simply - //returned to the call site for use in the rest of the application. - const processedStructure = determineStructureProcessing(structure, initialMemo, name); + //Build up the reducers, actions, and selectors for this level. Due to recursion, + //these objects will be assigned to a property in the parent object, or simply + //returned to the call site for use in the rest of the application. + const processedStructure = determineStructureProcessing( + structure, + initialMemo, + name + ); - //If the location string is equal to the name passed to build store chunk, then we must be - //at the top level. If the structure is a function (i.e. not nested reducers) then return - //the actions, and selectors as the top level of their respective objects. - if (isFunction(structure)) { - return { - reducers: processedStructure.reducers, - actions: processedStructure.actions[name], - selectors: processedStructure.selectors[name], - }; - } - - return processedStructure; -} - - -export function determineStructureProcessing(structure: any, initialMemo: PartialStoreChunk, name) { - if (isFunction(structure)) return combineStoreChunkReducers(processStructure(initialMemo, structure, name)); - return combineStoreChunkReducers(reduce(structure, processStructure, initialMemo)); -} - - -export function combineStoreChunkReducers(processedStoreChunk: PartialStoreChunk) { - //The Redux 'combineReducers' helper function is used here to save a little bit of boilerplate. - //This helper, if you're not aware, ensures that the correct store properties are passed to the - //reducers assigned to those properties. - return { ...omit(processedStoreChunk, ['baseSelector', 'locationString', 'name']), reducers: { - [processedStoreChunk.name]: combineReducers(processedStoreChunk.reducers) - }}; -} - - -export function processStructure(memo: PartialStoreChunk, propValue: StructureType | PrimitiveType, propName: string) { - //Get the structure from the propValue. In the case of 'StructureType' properties, this - //will be some form of shape (or primitives in the case of arrays). At this point we - //are only interested in whether or not the structure contains reducers, as that - //has an impact on how we proceed with regards to calls. - const { structure: propStructure } = propValue(); - const containsReducers = !!find(propStructure, v => v().type === PROP_TYPES._reducer); - - //Create the child reducer. Depending on whether or not the current structure level contains - //child reducers, we will either recursively call reducerBuilder, or we will call the - //createReducer function, which will create the correct reducer for the given structure - //(which can be either object, array, or primitive). - let childReducer = containsReducers - ? buildStoreChunk(propName, propStructure, { - locationString: `${memo.locationString}.${propName}`, - baseSelector: (state: any) => memo.baseSelector(state)[propName], - }) - : createReducer(propValue, { - locationString: `${memo.locationString}.${propName}`, - name: propName, - }); - - //As the chunk is built up, we want to assign the reducers/actions created - //by the child to a location on the reducers/actions object which will match up - //to their location. Selectors are created at this level, as the child does not - //need to know where it is located within the grand scheme of things. + //If the location string is equal to the name passed to build store chunk, then we must be + //at the top level. If the structure is a function (i.e. not nested reducers) then return + //the actions, and selectors as the top level of their respective objects. + if (isFunction(structure)) { return { - ...memo, - reducers: { - ...memo.reducers, - ...childReducer.reducers - }, - actions: { - ...memo.actions, - [propName]: childReducer.actions, - }, - selectors: { - ...memo.selectors, - [propName]: containsReducers ? childReducer.selectors : state => memo.baseSelector(state)[propName], - }, + reducers: processedStructure.reducers, + actions: processedStructure.actions[name], + selectors: processedStructure.selectors[name] }; + } + + return processedStructure; } +export function determineStructureProcessing( + structure: any, + initialMemo: PartialStoreChunk, + name +) { + if (isFunction(structure)) + return combineStoreChunkReducers( + processStructure(initialMemo, structure, name) + ); + return combineStoreChunkReducers( + reduce(structure, processStructure, initialMemo) + ); +} +export function combineStoreChunkReducers( + processedStoreChunk: PartialStoreChunk +) { + //The Redux 'combineReducers' helper function is used here to save a little bit of boilerplate. + //This helper, if you're not aware, ensures that the correct store properties are passed to the + //reducers assigned to those properties. + return { + ...omit(processedStoreChunk, ["baseSelector", "locationString", "name"]), + reducers: { + [processedStoreChunk.name]: combineReducers(processedStoreChunk.reducers) + } + }; +} +export function processStructure( + memo: PartialStoreChunk, + propValue: StructureType | PrimitiveType, + propName: string +) { + //Get the structure from the propValue. In the case of 'StructureType' properties, this + //will be some form of shape (or primitives in the case of arrays). At this point we + //are only interested in whether or not the structure contains reducers, as that + //has an impact on how we proceed with regards to calls. + const { structure: propStructure } = propValue(); + const containsReducers = !!find( + propStructure, + v => v().type === PROP_TYPES._reducer + ); + //Create the child reducer. Depending on whether or not the current structure level contains + //child reducers, we will either recursively call reducerBuilder, or we will call the + //createReducer function, which will create the correct reducer for the given structure + //(which can be either object, array, or primitive). + let childReducer = containsReducers + ? buildStoreChunk(propName, propStructure, { + locationString: `${memo.locationString}.${propName}`, + baseSelector: (state: any) => memo.baseSelector(state)[propName] + }) + : createReducer(propValue, { + locationString: `${memo.locationString}.${propName}`, + name: propName + }); - - - - + //As the chunk is built up, we want to assign the reducers/actions created + //by the child to a location on the reducers/actions object which will match up + //to their location. Selectors are created at this level, as the child does not + //need to know where it is located within the grand scheme of things. + return { + ...memo, + reducers: { + ...memo.reducers, + ...childReducer.reducers + }, + actions: { + ...memo.actions, + [propName]: childReducer.actions + }, + selectors: { + ...memo.selectors, + [propName]: containsReducers + ? childReducer.selectors + : state => memo.baseSelector(state)[propName] + } + }; +} diff --git a/src/index.js b/src/index.js index 601155d..64efcac 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,5 @@ -import { Types } from './structure'; -import { buildStoreChunk } from './buildStoreChunk'; -import { createCombinedAction } from './reducers/batchUpdates'; +import { Types } from "./structure"; +import { buildStoreChunk } from "./buildStoreChunk"; +import { createCombinedAction } from "./reducers/batchUpdates"; -export { - Types, - buildStoreChunk, - createCombinedAction, -} +export { Types, buildStoreChunk, createCombinedAction }; diff --git a/src/reducers.js b/src/reducers.js index 6cc7f55..001a68b 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -3,125 +3,140 @@ // Flow imports //============================== import type { - StructureType, - PrimitiveType, - ReducerType, - PropTypeKeys, -} from './structure'; + StructureType, + PrimitiveType, + ReducerType, + PropTypeKeys +} from "./structure"; //============================== // Flow types //============================== export type PartialStoreChunk = { - reducers: { [key: string]: any }, - actions: { [key: string]: any }, - selectors: { [key: string]: any }, - locationString: string, - baseSelector: () => {}, - name: string, + reducers: { [key: string]: any }, + actions: { [key: string]: any }, + selectors: { [key: string]: any }, + locationString: string, + baseSelector: () => {}, + name: string }; export type Selector = (state: Object) => any; type CallReducerInterface = { - name: string, - reducerFn: () => {}, - reducerStructureDescriptor: StructureType | PrimitiveType, - locationString: string, + name: string, + reducerFn: () => {}, + reducerStructureDescriptor: StructureType | PrimitiveType, + locationString: string }; //============================== // JS imports //============================== -import { - PROP_TYPES, -} from './structure'; -import reduce from 'lodash/reduce'; -import flowRight from 'lodash/fp/flowRight'; -import { createShapeReducer } from './reducers/objectReducer'; -import { createArrayReducer } from './reducers/arrayReducer'; -import { createPrimitiveReducer } from './reducers/primitiveReducer'; +import { PROP_TYPES } from "./structure"; +import reduce from "lodash/reduce"; +import flowRight from "lodash/fp/flowRight"; +import { createShapeReducer } from "./reducers/objectReducer"; +import { createArrayReducer } from "./reducers/arrayReducer"; +import { createPrimitiveReducer } from "./reducers/primitiveReducer"; export const REDUCER_CREATOR_MAPPING: { [key: PropTypeKeys]: any } = { - [PROP_TYPES._shape]: createShapeReducer, - [PROP_TYPES._array]: createArrayReducer, - [PROP_TYPES._boolean]: createPrimitiveReducer, - [PROP_TYPES._string]: createPrimitiveReducer, - [PROP_TYPES._number]: createPrimitiveReducer, - [PROP_TYPES._any]: createPrimitiveReducer, - [PROP_TYPES._custom]: createPrimitiveReducer, + [PROP_TYPES._shape]: createShapeReducer, + [PROP_TYPES._array]: createArrayReducer, + [PROP_TYPES._boolean]: createPrimitiveReducer, + [PROP_TYPES._string]: createPrimitiveReducer, + [PROP_TYPES._number]: createPrimitiveReducer, + [PROP_TYPES._any]: createPrimitiveReducer, + [PROP_TYPES._custom]: createPrimitiveReducer }; +export const createUniqueString = Math.random() + .toString(36) + .substring(7); -export const createUniqueString = Math.random().toString(36).substring(7); - - -export function determineReducerType(reducerDescriptor: ReducerType, { +export function determineReducerType( + reducerDescriptor: ReducerType, + { name, locationString, - reducerCreatorMapping = REDUCER_CREATOR_MAPPING, -}: { + reducerCreatorMapping = REDUCER_CREATOR_MAPPING + }: { name: string, locationString: string, - reducerCreatorMapping: { [key: PropTypeKeys]: any }, -}): CallReducerInterface { + reducerCreatorMapping: { [key: PropTypeKeys]: any } + } +): CallReducerInterface { + const { structure } = reducerDescriptor(); + const { type } = structure(); - const { structure } = reducerDescriptor(); - const { type } = structure(); + if (!reducerCreatorMapping[type]) + throw new Error( + `Reducer type ${type} does not have a corresponding createReducer function` + ); - if (!reducerCreatorMapping[type]) throw new Error(`Reducer type ${type} does not have a corresponding createReducer function`); - - return { - name, - reducerFn: reducerCreatorMapping[type], - reducerStructureDescriptor: structure, - locationString, - }; + return { + name, + reducerFn: reducerCreatorMapping[type], + reducerStructureDescriptor: structure, + locationString + }; } - export function callReducer({ - name, - reducerFn, - reducerStructureDescriptor, - locationString + name, + reducerFn, + reducerStructureDescriptor, + locationString }: CallReducerInterface) { - return reducerFn(reducerStructureDescriptor, { - locationString, - name, - }); + return reducerFn(reducerStructureDescriptor, { + locationString, + name + }); } - -export function createReducerBehaviors(behaviorsConfig: { [key: string]: { reducer: () => {} } }, locationString: string): any { - //Take a reducer behavior config object, and create the reducer behaviors using the location string. - //This is necessary since all action types are effectively global when Redux processes an action - //(i.e. every reducer will be ran using the action object). Therefore we need to ensure that all - //actions only result in the specific reducer performing a change. Actions are also generated using - //the location string/name combination, so will match up 1:1. - return reduce(behaviorsConfig, (memo, behavior, name) => ({ - ...memo, - [`${locationString}.${name}`]: behavior, - }), {}); +export function createReducerBehaviors( + behaviorsConfig: { [key: string]: { reducer: () => {} } }, + locationString: string +): any { + //Take a reducer behavior config object, and create the reducer behaviors using the location string. + //This is necessary since all action types are effectively global when Redux processes an action + //(i.e. every reducer will be ran using the action object). Therefore we need to ensure that all + //actions only result in the specific reducer performing a change. Actions are also generated using + //the location string/name combination, so will match up 1:1. + return reduce( + behaviorsConfig, + (memo, behavior, name) => ({ + ...memo, + [`${locationString}.${name}`]: behavior + }), + {} + ); } +export function calculateDefaults( + typeDescription: StructureType | PrimitiveType +) { + //Using the structure of a type, calculate the default for that type. + //Types can take two forms; a 'StructureType' and a 'PrimitiveType'. The former + //can (and usually does) contain nested type descriptions, so we need to recurse + //through the definition until defaults are found, and build up the corresponding + //structure. + const { type, structure = {}, defaultValue = "" } = typeDescription(); + const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1; -export function calculateDefaults(typeDescription: StructureType | PrimitiveType) { - //Using the structure of a type, calculate the default for that type. - //Types can take two forms; a 'StructureType' and a 'PrimitiveType'. The former - //can (and usually does) contain nested type descriptions, so we need to recurse - //through the definition until defaults are found, and build up the corresponding - //structure. - const { type, structure = {}, defaultValue = '' } = typeDescription(); - const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1; + if (!complex) return defaultValue; - if (!complex) return defaultValue; - - return reduce(structure, (memo, propValue, propName) => ({ - ...memo, - [propName]: calculateDefaults(propValue), - }), {}); + return reduce( + structure, + (memo, propValue, propName) => ({ + ...memo, + [propName]: calculateDefaults(propValue) + }), + {} + ); } - -export const createReducer = flowRight(callReducer, determineReducerType); +export const createReducer = flowRight( + callReducer, + determineReducerType +); diff --git a/src/reducers/arrayReducer.js b/src/reducers/arrayReducer.js index ae8534d..d632424 100644 --- a/src/reducers/arrayReducer.js +++ b/src/reducers/arrayReducer.js @@ -2,65 +2,78 @@ //============================== // Flow imports //============================== -import type { ArrayStructureType } from '../structure'; +import type { ArrayStructureType } from "../structure"; //============================== // Flow types //============================== export type ArrayReducerAction = { - type: string, - payload: any, - index?: number, + type: string, + payload: any, + index?: number }; -export type ArrayReducer = (state: Array, action: ArrayReducerAction) => Array; -export type ArrayReducerBehavior = (state: Array, payload: any, initialState: Array, index: number) => Array; +export type ArrayReducer = ( + state: Array, + action: ArrayReducerAction +) => Array; +export type ArrayReducerBehavior = ( + state: Array, + payload: any, + initialState: Array, + index: number +) => Array; export type ArrayReducerBehaviorsConfig = { - [key: string]: { - action?: (value: any) => any, - reducer: ArrayReducerBehavior, - validate: boolean, - } + [key: string]: { + action?: (value: any) => any, + reducer: ArrayReducerBehavior, + validate: boolean + } }; export type ArrayReducerBehaviors = { - [key: string]: ArrayReducerBehavior, + [key: string]: ArrayReducerBehavior }; -export type ArrayAction = (value: Array) => { type: string, payload: any, index?: number }; +export type ArrayAction = ( + value: Array +) => { type: string, payload: any, index?: number }; export type ArrayActions = { - [key: string]: Array + [key: string]: Array }; export type ArrayReducerOptions = { - behaviorsConfig: ArrayReducerBehaviorsConfig, - locationString: string, - name: string, + behaviorsConfig: ArrayReducerBehaviorsConfig, + locationString: string, + name: string }; export type ArraySelector = (state: Object) => Array; //============================== // JS imports //============================== -import isArray from 'lodash/isArray'; -import isNumber from 'lodash/isNumber'; -import { validateArray, validateShape, validateValue } from '../validatePayload'; -import { createReducerBehaviors } from '../reducers'; -import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils'; -import { PROP_TYPES } from '../structure'; +import isArray from "lodash/isArray"; +import isNumber from "lodash/isNumber"; import { - isCombinedAction, - getApplicableCombinedActions -} from './batchUpdates'; + validateArray, + validateShape, + validateValue +} from "../validatePayload"; +import { createReducerBehaviors } from "../reducers"; +import { updateAtIndex, removeAtIndex } from "../utils/arrayUtils"; +import { PROP_TYPES } from "../structure"; +import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates"; -const reduce = require('lodash/fp/reduce').convert({ cap: false }); +const reduce = require("lodash/fp/reduce").convert({ cap: false }); - -function checkIndex(index: ?number, payload: any = '', behaviorName: string): boolean { - if (!isNumber(index) || index === -1) { - console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`); - return false; - } - return true; +function checkIndex( + index: ?number, + payload: any = "", + behaviorName: string +): boolean { + if (!isNumber(index) || index === -1) { + console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`); + return false; + } + return true; } - //============================== // Array behaviors // ---------------- @@ -71,144 +84,184 @@ function checkIndex(index: ?number, payload: any = '', behaviorName: string): bo // to create a few helper behaviors to aid with the most common array operations. //============================== export const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = { - //Index specific behaviors. - replaceAtIndex: { - reducer(state, payload, initialState, index) { - if (!checkIndex(index, payload, 'updateAtIndex')) return state; - if (payload === undefined) return console.warn('Undefined was passed when updating index. Update not performed') || state; - return updateAtIndex(state, payload, index); - }, - validate: true, + //Index specific behaviors. + replaceAtIndex: { + reducer(state, payload, initialState, index) { + if (!checkIndex(index, payload, "updateAtIndex")) return state; + if (payload === undefined) + return ( + console.warn( + "Undefined was passed when updating index. Update not performed" + ) || state + ); + return updateAtIndex(state, payload, index); }, - resetAtIndex: { - reducer(state, payload, initialState, index) { - if (!checkIndex(index, payload, 'resetAtIndex')) return state; - return updateAtIndex(state, initialState, index); - }, - validate: false, + validate: true + }, + resetAtIndex: { + reducer(state, payload, initialState, index) { + if (!checkIndex(index, payload, "resetAtIndex")) return state; + return updateAtIndex(state, initialState, index); }, - removeAtIndex: { - reducer(state, index) { - if (!checkIndex(index, '', 'removeAtIndex')) return state; - return removeAtIndex(state, index); - }, - validate: false, + validate: false + }, + removeAtIndex: { + reducer(state, index) { + if (!checkIndex(index, "", "removeAtIndex")) return state; + return removeAtIndex(state, index); }, - //Whole array behaviors. - replace: { - reducer(state, payload) { - if(!isArray(payload)) return console.warn('An array must be provided when replacing an array') || state; - return payload; - }, - validate: true, + validate: false + }, + //Whole array behaviors. + replace: { + reducer(state, payload) { + if (!isArray(payload)) + return ( + console.warn("An array must be provided when replacing an array") || + state + ); + return payload; }, - reset: { - reducer(state, payload, initialState) { - return initialState; - }, - validate: false, + validate: true + }, + reset: { + reducer(state, payload, initialState) { + return initialState; }, - push: { - reducer(state, payload) { - return [...state, payload]; - }, - validate: true, + validate: false + }, + push: { + reducer(state, payload) { + return [...state, payload]; }, - pop: { - reducer(state) { - return state.slice(0, -1); - }, - validate: false, + validate: true + }, + pop: { + reducer(state) { + return state.slice(0, -1); }, - unshift: { - reducer(state, payload) { - return [payload, ...state]; - }, - validate: true, + validate: false + }, + unshift: { + reducer(state, payload) { + return [payload, ...state]; }, - shift: { - reducer(state) { - return state.slice(1); - }, - validate: false, - } + validate: true + }, + shift: { + reducer(state) { + return state.slice(1); + }, + validate: false + } }; - -export function createArrayReducer(arrayTypeDescription: ArrayStructureType, { - locationString, - name, -}: ArrayReducerOptions) { - const uniqueId = Math.random().toString(36).substring(5); - return { - reducers: { - [name]: createReducer(arrayTypeDescription, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, `${uniqueId}-${locationString}`)) - }, - actions: createActions(DEFAULT_ARRAY_BEHAVIORS, `${uniqueId}-${locationString}`, {}), - }; +export function createArrayReducer( + arrayTypeDescription: ArrayStructureType, + { locationString, name }: ArrayReducerOptions +) { + const uniqueId = Math.random() + .toString(36) + .substring(5); + return { + reducers: { + [name]: createReducer( + arrayTypeDescription, + createReducerBehaviors( + DEFAULT_ARRAY_BEHAVIORS, + `${uniqueId}-${locationString}` + ) + ) + }, + actions: createActions( + DEFAULT_ARRAY_BEHAVIORS, + `${uniqueId}-${locationString}`, + {} + ) + }; } +export function createReducer( + arrayTypeDescription: ArrayStructureType, + behaviors: ArrayReducerBehaviors +): ArrayReducer { + //Take the initial value specified as the default for the array, then apply it, using the validation + //when doing so. The initial value must be an array. + const initialValue = validateArray( + arrayTypeDescription, + arrayTypeDescription().defaultValue + ); -export function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: ArrayReducerBehaviors): ArrayReducer { - //Take the initial value specified as the default for the array, then apply it, using the validation - //when doing so. The initial value must be an array. - const initialValue = validateArray(arrayTypeDescription, arrayTypeDescription().defaultValue); + //Return the array reducer. + return ( + state: Array = initialValue, + { type, payload, index }: ArrayReducerAction + ) => { + const matchedBehaviors = behaviors[type] + ? [{ type, payload }] + : isCombinedAction(type) + ? getApplicableCombinedActions(behaviors)(payload) + : []; - //Return the array reducer. - return (state: Array = initialValue, { type, payload, index }: ArrayReducerAction) => { - const matchedBehaviors = behaviors[type] - ? [{ type, payload }] - : isCombinedAction(type) - ? getApplicableCombinedActions(behaviors)(payload) - : []; - - if (matchedBehaviors.length) { - //Call every behaviour relevant to this reducer as part of this action call, - //and merge the result (later actions take priority). - //Sanitize the payload using the reducer shape, then apply the sanitized - //payload to the state using the behavior linked to this action type. - return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer( - interimState, - behaviors[matchedBehavior.type].validate - ? applyValidation(arrayTypeDescription, matchedBehavior.payload) - : matchedBehavior.payload, - initialValue, - index, - ), state)(matchedBehaviors); - } - - //If the action type does not match any of the specified behaviors, just return the current state. - return state; + if (matchedBehaviors.length) { + //Call every behaviour relevant to this reducer as part of this action call, + //and merge the result (later actions take priority). + //Sanitize the payload using the reducer shape, then apply the sanitized + //payload to the state using the behavior linked to this action type. + return reduce( + (interimState, matchedBehavior) => + behaviors[matchedBehavior.type].reducer( + interimState, + behaviors[matchedBehavior.type].validate + ? applyValidation(arrayTypeDescription, matchedBehavior.payload) + : matchedBehavior.payload, + initialValue, + index + ), + state + )(matchedBehaviors); } + + //If the action type does not match any of the specified behaviors, just return the current state. + return state; + }; } +export function applyValidation( + arrayTypeDescription: ArrayStructureType, + payload: any +) { + // Array validation is more tricky than object/primitive, as it is possible that the current + // action may involve updating the contents of a specific array element, rather than the + // whole array. As a result, some extra functionality is required to determine which + // form of validation to apply. -export function applyValidation(arrayTypeDescription: ArrayStructureType, payload: any) { - // Array validation is more tricky than object/primitive, as it is possible that the current - // action may involve updating the contents of a specific array element, rather than the - // whole array. As a result, some extra functionality is required to determine which - // form of validation to apply. + // First case is simple - if the action payload is an array, then we simply validate it against + // the structure of this reducer. + if (isArray(payload)) return validateArray(arrayTypeDescription, payload); - // First case is simple - if the action payload is an array, then we simply validate it against - // the structure of this reducer. - if (isArray(payload)) return validateArray(arrayTypeDescription, payload); - - // If a non-array payload has been passed in, then we need to check which form of validation - // to use, by checking the structure of the array. - const { structure } = arrayTypeDescription(); - if (structure().type === PROP_TYPES._shape) return validateShape(structure, payload); - return validateValue(structure, payload); + // If a non-array payload has been passed in, then we need to check which form of validation + // to use, by checking the structure of the array. + const { structure } = arrayTypeDescription(); + if (structure().type === PROP_TYPES._shape) + return validateShape(structure, payload); + return validateValue(structure, payload); } - -function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string): ArrayActions { - //Take a reducer behavior config object, and create actions using the location string - return reduce((memo, behavior, name) => ({ - ...memo, - [name]: (payload: Array, index: ?number) => ({ - type: `${locationString}.${name}`, - payload: (behavior.action || (payload => payload))(payload), - index, - }) - }), {})(behaviorsConfig); +function createActions( + behaviorsConfig: ArrayReducerBehaviorsConfig, + locationString: string +): ArrayActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce( + (memo, behavior, name) => ({ + ...memo, + [name]: (payload: Array, index: ?number) => ({ + type: `${locationString}.${name}`, + payload: (behavior.action || (payload => payload))(payload), + index + }) + }), + {} + )(behaviorsConfig); } diff --git a/src/reducers/batchUpdates.js b/src/reducers/batchUpdates.js index c4e1701..bc1fc06 100644 --- a/src/reducers/batchUpdates.js +++ b/src/reducers/batchUpdates.js @@ -1,36 +1,36 @@ //@flow -import keys from 'lodash/fp/keys'; -import includes from 'lodash/fp/includes'; -import filter from 'lodash/fp/filter'; - -export const COMBINED_ACTION = '/@@redux-scc-combined-action'; +import keys from "lodash/fp/keys"; +import includes from "lodash/fp/includes"; +import filter from "lodash/fp/filter"; +export const COMBINED_ACTION = "/@@redux-scc-combined-action"; type BatchUpdateInterface = { name?: string, - actions: Array<{ type: string, payload: any, meta?: any }>, + actions: Array<{ type: string, payload: any, meta?: any }> }; type Behaviors = { [key: string]: { [key: string]: { - reducer: (state: mixed, payload: mixed | void, initialState: mixed) => mixed, - }, - }; -} + reducer: ( + state: mixed, + payload: mixed | void, + initialState: mixed + ) => mixed + } + } +}; export const createCombinedAction = ({ - name = '', - actions, + name = "", + actions }: BatchUpdateInterface) => ({ - type: `${ name }${ COMBINED_ACTION }`, - payload: actions, + type: `${name}${COMBINED_ACTION}`, + payload: actions }); - -export const isCombinedAction = (actionType: string) => actionType - ? actionType.indexOf(COMBINED_ACTION) > -1 - : false; - +export const isCombinedAction = (actionType: string) => + actionType ? actionType.indexOf(COMBINED_ACTION) > -1 : false; export const getApplicableCombinedActions = (behaviors: Behaviors) => - filter(({ type }) => includes(type)(keys(behaviors))); \ No newline at end of file + filter(({ type }) => includes(type)(keys(behaviors))); diff --git a/src/reducers/objectReducer.js b/src/reducers/objectReducer.js index 9d589a9..0d2ebbe 100644 --- a/src/reducers/objectReducer.js +++ b/src/reducers/objectReducer.js @@ -2,53 +2,55 @@ //============================== // Flow imports //============================== -import type { StructureType } from '../structure'; - +import type { StructureType } from "../structure"; //============================== // Flow types //============================== export type ShapeReducerAction = { - type: string, - payload: Object, - validate: boolean, + type: string, + payload: Object, + validate: boolean }; -export type ShapeReducer = (state: Object, action: ShapeReducerAction) => Object; -export type ShapeReducerBehavior = (state: {}, payload: Object | void, initialState: {}) => Object; +export type ShapeReducer = ( + state: Object, + action: ShapeReducerAction +) => Object; +export type ShapeReducerBehavior = ( + state: {}, + payload: Object | void, + initialState: {} +) => Object; export type ShapeReducerBehaviorsConfig = { - [key: string]: { - action?: (value: Object) => Object, - reducer: ShapeReducerBehavior, - } + [key: string]: { + action?: (value: Object) => Object, + reducer: ShapeReducerBehavior + } }; export type ShapeReducerBehaviors = { - [key: string]: ShapeReducerBehavior, + [key: string]: ShapeReducerBehavior }; export type ShapeAction = (value: Object) => { type: string, payload: Object }; export type ShapeActions = { - [key: string]: ShapeAction + [key: string]: ShapeAction }; export type ShapeReducerOptions = { - behaviorsConfig: ShapeReducerBehaviorsConfig, - locationString: string, - name: string, + behaviorsConfig: ShapeReducerBehaviorsConfig, + locationString: string, + name: string }; - //============================== // JS imports //============================== -import isObject from 'lodash/isObject'; -import omit from 'lodash/omit'; -import { validateShape } from '../validatePayload'; -import { createReducerBehaviors } from '../reducers'; -import { PROP_TYPES } from '../structure'; -import { - isCombinedAction, - getApplicableCombinedActions -} from './batchUpdates'; +import isObject from "lodash/isObject"; +import omit from "lodash/omit"; +import { validateShape } from "../validatePayload"; +import { createReducerBehaviors } from "../reducers"; +import { PROP_TYPES } from "../structure"; +import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates"; -const reduce = require('lodash/fp/reduce').convert({ cap: false }); +const reduce = require("lodash/fp/reduce").convert({ cap: false }); //============================== // Shape behaviors @@ -58,93 +60,118 @@ const reduce = require('lodash/fp/reduce').convert({ cap: false }); // behavior, which still replaces the previous state with the payload. //============================== export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = { - update: { - reducer(state, payload) { - if (!isObject(payload)) return state; - return { ...state, ...payload }; - }, - validate: true, + update: { + reducer(state, payload) { + if (!isObject(payload)) return state; + return { ...state, ...payload }; }, - reset: { - reducer(state, payload, initialState) { - if (!isObject(payload)) return initialState; - return { ...initialState, ...payload }; - }, - validate: false, + validate: true + }, + reset: { + reducer(state, payload, initialState) { + if (!isObject(payload)) return initialState; + return { ...initialState, ...payload }; }, - replace: { - reducer(state, payload, initialState) { - if (!payload) return state; - return { - ...initialState, - ...payload, - }; - }, - validate: true, - } + validate: false + }, + replace: { + reducer(state, payload, initialState) { + if (!payload) return state; + return { + ...initialState, + ...payload + }; + }, + validate: true + } }; - -export function createShapeReducer(reducerShape: StructureType, { - locationString, - name, -}: ShapeReducerOptions) { - const uniqueId = Math.random().toString(36).substring(5); - return { - reducers: { - [name]: createReducer(reducerShape, createReducerBehaviors(DEFAULT_SHAPE_BEHAVIORS, `${uniqueId}-${locationString}`)), - }, - actions: createActions(DEFAULT_SHAPE_BEHAVIORS, `${uniqueId}-${locationString}`), - }; +export function createShapeReducer( + reducerShape: StructureType, + { locationString, name }: ShapeReducerOptions +) { + const uniqueId = Math.random() + .toString(36) + .substring(5); + return { + reducers: { + [name]: createReducer( + reducerShape, + createReducerBehaviors( + DEFAULT_SHAPE_BEHAVIORS, + `${uniqueId}-${locationString}` + ) + ) + }, + actions: createActions( + DEFAULT_SHAPE_BEHAVIORS, + `${uniqueId}-${locationString}` + ) + }; } - export function calculateDefaults(reducerStructure: any) { - return reduce((memo, propValue, propName) => ({ - ...memo, - [propName]: propValue().type === PROP_TYPES._shape - ? calculateDefaults(propValue().structure) - : propValue().defaultValue, - }), {})(omit(reducerStructure, ['_wildcardKey'])); + return reduce( + (memo, propValue, propName) => ({ + ...memo, + [propName]: + propValue().type === PROP_TYPES._shape + ? calculateDefaults(propValue().structure) + : propValue().defaultValue + }), + {} + )(omit(reducerStructure, ["_wildcardKey"])); } +export function createReducer( + objectStructure: StructureType, + behaviors: ShapeReducerBehaviors +): ShapeReducer { + const initialState: Object = validateShape( + objectStructure, + calculateDefaults(objectStructure().structure) + ); + return (state = initialState, { type, payload }: ShapeReducerAction) => { + //If the action type does not match any of the specified behaviors, just return the current state. + const matchedBehaviors = behaviors[type] + ? [{ type, payload }] + : isCombinedAction(type) + ? getApplicableCombinedActions(behaviors)(payload) + : []; -export function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBehaviors): ShapeReducer { - const initialState: Object = validateShape(objectStructure, calculateDefaults(objectStructure().structure)); - return (state = initialState, { type, payload }: ShapeReducerAction) => { - //If the action type does not match any of the specified behaviors, just return the current state. - const matchedBehaviors = behaviors[type] - ? [{ type, payload }] - : isCombinedAction(type) - ? getApplicableCombinedActions(behaviors)(payload) - : []; - - if (matchedBehaviors.length) { - //Sanitize the payload using the reducer shape, then apply the sanitized - //payload to the state using the behavior linked to this action type. - return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer( - interimState, - behaviors[matchedBehavior.type].validate - ? validateShape(objectStructure, matchedBehavior.payload) - : matchedBehavior.payload, - initialState, - ), state)(matchedBehaviors); - } - - return state; + if (matchedBehaviors.length) { + //Sanitize the payload using the reducer shape, then apply the sanitized + //payload to the state using the behavior linked to this action type. + return reduce( + (interimState, matchedBehavior) => + behaviors[matchedBehavior.type].reducer( + interimState, + behaviors[matchedBehavior.type].validate + ? validateShape(objectStructure, matchedBehavior.payload) + : matchedBehavior.payload, + initialState + ), + state + )(matchedBehaviors); } + + return state; + }; } - -function createActions(behaviorsConfig: ShapeReducerBehaviorsConfig, locationString: string): ShapeActions { - //Take a reducer behavior config object, and create actions using the location string - return reduce((memo, behavior, name) => ({ - ...memo, - [name]: (payload: Object) => ({ - type: `${locationString}.${name}`, - payload: (behavior.action || (payload => payload))(payload), - }) - }), {})(behaviorsConfig); +function createActions( + behaviorsConfig: ShapeReducerBehaviorsConfig, + locationString: string +): ShapeActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce( + (memo, behavior, name) => ({ + ...memo, + [name]: (payload: Object) => ({ + type: `${locationString}.${name}`, + payload: (behavior.action || (payload => payload))(payload) + }) + }), + {} + )(behaviorsConfig); } - - diff --git a/src/reducers/primitiveReducer.js b/src/reducers/primitiveReducer.js index 5dd1859..501df6a 100644 --- a/src/reducers/primitiveReducer.js +++ b/src/reducers/primitiveReducer.js @@ -2,49 +2,54 @@ //============================== // Flow imports //============================== -import type { PrimitiveType } from '../structure'; +import type { PrimitiveType } from "../structure"; //============================== // Flow types //============================== export type PrimitiveReducerAction = { - type: string, - payload: mixed, + type: string, + payload: mixed }; -export type PrimitiveReducer = (state: mixed, action: PrimitiveReducerAction) => mixed; -export type PrimitiveReducerBehavior = (state: mixed, payload: mixed | void, initialState: mixed) => mixed; +export type PrimitiveReducer = ( + state: mixed, + action: PrimitiveReducerAction +) => mixed; +export type PrimitiveReducerBehavior = ( + state: mixed, + payload: mixed | void, + initialState: mixed +) => mixed; export type PrimitiveReducerBehaviorsConfig = { - [key: string]: { - action?: (value: mixed) => mixed, - reducer: PrimitiveReducerBehavior, - validate: boolean, - } + [key: string]: { + action?: (value: mixed) => mixed, + reducer: PrimitiveReducerBehavior, + validate: boolean + } }; export type PrimitiveReducerBehaviors = { - [key: string]: PrimitiveReducerBehavior, + [key: string]: PrimitiveReducerBehavior }; -export type PrimitiveAction = (value: mixed) => { type: string, payload: mixed }; +export type PrimitiveAction = ( + value: mixed +) => { type: string, payload: mixed }; export type PrimitiveActions = { - [key: string]: PrimitiveAction + [key: string]: PrimitiveAction }; export type PrimitiveReducerOptions = { - behaviorsConfig: PrimitiveReducerBehaviorsConfig, - locationString: string, - name: string, + behaviorsConfig: PrimitiveReducerBehaviorsConfig, + locationString: string, + name: string }; //============================== // JS imports //============================== -import { validateValue } from '../validatePayload'; -import { createReducerBehaviors } from '../reducers'; -import { - isCombinedAction, - getApplicableCombinedActions -} from './batchUpdates'; - -const reduce = require('lodash/fp/reduce').convert({ cap: false }); +import { validateValue } from "../validatePayload"; +import { createReducerBehaviors } from "../reducers"; +import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates"; +const reduce = require("lodash/fp/reduce").convert({ cap: false }); //============================== // Primitive behaviors @@ -54,76 +59,97 @@ const reduce = require('lodash/fp/reduce').convert({ cap: false }); //============================== export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = { - replace: { - reducer(state, payload) { - if (payload === undefined) return state; - return payload; - }, - validate: true, + replace: { + reducer(state, payload) { + if (payload === undefined) return state; + return payload; }, - reset: { - reducer(state, payload, initialState) { - return initialState; - }, - validate: false, + validate: true + }, + reset: { + reducer(state, payload, initialState) { + return initialState; }, + validate: false + } }; - -export function createPrimitiveReducer(primitiveType: PrimitiveType, { - locationString, - name, -}: PrimitiveReducerOptions) { - const uniqueId = Math.random().toString(36).substring(5); - return { - reducers: { - [name]: createReducer(primitiveType, createReducerBehaviors(DEFAULT_PRIMITIVE_BEHAVIORS, `${uniqueId}-${locationString}`)), - }, - actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, `${uniqueId}-${locationString}`), - }; +export function createPrimitiveReducer( + primitiveType: PrimitiveType, + { locationString, name }: PrimitiveReducerOptions +) { + const uniqueId = Math.random() + .toString(36) + .substring(5); + return { + reducers: { + [name]: createReducer( + primitiveType, + createReducerBehaviors( + DEFAULT_PRIMITIVE_BEHAVIORS, + `${uniqueId}-${locationString}` + ) + ) + }, + actions: createActions( + DEFAULT_PRIMITIVE_BEHAVIORS, + `${uniqueId}-${locationString}` + ) + }; } +function createReducer( + primitiveType: PrimitiveType, + behaviors: PrimitiveReducerBehaviors +): PrimitiveReducer { + //Calculate and validate the initial state of the reducer + const initialState: mixed = validateValue( + primitiveType, + primitiveType().defaultValue + ); + return (state = initialState, { type, payload }: PrimitiveReducerAction) => { + //If the action type does not match any of the specified behaviors, just return the current state. + const matchedBehaviors = behaviors[type] + ? [{ type, payload }] + : isCombinedAction(type) + ? getApplicableCombinedActions(behaviors)(payload) + : []; -function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducerBehaviors): PrimitiveReducer { - //Calculate and validate the initial state of the reducer - const initialState: mixed = validateValue(primitiveType, primitiveType().defaultValue); - return (state = initialState, { type, payload }: PrimitiveReducerAction) => { - //If the action type does not match any of the specified behaviors, just return the current state. - const matchedBehaviors = behaviors[type] - ? [{ type, payload }] - : isCombinedAction(type) - ? getApplicableCombinedActions(behaviors)(payload) - : []; - - if (matchedBehaviors.length) { - //Call every behaviour relevant to this reducer as part of this action call, - //and merge the result (later actions take priority). - //Sanitize the payload using the reducer shape, then apply the sanitized - //payload to the state using the behavior linked to this action type. - return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer( - interimState, - behaviors[matchedBehavior.type].validate - ? validateValue(primitiveType, matchedBehavior.payload) - : matchedBehavior.payload, - initialState - ), state)(matchedBehaviors); - } - - return state; + if (matchedBehaviors.length) { + //Call every behaviour relevant to this reducer as part of this action call, + //and merge the result (later actions take priority). + //Sanitize the payload using the reducer shape, then apply the sanitized + //payload to the state using the behavior linked to this action type. + return reduce( + (interimState, matchedBehavior) => + behaviors[matchedBehavior.type].reducer( + interimState, + behaviors[matchedBehavior.type].validate + ? validateValue(primitiveType, matchedBehavior.payload) + : matchedBehavior.payload, + initialState + ), + state + )(matchedBehaviors); } + + return state; + }; } - -function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string): PrimitiveActions { - //Take a reducer behavior config object, and create actions using the location string - return reduce((memo, behavior, name) => ({ - ...memo, - [name]: (payload: mixed) => ({ - type: `${locationString}.${name}`, - payload: (behavior.action || (payload => payload))(payload), - }) - }), {})(behaviorsConfig); +function createActions( + behaviorsConfig: PrimitiveReducerBehaviorsConfig, + locationString: string +): PrimitiveActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce( + (memo, behavior, name) => ({ + ...memo, + [name]: (payload: mixed) => ({ + type: `${locationString}.${name}`, + payload: (behavior.action || (payload => payload))(payload) + }) + }), + {} + )(behaviorsConfig); } - - - diff --git a/src/structure.js b/src/structure.js index 4859bbf..71f54d9 100644 --- a/src/structure.js +++ b/src/structure.js @@ -6,94 +6,98 @@ export type PropTypeKeys = $Keys; export type ShapeStructure = { - [key: string]: StructureType | PrimitiveType | ArrayStructureType, -} + [key: string]: StructureType | PrimitiveType | ArrayStructureType +}; export type StructureType = () => { - type: PropTypeKeys, - structure: ShapeStructure | StructureType | PrimitiveType, - defaultValue?: any, + type: PropTypeKeys, + structure: ShapeStructure | StructureType | PrimitiveType, + defaultValue?: any }; export type ReducerType = () => { - type: PropTypeKeys, - structure: StructureType | PrimitiveType, + type: PropTypeKeys, + structure: StructureType | PrimitiveType }; export type ArrayStructureType = () => { - type: PropTypeKeys, - structure: StructureType | PrimitiveType, - defaultValue: any, -} + type: PropTypeKeys, + structure: StructureType | PrimitiveType, + defaultValue: any +}; export type PrimitiveType = () => { - type: PropTypeKeys, - defaultValue?: any, - typeofValue: string, - structure?: PrimitiveType, + type: PropTypeKeys, + defaultValue?: any, + typeofValue: string, + structure?: PrimitiveType }; export type TypesObject = { - [key: string]: any, + [key: string]: any }; //============================== // Structure //============================== export const PROP_TYPES = { - _string: '_string', - _number: '_number', - _boolean: '_boolean', - _reducer: '_reducer', - _shape: '_shape', - _array: '_array', - _any: '_any', - _wildcardKey: '_wildcardKey', - _custom: '_custom', + _string: "_string", + _number: "_number", + _boolean: "_boolean", + _reducer: "_reducer", + _shape: "_shape", + _array: "_array", + _any: "_any", + _wildcardKey: "_wildcardKey", + _custom: "_custom" }; //The types objects are used in order to build up the structure of a store chunk, and provide/accept //default values whilst doing so. export const Types: TypesObject = { - string: (defaultValue: string = '') => () => ({ - type: PROP_TYPES._string, - defaultValue, - typeofValue: 'string', - }), - number: (defaultValue: number = 0) => () => ({ - type: PROP_TYPES._number, - defaultValue, - typeofValue: 'number', - }), - boolean: (defaultValue: boolean = false) => () => ({ - type: PROP_TYPES._boolean, - defaultValue, - typeofValue: 'boolean', - }), - any: (defaultValue: any = null) => () => ({ - type: PROP_TYPES._any, - defaultValue, - typeofValue: 'any', - }), - arrayOf: (structure: StructureType | PrimitiveType, defaultValue = []) => () => ({ - type: PROP_TYPES._array, - structure, - defaultValue, - }), - reducer: (structure: ShapeStructure) => () => ({ - type: PROP_TYPES._reducer, - structure, - }), - shape: (structure: ShapeStructure) => () => ({ - type: PROP_TYPES._shape, - structure, - }), - custom: ({ - validator = () => true, - validationErrorMessage = (value: any) => `${ value } failed custom type validation`, - }: { - validator: (value: any) => boolean, - validationErrorMessage: (value: any) => string, - } = {}) => (defaultValue: any) => () => ({ - type: PROP_TYPES._custom, - defaultValue, - validator, - validationErrorMessage, - }), - wildcardKey: () => PROP_TYPES._wildcardKey, + string: (defaultValue: string = "") => () => ({ + type: PROP_TYPES._string, + defaultValue, + typeofValue: "string" + }), + number: (defaultValue: number = 0) => () => ({ + type: PROP_TYPES._number, + defaultValue, + typeofValue: "number" + }), + boolean: (defaultValue: boolean = false) => () => ({ + type: PROP_TYPES._boolean, + defaultValue, + typeofValue: "boolean" + }), + any: (defaultValue: any = null) => () => ({ + type: PROP_TYPES._any, + defaultValue, + typeofValue: "any" + }), + arrayOf: ( + structure: StructureType | PrimitiveType, + defaultValue = [] + ) => () => ({ + type: PROP_TYPES._array, + structure, + defaultValue + }), + reducer: (structure: ShapeStructure) => () => ({ + type: PROP_TYPES._reducer, + structure + }), + shape: (structure: ShapeStructure) => () => ({ + type: PROP_TYPES._shape, + structure + }), + custom: ({ + validator = () => true, + validationErrorMessage = (value: any) => + `${value} failed custom type validation` + }: { + validator: (value: any) => boolean, + validationErrorMessage: (value: any) => string + } = {}) => (defaultValue: any) => () => ({ + type: PROP_TYPES._custom, + defaultValue, + validator, + validationErrorMessage + }), + wildcardKey: () => PROP_TYPES._wildcardKey }; diff --git a/src/utils/arrayUtils.js b/src/utils/arrayUtils.js index 8b0d5a4..4b8a7a6 100644 --- a/src/utils/arrayUtils.js +++ b/src/utils/arrayUtils.js @@ -1,20 +1,24 @@ //@flow -export function updateAtIndex(array: Array, value: any, index: number): Array { - if (typeof index !== 'number') throw new Error('Must provide a numeric index to updateAtIndex'); - if (index < 0 || index > array.length - 1) throw new Error(`The index ${index} is out of range for the array provided`); - return [ - ...array.slice(0,index), - value, - ...array.slice(index + 1), - ]; +export function updateAtIndex( + array: Array, + value: any, + index: number +): Array { + if (typeof index !== "number") + throw new Error("Must provide a numeric index to updateAtIndex"); + if (index < 0 || index > array.length - 1) + throw new Error( + `The index ${index} is out of range for the array provided` + ); + return [...array.slice(0, index), value, ...array.slice(index + 1)]; } - export function removeAtIndex(array: Array, index: number): Array { - if (typeof index !== 'number') throw new Error('Must provide a numeric index to removeAtIndex'); - if (index < 0 || index > array.length - 1) throw new Error(`The index ${index} is out of range for the array provided`); - return [ - ...array.slice(0, index), - ...array.slice(index + 1), - ]; + if (typeof index !== "number") + throw new Error("Must provide a numeric index to removeAtIndex"); + if (index < 0 || index > array.length - 1) + throw new Error( + `The index ${index} is out of range for the array provided` + ); + return [...array.slice(0, index), ...array.slice(index + 1)]; } diff --git a/src/validatePayload.js b/src/validatePayload.js index 72fe405..dec8f03 100644 --- a/src/validatePayload.js +++ b/src/validatePayload.js @@ -2,106 +2,144 @@ //============================== // Flow imports and types //============================== -import type { PrimitiveType, StructureType, ShapeStructure } from './structure'; +import type { PrimitiveType, StructureType, ShapeStructure } from "./structure"; -type validationFunction = (structure: StructureType | PrimitiveType | ShapeStructure, value: any) => any; +type validationFunction = ( + structure: StructureType | PrimitiveType | ShapeStructure, + value: any +) => any; //============================== // JS imports //============================== -import reduce from 'lodash/reduce'; -import isObject from 'lodash/isObject'; -import { PROP_TYPES } from './structure'; -const find = require('lodash/fp/find').convert({ cap: false }); - +import reduce from "lodash/reduce"; +import isObject from "lodash/isObject"; +import { PROP_TYPES } from "./structure"; +const find = require("lodash/fp/find").convert({ cap: false }); export const hasWildcardKey = (objectStructure: any) => - !!find((prop, key) => key === PROP_TYPES._wildcardKey)(objectStructure().structure); - - -export const getValueType = (objectStructure: any, key: string, wildcardKeyPresent: boolean) => - wildcardKeyPresent - ? objectStructure().structure[key] || objectStructure().structure[PROP_TYPES._wildcardKey] - : objectStructure().structure[key]; + !!find((prop, key) => key === PROP_TYPES._wildcardKey)( + objectStructure().structure + ); +export const getValueType = ( + objectStructure: any, + key: string, + wildcardKeyPresent: boolean +) => + wildcardKeyPresent + ? objectStructure().structure[key] || + objectStructure().structure[PROP_TYPES._wildcardKey] + : objectStructure().structure[key]; export function validateShape(objectStructure: any, value: mixed): Object { - if (!isObject(value)) { - console.error(`The value passed to validateObject() was not an object. Value: `, value); - return {}; - } + if (!isObject(value)) { + console.error( + `The value passed to validateObject() was not an object. Value: `, + value + ); + return {}; + } - const wildcardKeyPresent = hasWildcardKey(objectStructure); + const wildcardKeyPresent = hasWildcardKey(objectStructure); - return reduce(value, (memo, value, name) => { - const valueType = getValueType(objectStructure, name, wildcardKeyPresent); - //If the value type does not exist in the reducer structure, and there's no wildcard key, then - //we don't want to include it in the payload. - //Display a console error for the developer, and skip the inclusion of this property in the payload. - if (!valueType) { - console.warn(`The property, ${name}, was not specified in the structure` + - ` and was stripped out of the payload. Structure: ${ objectStructure().structure }`); - return memo; - } + return reduce( + value, + (memo, value, name) => { + const valueType = getValueType(objectStructure, name, wildcardKeyPresent); + //If the value type does not exist in the reducer structure, and there's no wildcard key, then + //we don't want to include it in the payload. + //Display a console error for the developer, and skip the inclusion of this property in the payload. + if (!valueType) { + console.warn( + `The property, ${name}, was not specified in the structure` + + ` and was stripped out of the payload. Structure: ${ + objectStructure().structure + }` + ); + return memo; + } - const validatedValue = getTypeValidation(valueType().type)(valueType, value); + const validatedValue = getTypeValidation(valueType().type)( + valueType, + value + ); - if (validatedValue === undefined) { - console.warn(`The property, ${name}, was populated with a type ${ typeof value } which does not` + - ` match that specified in the reducer configuration ${ wildcardKeyPresent ? ', nor did it match a wildcardKey': ''}. It has been stripped from` + - ' the payload'); - return memo; - } + if (validatedValue === undefined) { + console.warn( + `The property, ${name}, was populated with a type ${typeof value} which does not` + + ` match that specified in the reducer configuration ${ + wildcardKeyPresent ? ", nor did it match a wildcardKey" : "" + }. It has been stripped from` + + " the payload" + ); + return memo; + } - return { - ...memo, - [name]: validatedValue, - } - }, {}); + return { + ...memo, + [name]: validatedValue + }; + }, + {} + ); } - export function validateValue(primitive: any, value: any): mixed { - const evaluatedPrimitive = primitive(); - //If this value is a custom value, then we should apply it's custom validator! - if (evaluatedPrimitive.type === PROP_TYPES._custom) { - if (evaluatedPrimitive.validator(value)) return value; - return console.warn(evaluatedPrimitive.validationErrorMessage(value)); - } + const evaluatedPrimitive = primitive(); + //If this value is a custom value, then we should apply it's custom validator! + if (evaluatedPrimitive.type === PROP_TYPES._custom) { + if (evaluatedPrimitive.validator(value)) return value; + return console.warn(evaluatedPrimitive.validationErrorMessage(value)); + } - //Otherwise we will use the standard, basic, typeof checks. - if (typeof value === evaluatedPrimitive.typeofValue || evaluatedPrimitive.typeofValue === 'any') return value; - return console.warn(`The value, ${value}, did not match the type specified (${evaluatedPrimitive.type}).`); + //Otherwise we will use the standard, basic, typeof checks. + if ( + typeof value === evaluatedPrimitive.typeofValue || + evaluatedPrimitive.typeofValue === "any" + ) + return value; + return console.warn( + `The value, ${value}, did not match the type specified (${ + evaluatedPrimitive.type + }).` + ); } - -export function validateArray(arrayStructure: any, value: Array): Array { - //Validate arrays by performing either of the other validation types to each element of the array, - //based on the provided reducer structure. - if (!Array.isArray(value)) { - console.error(`The value passed to validateArray() was not an array. Value: `, value); - return []; - } - const elementStructure = arrayStructure().structure; - const elementType = elementStructure().type; - return value.map(element => getTypeValidation(elementType)(elementStructure, element)).filter(e => e); +export function validateArray( + arrayStructure: any, + value: Array +): Array { + //Validate arrays by performing either of the other validation types to each element of the array, + //based on the provided reducer structure. + if (!Array.isArray(value)) { + console.error( + `The value passed to validateArray() was not an array. Value: `, + value + ); + return []; + } + const elementStructure = arrayStructure().structure; + const elementType = elementStructure().type; + return value + .map(element => getTypeValidation(elementType)(elementStructure, element)) + .filter(e => e); } - export function getTypeValidation(type: string): validationFunction { - const TYPE_VALIDATIONS = { - [PROP_TYPES._string]: validateValue, - [PROP_TYPES._number]: validateValue, - [PROP_TYPES._boolean]: validateValue, - [PROP_TYPES._array]: validateArray, - [PROP_TYPES._shape]: validateShape, - [PROP_TYPES._any]: validateValue, - [PROP_TYPES._custom]: validateValue, - }; - const typeValidation = TYPE_VALIDATIONS[type]; - if (!typeValidation) { - throw new Error(`The type ${type} does not have a corresponding + const TYPE_VALIDATIONS = { + [PROP_TYPES._string]: validateValue, + [PROP_TYPES._number]: validateValue, + [PROP_TYPES._boolean]: validateValue, + [PROP_TYPES._array]: validateArray, + [PROP_TYPES._shape]: validateShape, + [PROP_TYPES._any]: validateValue, + [PROP_TYPES._custom]: validateValue + }; + const typeValidation = TYPE_VALIDATIONS[type]; + if (!typeValidation) { + throw new Error(`The type ${type} does not have a corresponding validation function!`); - } - return typeValidation; -} \ No newline at end of file + } + return typeValidation; +}