Improve validation of the arrayReducer

This commit is contained in:
Kai Moseley 2016-12-05 16:50:51 +00:00
parent 0f7bd6efe4
commit 128c55bc52
6 changed files with 56 additions and 16 deletions

View File

@ -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));

View File

@ -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({

View File

@ -37,13 +37,15 @@ export type ArraySelector = (state: Object) => Array<any>;
// 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<any> = [], { 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<any> = 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) => ({

View File

@ -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,
}), {});
}

View File

@ -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,

View File

@ -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<mixed>): Array<mixed> {
@ -55,7 +56,7 @@ export function validateArray(arrayStructure: any, value: Array<mixed>): Array<m
if (!Array.isArray(value)) return [];
const elementStructure = arrayStructure().structure;
const elementType = elementStructure().type;
return value.map(element => getTypeValidation(elementType)(elementStructure, element));
return value.map(element => getTypeValidation(elementType)(elementStructure, element)).filter(e => e);
}
function getTypeValidation(type): validationFunction {