From f0db8af726b811631a1d7ad608ab19a1088b67dd Mon Sep 17 00:00:00 2001 From: Kai Moseley Date: Tue, 29 Nov 2016 16:38:18 +0000 Subject: [PATCH] Created basic shape validation functionality --- src/redux-arg/constants.js | 15 --- src/redux-arg/generateReducer.js | 6 +- src/redux-arg/reducers.js | 35 +------ src/redux-arg/reducers/arrayReducer.js | 4 +- src/redux-arg/reducers/objectReducer.js | 103 +++++++++++++++++++-- src/redux-arg/reducers/primitiveReducer.js | 4 +- src/redux-arg/structure.js | 41 +++++--- src/redux-arg/validatePayload.js | 64 +++++++++++++ 8 files changed, 197 insertions(+), 75 deletions(-) delete mode 100644 src/redux-arg/constants.js create mode 100644 src/redux-arg/validatePayload.js diff --git a/src/redux-arg/constants.js b/src/redux-arg/constants.js deleted file mode 100644 index 4a771f6..0000000 --- a/src/redux-arg/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -export const PROP_TYPES = { - _string: '_string', - _number: '_number', - _reducer: '_reducer', - _shape: '_shape', - _array: '_array', -}; - -export const Types = { - string: () => ({ type: PROP_TYPES._string, structure: PROP_TYPES._string }), - number: () => ({ type: PROP_TYPES._number, structure: PROP_TYPES._number }), - arrayOf: structure => () => ({ type: PROP_TYPES._array, structure }), - reducer: structure => () => ({ type: PROP_TYPES._reducer, structure }), - shape: structure => () => ({ type: PROP_TYPES._shape, structure}), -}; diff --git a/src/redux-arg/generateReducer.js b/src/redux-arg/generateReducer.js index 3080669..bfe69af 100644 --- a/src/redux-arg/generateReducer.js +++ b/src/redux-arg/generateReducer.js @@ -1,12 +1,12 @@ //@flow -import type { ReducerStructure } from './structure'; +import type { ShapeStructure } from './structure'; import { combineReducers } from 'redux'; import { reduce, find } from 'lodash'; import { createReducer } from './reducers'; -import { PROP_TYPES } from './constants'; +import { PROP_TYPES } from './structure'; -export function buildReducers(structure: ReducerStructure) { +export function buildReducers(structure: ShapeStructure) { const tmp = combineReducers(reduce(structure, (memo, propValue, propName) => { const { structure } = propValue(); diff --git a/src/redux-arg/reducers.js b/src/redux-arg/reducers.js index 50f6f3d..81b8eed 100644 --- a/src/redux-arg/reducers.js +++ b/src/redux-arg/reducers.js @@ -1,42 +1,19 @@ //@flow import { PROP_TYPES, - TYPE_DEFAULTS, -} from './constants'; +} from './structure'; import { reduce } from 'lodash'; import { compose } from 'ramda'; import { primitiveReducer } from './reducers/primitiveReducer'; -import { objectReducer } from './reducers/objectReducer'; +import { createObjectReducer } from './reducers/objectReducer'; import { arrayReducer } from './reducers/arrayReducer'; -const DEFAULTS_FUNCTIONS = new Map([ - [PROP_TYPES._shape, objectDefaults], - [PROP_TYPES._array, arrayDefaults], - [PROP_TYPES._string, primitiveDefaults], - [PROP_TYPES._number, primitiveDefaults], -]); - -function objectDefaults(structure) { - return reduce(structure, (memo, propValue, propName) => console.log(333, { propName, propValue: propValue() }) || ({ - ...memo, - [propName]: TYPE_DEFAULTS.get(propValue().type) - }), {}); -} - -function arrayDefaults() { - return []; -} - -function primitiveDefaults(structure) { - return TYPE_DEFAULTS.get(structure); -} - function determineReducerType(reducerDescriptor) { const { structure } = reducerDescriptor(); const { type } = structure(); let reducerFn = primitiveReducer; - if (type === PROP_TYPES._shape) reducerFn = objectReducer; + if (type === PROP_TYPES._shape) reducerFn = createObjectReducer; if (type === PROP_TYPES._array) reducerFn = arrayReducer; return { reducerFn, @@ -48,10 +25,4 @@ function callReducer({ reducerFn, reducerStructureDescriptor } = {}) { return reducerFn(reducerStructureDescriptor); } -function determineDefaults(reducerStructureDescriptor) { - const { type } = reducerStructureDescriptor(); - console.log(111, { type, fn: DEFAULTS_FUNCTIONS.get(type), DEFAULTS_FUNCTIONS }); - return DEFAULTS_FUNCTIONS.get(type); -} - export const createReducer = compose(callReducer, determineReducerType); diff --git a/src/redux-arg/reducers/arrayReducer.js b/src/redux-arg/reducers/arrayReducer.js index b5df5fc..45a2632 100644 --- a/src/redux-arg/reducers/arrayReducer.js +++ b/src/redux-arg/reducers/arrayReducer.js @@ -1,5 +1,5 @@ //@flow -import type { ReducerStructure } from '../structure'; +import type { ShapeStructure } from '../structure'; export type ArrayReducerAction = { type: string, @@ -9,7 +9,7 @@ export type ArrayReducerAction = { export type ArrayReducerFactory = (structure: Object) => ArrayReducer; export type ArrayReducer = (state: any[], action: ArrayReducerAction) => any[]; -export function arrayReducer(reducerStructureDescriptor: ReducerStructure) { +export function arrayReducer(reducerStructureDescriptor: ShapeStructure) { return(state: any[] = [], { type, payload = []}: ArrayReducerAction = {}): any[] => { switch(type) { case 'BLARG3': diff --git a/src/redux-arg/reducers/objectReducer.js b/src/redux-arg/reducers/objectReducer.js index 1a6fb62..4df7522 100644 --- a/src/redux-arg/reducers/objectReducer.js +++ b/src/redux-arg/reducers/objectReducer.js @@ -1,20 +1,105 @@ //@flow +//============================== +// Flow imports +//============================== +import type { ShapeStructure, StructureType } from '../structure'; + +//============================== +// Flow types +//============================== export type ObjectReducerAction = { type: string, - payload?: any, + payload: Object, }; -export type ObjectReducerFactory = (structure: Object) => ObjectReducer; +export type ObjectReducerFactory = (reducerStructure: ShapeStructure) => ObjectReducer; export type ObjectReducer = (state: Object, action: ObjectReducerAction) => Object; +export type ObjectReducerBehavior = (state: Object, payload: Object, initialState: Object) => Object; +export type ObjectReducerBehaviorsConfig = { + [key: string]: { + action?: (value: Object) => Object, + reducer: ObjectReducerBehavior, + } +}; +export type ObjectReducerBehaviors = { + [key: string]: ObjectReducerBehavior, +}; +export type ObjectAction = (value: Object) => { type: string, payload: Object }; +export type ObjectActions = { + [key: string]: ObjectAction +}; +export type ObjectReducerOptions = { + behaviorsConfig: ObjectReducerBehaviorsConfig, + locationString: string, +}; +//============================== +// JS imports +//============================== +import { reduce } from 'lodash/reduce'; +import { validateObject } from '../validatePayload'; -export function objectReducer(): ObjectReducer { - return (state = {}, { type, payload } = {}) => { - switch (type) { - case 'BLARG!': - return { state, ...payload }; - default: - return state; +const DEFAULT_OBJECT_BEHAVIORS: ObjectReducerBehaviorsConfig = { + update: { + action(value) { return value }, + reducer(state, payload) { + return { ...state, ...payload }; } + }, + reset: { + reducer(state, payload, initialState) { + return initialState; + } + }, + replace: { + reducer(state, payload, initialState) { + return payload; + } + } +}; + +export function createObjectReducer(reducerShape: StructureType, { + behaviorsConfig = {}, + locationString +}: ObjectReducerOptions) { + return { + reducer: createReducer(reducerShape, createReducerBehaviors(behaviorsConfig, locationString)), + actions: createActions(behaviorsConfig, locationString), + }; +} + +function calculateDefaults(reducerStructure) { + return reduce(reducerStructure, (memo, propValue, propName) => ({ + ...memo, + [propName]: propValue().defaultValue, + }), {}); +} + +function createReducer(objectStructure: StructureType, behaviors: ObjectReducerBehaviors): ObjectReducer { + const initialState = calculateDefaults(objectStructure().structure); + return (state = initialState, { type, payload }: ObjectReducerAction) => { + //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, validateObject(payload), initialState); } } +function createActions(behaviorsConfig: ObjectReducerBehaviorsConfig, locationString: string): ObjectActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce(behaviorsConfig, (memo, behavior, name) => ({ + ...memo, + [`${locationString}.${name}`]: (value: Object) => + ({ type: `${locationString}.${name}`, payload: behavior.action(value) || {} }) + }), {}); +} + +function createReducerBehaviors(behaviorsConfig: ObjectReducerBehaviorsConfig, locationString: string): ObjectReducerBehaviors { + //Take a reducer behavior config object, and create the reducer behaviors using the location string + return reduce(behaviorsConfig, (memo, behavior, name) => ({ + ...memo, + [`${locationString}.${name}`]: behavior, + }), {}); +} + + diff --git a/src/redux-arg/reducers/primitiveReducer.js b/src/redux-arg/reducers/primitiveReducer.js index 550b0e9..8c4b624 100644 --- a/src/redux-arg/reducers/primitiveReducer.js +++ b/src/redux-arg/reducers/primitiveReducer.js @@ -1,5 +1,5 @@ //@flow -import type { ReducerStructure } from '../structure'; +import type { ShapeStructure } from '../structure'; export type PrimitiveReducerAction = { type: string, @@ -8,7 +8,7 @@ export type PrimitiveReducerAction = { export type PrimitiveReducerFactory = (structure: Object) => PrimitiveReducer; export type PrimitiveReducer = (state: any, action: PrimitiveReducerAction) => any; -export function primitiveReducer(reducerStructureDescriptor: ReducerStructure) { +export function primitiveReducer(reducerStructureDescriptor: ShapeStructure) { return(state: any = '', { type, payload }: PrimitiveReducerAction = {}) => { switch(type) { case 'BLARG3': diff --git a/src/redux-arg/structure.js b/src/redux-arg/structure.js index 7d7c7d5..51ad3b8 100644 --- a/src/redux-arg/structure.js +++ b/src/redux-arg/structure.js @@ -3,21 +3,20 @@ //============================== // Flow types //============================== -export type ReducerStructure = { +export type ShapeStructure = { [key: string]: StructureType|PrimitiveType, } export type StructureType = () => { type: string, - structure: StructureType|ReducerStructure + structure: ShapeStructure|StructureType|PrimitiveType }; export type PrimitiveType = () => { type: string, - structure: $Keys, - defaultValue: ?any, + defaultValue?: any, + typeofValue: string, }; - export type TypesObject = { - [key: string]: CreateStructure|CreateStringType|CreateNumberType + [key: string]: CreateArrayType|CreateStringType|CreateNumberType|CreateObjectType; } export type TypesObjectDefaults = { @@ -27,7 +26,8 @@ export type TypesArrayDefaults = Array|Array; type CreateStringType = (defaultValue: string) => PrimitiveType; type CreateNumberType = (defaultValue: number) => PrimitiveType; -type CreateStructure = (structure: ReducerStructure, defaultValue: TypesArrayDefaults|TypesObjectDefaults) => StructureType; +type CreateArrayType = (structure: StructureType|PrimitiveType, defaultValue: TypesArrayDefaults|TypesObjectDefaults) => StructureType; +type CreateObjectType = (structure: ShapeStructure, defaultValue: TypesArrayDefaults|TypesObjectDefaults) => StructureType; //============================== // Structure @@ -41,9 +41,26 @@ export const PROP_TYPES = { }; export const Types: TypesObject = { - string: (defaultValue: string = '') => () => ({ type: PROP_TYPES._string, structure: PROP_TYPES._string, defaultValue }), - number: (defaultValue: number = 0) => () => ({ type: PROP_TYPES._number, structure: PROP_TYPES._number, defaultValue }), - arrayOf: (structure: ReducerStructure) => () => ({ type: PROP_TYPES._array, structure }), - reducer: (structure: ReducerStructure) => () => ({ type: PROP_TYPES._reducer, structure }), - shape: (structure: ReducerStructure) => () => ({ type: PROP_TYPES._shape, structure}), + string: (defaultValue: string = '') => () => ({ + type: PROP_TYPES._string, + defaultValue, + typeofValue: 'string' + }), + number: (defaultValue: number = 0) => () => ({ + type: PROP_TYPES._number, + defaultValue, + typeofValue: 'number' + }), + arrayOf: (structure: StructureType|PrimitiveType) => () => ({ + type: PROP_TYPES._array, + structure, + }), + reducer: (structure: ShapeStructure) => () => ({ + type: PROP_TYPES._reducer, + structure, + }), + shape: (structure: ShapeStructure) => () => ({ + type: PROP_TYPES._shape, + structure, + }), }; diff --git a/src/redux-arg/validatePayload.js b/src/redux-arg/validatePayload.js new file mode 100644 index 0000000..c39d86b --- /dev/null +++ b/src/redux-arg/validatePayload.js @@ -0,0 +1,64 @@ +//@flow +//============================== +// Flow imports +//============================== +import type { PrimitiveType, StructureType, ShapeStructure } from './structure'; + +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'; + +export function validateObject(objectStructure: any, value: mixed): Object { + if (!isObject(value)) { + console.error(`The value passed to validateObject() was not an object. Value: `, value); + return {}; + } + return reduce(value, (memo, value, name) => { + const valueType = objectStructure.structure[name]; + //If the value type does not exist in the reducer structure, 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.error(`The property, ${name}, was not specified in the structure + and was stripped out of the payload. Structure:`, objectStructure); + return memo; + } + + return { + ...memo, + [name]: getTypeValidation(valueType().type)(valueType, value), + } + }, {}); +} + +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; +} + +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. + const elementStructure = arrayStructure().structure; + const elementType = arrayStructure().type; + return value.map(v => getTypeValidation(elementType)(elementStructure, value)) +} + +function getTypeValidation(type): validationFunction { + const TYPE_VALIDATIONS = new Map([ + [[PROP_TYPES._string], validatePrimitive], + [[PROP_TYPES._number], validatePrimitive], + [[PROP_TYPES._array], validateArray], + [[PROP_TYPES._shape], validateObject], + ]); + const typeValidation = TYPE_VALIDATIONS.get(type); + if (!typeValidation) { + throw new Error(`The type ${type} does not have a corresponding + validation function!`); + } + return typeValidation; +} \ No newline at end of file