diff --git a/src/__tests__/reducers.test.js b/src/__tests__/reducers.test.js index 78cce6a..3ec45e7 100644 --- a/src/__tests__/reducers.test.js +++ b/src/__tests__/reducers.test.js @@ -99,9 +99,14 @@ describe('reducers', () => { 'toast': { reducer: 'foo', action: 'bar', + validate: true, } }, 'location')).toEqual({ - 'location.toast': 'foo', + 'location.toast': { + reducer: 'foo', + action: 'bar', + validate: true, + }, }) }); }); diff --git a/src/reducers.js b/src/reducers.js index 8b6e0c4..8cb57fb 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -97,7 +97,7 @@ export function createReducerBehaviors(behaviorsConfig: { [key: string]: { reduc //the location string/name combination, so will match up 1:1. return reduce(behaviorsConfig, (memo, behavior, name) => ({ ...memo, - [`${locationString}.${name}`]: behavior.reducer, + [`${locationString}.${name}`]: behavior, }), {}); } diff --git a/src/reducers/__tests__/arrayReducer.test.js b/src/reducers/__tests__/arrayReducer.test.js index 2c8563f..f8f9ed2 100644 --- a/src/reducers/__tests__/arrayReducer.test.js +++ b/src/reducers/__tests__/arrayReducer.test.js @@ -64,6 +64,33 @@ describe('arrayReducer', () => { }); }); + describe('push', () => { + const { push } = DEFAULT_ARRAY_BEHAVIORS; + it('should push the payload onto the end of the array', () => { + expect(push.reducer([1,2,3], 4)).toEqual([1,2,3,4]); + }); + }); + + describe('pop', () => { + const { pop } = DEFAULT_ARRAY_BEHAVIORS; + it('should remove the last element from the array', () => { + expect(pop.reducer([1,2,3])).toEqual([1,2]); + }); + }); + + describe('unshift', () => { + const { unshift } = DEFAULT_ARRAY_BEHAVIORS; + it('should add the payload to the beginning of the array', () => { + expect(unshift.reducer([1,2,3], 4)).toEqual([4,1,2,3]); + }); + }); + + describe('shift', () => { + const { shift } = DEFAULT_ARRAY_BEHAVIORS; + it('should remove the first element of the array', () => { + expect(shift.reducer([1,2,3])).toEqual([2,3]); + }); + }); }); describe('applyValidation', () => { @@ -82,7 +109,7 @@ describe('arrayReducer', () => { }); describe('createReducer', () => { - const arrayStructure = Types.arrayOf(Types.number()); + const arrayStructure = Types.arrayOf(Types.number(), [1,2,3,4]); const reducer = createReducer(arrayStructure, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, 'string')); it('should call the correct behavior', () => { @@ -92,6 +119,12 @@ describe('arrayReducer', () => { index: 0, })).toEqual([4,2,3]); }); + + it('do not trigger validation if not required', () => { + expect(reducer([1,2], { + type: 'string.reset', + })).toEqual([1,2,3,4]); + }); }); }); \ No newline at end of file diff --git a/src/reducers/__tests__/objectReducer.test.js b/src/reducers/__tests__/objectReducer.test.js index 30ed9c3..09db08f 100644 --- a/src/reducers/__tests__/objectReducer.test.js +++ b/src/reducers/__tests__/objectReducer.test.js @@ -71,6 +71,12 @@ describe('ObjectReducer', () => { payload: { foo: 'toast'} })).toEqual({ foo: 'toast', bar: { baz: 5 }}); }); + + it('do not trigger validation if not required', () => { + expect(reducer({ foo: 'foo', bar: { baz: 6 }}, { + type: 'string.reset', + })).toEqual({ foo: 'foo', bar: { baz: 5 }}); + }); }); }); \ No newline at end of file diff --git a/src/reducers/arrayReducer.js b/src/reducers/arrayReducer.js index 1e704a0..45fe122 100644 --- a/src/reducers/arrayReducer.js +++ b/src/reducers/arrayReducer.js @@ -18,6 +18,7 @@ export type ArrayReducerBehaviorsConfig = { [key: string]: { action?: (value: any) => any, reducer: ArrayReducerBehavior, + validate: boolean, } }; export type ArrayReducerBehaviors = { @@ -69,32 +70,61 @@ export const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = { 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, }, resetAtIndex: { reducer(state, payload, initialState, index) { if (!checkIndex(index, payload, 'resetAtIndex')) return state; return updateAtIndex(state, initialState, index); - } + }, + validate: false, }, removeAtIndex: { reducer(state, payload, initialState, index) { if (!checkIndex(index, payload, 'removeAtIndex')) return state; return removeAtIndex(state, index); - } + }, + 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; - } + }, + validate: true, }, reset: { reducer(state, payload, initialState) { return initialState; - } + }, + validate: false, }, + push: { + reducer(state, payload) { + return [...state, payload]; + }, + validate: true, + }, + pop: { + reducer(state) { + return state.slice(0, -1); + }, + validate: false, + }, + unshift: { + reducer(state, payload) { + return [payload, ...state]; + }, + validate: true, + }, + shift: { + reducer(state) { + return state.slice(1); + }, + validate: false, + } }; @@ -123,7 +153,12 @@ export function createReducer(arrayTypeDescription: ArrayStructureType, behavior //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); + return behaviors[type].reducer( + state, + behaviors[type].validate ? applyValidation(arrayTypeDescription, payload) : payload, + initialValue, + index + ); } } diff --git a/src/reducers/objectReducer.js b/src/reducers/objectReducer.js index 39ccf23..2d61df0 100644 --- a/src/reducers/objectReducer.js +++ b/src/reducers/objectReducer.js @@ -11,6 +11,7 @@ import type { StructureType } from '../structure'; export type ShapeReducerAction = { type: string, payload: Object, + validate: boolean, }; export type ShapeReducer = (state: Object, action: ShapeReducerAction) => Object; export type ShapeReducerBehavior = (state: {}, payload: Object | void, initialState: {}) => Object; @@ -55,18 +56,21 @@ export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = { reducer(state, payload) { if (!isObject(payload)) return state; return { ...state, ...payload }; - } + }, + validate: true, }, reset: { reducer(state, payload, initialState) { return initialState; - } + }, + validate: false, }, replace: { reducer(state, payload) { if (!payload) return state; return payload; - } + }, + validate: true, } }; @@ -102,7 +106,11 @@ export function createReducer(objectStructure: StructureType, behaviors: ShapeRe //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, validateShape(objectStructure, payload), initialState); + return behaviors[type].reducer( + state, + behaviors[type].validate ? validateShape(objectStructure, payload) : payload, + initialState + ); } } diff --git a/src/reducers/primitiveReducer.js b/src/reducers/primitiveReducer.js index bb8f71d..df20810 100644 --- a/src/reducers/primitiveReducer.js +++ b/src/reducers/primitiveReducer.js @@ -17,6 +17,7 @@ export type PrimitiveReducerBehaviorsConfig = { [key: string]: { action?: (value: mixed) => mixed, reducer: PrimitiveReducerBehavior, + validate: boolean, } }; export type PrimitiveReducerBehaviors = { @@ -52,12 +53,14 @@ export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = { reducer(state, payload) { if (payload === undefined) return state; return payload; - } + }, + validate: true, }, reset: { reducer(state, payload, initialState) { return initialState; - } + }, + validate: false, }, }; @@ -76,6 +79,7 @@ export function createPrimitiveReducer(primitiveType: PrimitiveType, { function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducerBehaviors): PrimitiveReducer { + //Calculate and validate the initial state of the reducer const initialState: mixed = validatePrimitive(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. @@ -83,7 +87,11 @@ function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducer //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, validatePrimitive(primitiveType, payload), initialState); + return behaviors[type].reducer( + state, + behaviors[type].validate ? validatePrimitive(primitiveType, payload) : payload, + initialState + ); } }