diff --git a/src/index.js b/src/index.js index c86b444..ed2b66f 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,10 @@ const exampleReducer = { form2: Types.reducer(Types.shape({ lowerLevel: Types.number(5), lowerLevel2: Types.string('Blargle'), - lowerLevelArray: Types.arrayOf(Types.string()), + lowerLevelArray: Types.arrayOf(Types.string(), ['foo', 'bar', 'toast']), + nested: Types.shape({ + lowerLevel3: Types.number(), + }) })), form3: Types.reducer({ example2: Types.reducer(Types.shape({ @@ -25,7 +28,8 @@ const exampleReducer = { }) }), arrayTest: Types.reducer(Types.arrayOf( - Types.number() + Types.number(), + [1,3,4] )) }) }; @@ -37,7 +41,7 @@ const store = createStore( compose(window.devToolsExtension ? window.devToolsExtension() : f => f) ); -store.dispatch(test.actionsObject.example.form2.update({ lowerLevel: 2, lowerLevel2: 'Rawrg' })); +store.dispatch(test.actionsObject.example.form2.update({ lowerLevel: 2, lowerLevel2: 'Rawrg', lowerLevelArray: [3, 'foo'] })); store.dispatch(test.actionsObject.example.form2.reset()); store.dispatch(test.actionsObject.example.form2.replace({ toast: 'nommyNom' })); store.dispatch(test.actionsObject.example.form2.reset()); @@ -47,4 +51,5 @@ console.log(222, test.actionsObject); store.dispatch(test.actionsObject.example.arrayTest.replace([1,2,3])); store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex(5, 0)); +store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex('foo', 0)); diff --git a/src/redux-arg/__tests__/validatePayload.test.js b/src/redux-arg/__tests__/validatePayload.test.js index 98ec242..0c619ab 100644 --- a/src/redux-arg/__tests__/validatePayload.test.js +++ b/src/redux-arg/__tests__/validatePayload.test.js @@ -20,9 +20,9 @@ describe('Testing validation functionality', () => { expect(validateArray(testArrayStructure, ['a','b','c','d'])) .toEqual(['a','b','c','d']); }); - it('Arrays should return undefined for primitives which fail the test', () => { + it('Arrays should strip values for primitives which fail the test', () => { expect(validateArray(testArrayStructure, ['a','b',3,'d'])) - .toEqual(['a','b',undefined,'d']); + .toEqual(['a','b','d']); }); const testArrayStructure2 = Types.arrayOf(Types.shape({ diff --git a/src/redux-arg/reducers/arrayReducer.js b/src/redux-arg/reducers/arrayReducer.js index eefea15..a279658 100644 --- a/src/redux-arg/reducers/arrayReducer.js +++ b/src/redux-arg/reducers/arrayReducer.js @@ -37,13 +37,15 @@ export type ArraySelector = (state: Object) => Array; // JS imports //============================== import { reduce, isArray, isNumber, isObject } from 'lodash'; -//import { validateArray } from '../validatePayload'; +import { validateArray, validateObject, validatePrimitive } from '../validatePayload'; import { createReducerBehaviors } from '../reducers'; import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils'; +import { PROP_TYPES } from '../structure'; function checkIndex(index: ?number, payload: any, behaviorName: string): boolean { if (!isNumber(index) || index === -1) { - throw new Error(`Index not passed to ${behaviorName} for payload ${payload}.`); + console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`); + return false; } return true; } @@ -51,7 +53,8 @@ function checkIndex(index: ?number, payload: any, behaviorName: string): boolean const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = { updateAtIndex: { reducer(state, payload, initialState, index = -1) { - checkIndex(index, payload, 'updateAtIndex'); + if (!checkIndex(index, payload, 'updateAtIndex')) return state; + if (payload === undefined) return console.warn('Undefined was passed when updating index. Update not performed') || state; if (isArray(payload) || isObject(payload)) return updateAtIndex(state, { ...state[index], ...payload }, index); return updateAtIndex(state, payload, index); } @@ -71,6 +74,7 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = { replaceAtIndex: { reducer(state, payload, initialState, index) { checkIndex(index, payload, 'replaceAtIndex'); + if (payload === undefined) console.warn('Undefined was passed when updating index. Update not performed'); return updateAtIndex(state, payload, index); } }, @@ -90,6 +94,7 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = { }, }; + export function createArrayReducer(reducerShape: StructureType, { locationString }: ArrayReducerOptions = {}) { @@ -99,17 +104,42 @@ export function createArrayReducer(reducerShape: StructureType, { }; } + function createReducer(arrayTypeDescription: StructureType, behaviors: ArrayReducerBehaviors): ArrayReducer { - return (state: Array = [], { type, payload, index }: ArrayReducerAction) => { + //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) => { //If the action type does not match any of the specified behaviors, just return the current state. if (!behaviors[type]) return state; - //Sanitize the payload using the reducer shape, then apply the sanitized - //payload to the state using the behavior linked to this action type. - return behaviors[type](state, payload, [], index); + //Validating the payload of an array is more tricky, as we do not know ahead of time if the + //payload should be an object, primitive, or an array. However, we can still validate here based on the + //payload type passed. + return behaviors[type](state, applyValidation(arrayTypeDescription, payload), initialValue, index); } } +function applyValidation(arrayTypeDescription: StructureType, 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); + + // 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 validateObject(structure, payload); + return validatePrimitive(structure, payload); +} + + function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string): ArrayActions { //Take a reducer behavior config object, and create actions using the location string return reduce(behaviorsConfig, (memo, behavior, name) => ({ diff --git a/src/redux-arg/reducers/objectReducer.js b/src/redux-arg/reducers/objectReducer.js index d7ea848..e9f5a08 100644 --- a/src/redux-arg/reducers/objectReducer.js +++ b/src/redux-arg/reducers/objectReducer.js @@ -39,6 +39,7 @@ export type ObjectSelector = (state: Object) => Object; import { reduce } from 'lodash'; import { validateObject } from '../validatePayload'; import { createReducerBehaviors } from '../reducers'; +import { PROP_TYPES } from '../structure'; const DEFAULT_OBJECT_BEHAVIORS: ObjectReducerBehaviorsConfig = { update: { @@ -73,7 +74,9 @@ export function createObjectReducer(reducerShape: StructureType, { function calculateDefaults(reducerStructure) { return reduce(reducerStructure, (memo, propValue, propName) => ({ ...memo, - [propName]: propValue().defaultValue, + [propName]: propValue().type === PROP_TYPES._shape + ? calculateDefaults(propValue().structure) + : propValue().defaultValue, }), {}); } diff --git a/src/redux-arg/structure.js b/src/redux-arg/structure.js index db885fe..b26315b 100644 --- a/src/redux-arg/structure.js +++ b/src/redux-arg/structure.js @@ -60,9 +60,10 @@ export const Types: TypesObject = { defaultValue, typeofValue: 'boolean', }), - arrayOf: (structure: StructureType | PrimitiveType) => () => ({ + arrayOf: (structure: StructureType | PrimitiveType, defaultValue = []) => () => ({ type: PROP_TYPES._array, structure, + defaultValue, }), reducer: (structure: ShapeStructure) => () => ({ type: PROP_TYPES._reducer, diff --git a/src/redux-arg/validatePayload.js b/src/redux-arg/validatePayload.js index 4753e45..1c53a33 100644 --- a/src/redux-arg/validatePayload.js +++ b/src/redux-arg/validatePayload.js @@ -46,7 +46,8 @@ export function validateObject(objectStructure: any, value: mixed): Object | voi export function validatePrimitive(primitive: any, value: mixed): mixed { //Validate primitives using the typeofValue property of the primitive type definitions. - return typeof value === primitive().typeofValue ? value : undefined; + if (typeof value === primitive().typeofValue ) return value; + return console.warn(`The value, ${value}, did not match the type specified (${primitive().type}).`); } export function validateArray(arrayStructure: any, value: Array): Array { @@ -55,7 +56,7 @@ export function validateArray(arrayStructure: any, value: Array): Array getTypeValidation(elementType)(elementStructure, element)); + return value.map(element => getTypeValidation(elementType)(elementStructure, element)).filter(e => e); } function getTypeValidation(type): validationFunction {