Improve validation of the arrayReducer
This commit is contained in:
parent
0f7bd6efe4
commit
128c55bc52
11
src/index.js
11
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));
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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,
|
||||
}), {});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue