Created basic shape validation functionality

This commit is contained in:
Kai Moseley 2016-11-29 16:38:18 +00:00
parent 36b5eadc99
commit f0db8af726
8 changed files with 197 additions and 75 deletions

View File

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

View File

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

View File

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

View File

@ -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<ArrayReducerFactory>(reducerStructureDescriptor: ReducerStructure) {
export function arrayReducer<ArrayReducerFactory>(reducerStructureDescriptor: ShapeStructure) {
return(state: any[] = [], { type, payload = []}: ArrayReducerAction = {}): any[] => {
switch(type) {
case 'BLARG3':

View File

@ -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 function objectReducer(): ObjectReducer {
return (state = {}, { type, payload } = {}) => {
switch (type) {
case 'BLARG!':
return { state, ...payload };
default:
return state;
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';
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,
}), {});
}

View File

@ -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<ArrayReducerFactory>(reducerStructureDescriptor: ReducerStructure) {
export function primitiveReducer<ArrayReducerFactory>(reducerStructureDescriptor: ShapeStructure) {
return(state: any = '', { type, payload }: PrimitiveReducerAction = {}) => {
switch(type) {
case 'BLARG3':

View File

@ -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<typeof PROP_TYPES>,
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<mixed>|Array<TypesObjectDefaults>;
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,
}),
};

View File

@ -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<mixed>): Array<mixed> {
//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;
}