From 065126c761203b2a53dafed06902c88fa9f7f071 Mon Sep 17 00:00:00 2001 From: Kai Moseley Date: Sun, 4 Dec 2016 14:42:51 +0000 Subject: [PATCH] Initial draft of array behaviors --- src/redux-arg/reducers.js | 35 +++++-- src/redux-arg/reducers/arrayReducer.js | 133 ++++++++++++++++++++++++ src/redux-arg/reducers/objectReducer.js | 10 +- src/redux-arg/utils/arrayUtils.js | 15 +++ 4 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 src/redux-arg/utils/arrayUtils.js diff --git a/src/redux-arg/reducers.js b/src/redux-arg/reducers.js index f24fc4b..b49ab83 100644 --- a/src/redux-arg/reducers.js +++ b/src/redux-arg/reducers.js @@ -2,14 +2,17 @@ //============================== // Flow imports //============================== -import type { ObjectReducer, ObjectAction, ObjectSelector } from './reducers/objectReducer'; - -import { - PROP_TYPES, -} from './structure'; -import { compose } from 'ramda'; -import { createObjectReducer } from './reducers/objectReducer'; +import type { + ObjectReducer, + ObjectAction, + ObjectSelector, + ObjectReducerBehaviorsConfig, + ObjectReducerBehaviors, +} from './reducers/objectReducer'; +//============================== +// Flow types +//============================== export type Selectors = ObjectSelector; export type Actions = ObjectAction; export type Reducers = ObjectReducer; @@ -18,6 +21,16 @@ export type PartialReducer = { actionsObject: { [key: string]: Actions }, selectorsObject?: { [key: string]: Selectors }, }; +type ReducerBehaviorsConfig = ObjectReducerBehaviorsConfig; +type ReducerBehaviors = ObjectReducerBehaviors; + +import { + PROP_TYPES, +} from './structure'; +import { compose } from 'ramda'; +import { reduce } from 'lodash'; +import { createObjectReducer } from './reducers/objectReducer'; + function determineReducerType(reducerDescriptor, { locationString, @@ -41,3 +54,11 @@ function callReducer({ reducerFn, reducerStructureDescriptor, locationString } = } export const createReducer = compose(callReducer, determineReducerType); + +export function createReducerBehaviors(behaviorsConfig: ReducerBehaviorsConfig, locationString: string): ReducerBehaviors { + //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.reducer, + }), {}); +} diff --git a/src/redux-arg/reducers/arrayReducer.js b/src/redux-arg/reducers/arrayReducer.js index 8b13789..db770d2 100644 --- a/src/redux-arg/reducers/arrayReducer.js +++ b/src/redux-arg/reducers/arrayReducer.js @@ -1 +1,134 @@ +//@flow +//============================== +// Flow imports +//============================== +import type { ShapeStructure, StructureType } from '../structure'; +//============================== +// Flow types +//============================== +export type ArrayReducerAction = { + type: string, + payload: any, +}; +export type ArrayReducer = (state: Array, action: ArrayReducerAction) => Array; +export type ArrayReducerBehavior = (state: Array, payload: any, initialState: Array, index: number | void) => Array; +export type ArrayReducerBehaviorsConfig = { + [key: string]: { + action?: (value: any) => any, + reducer: ArrayReducerBehavior, + } +}; +export type ArrayReducerBehaviors = { + [key: string]: ArrayReducerBehavior, +}; +export type ArrayAction = (value: Array) => { type: string, payload: any, index?: number }; +export type ArrayActions = { + [key: string]: Array +}; +export type ArrayReducerOptions = { + behaviorsConfig: ArrayReducerBehaviorsConfig, + locationString: string, +}; +export type ArraySelector = (state: Object) => Array; + +//============================== +// JS imports +//============================== +import { reduce, isArray, isObject, isNull } from 'lodash'; +import { validateArray } from '../validatePayload'; +import { createReducerBehaviors } from '../reducers'; +import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils'; + +function checkIndex(state: Object, payload: any, behaviorName: string): boolean { + if (!isNumber(index)) { + throw new Error(`Index not passed to ${behaviorName} for payload ${payload}.`); + } + return true; +} + +const DEFAULT_OBJECT_BEHAVIORS: ArrayReducerBehaviorsConfig = { + updateAtIndex: { + action(value) { return value }, + reducer(state, payload, initialState, index) { + checkIndex(index, payload, 'updateOne'); + if (isArray(payload) || isObject(payload)) return updateAtIndex(state, { ...state[index], ...payload }, index); + return updateAtIndex(state, payload, index); + } + }, + resetAtIndex: { + reducer(state, payload, initialState, index) { + checkIndex(index, payload, 'updateOne'); + return updateAtIndex(state, initialState, index); + } + }, + removeAtIndex: { + reducer(state, payload, initialState, index) { + checkIndex(index, payload, 'updateOne'); + return removeAtIndex(state, index); + } + }, + replaceAtIndex: { + reducer(state, payload, initialState, index) { + checkIndex(index, payload, 'updateOne'); + return updateAtIndex(state, payload, index); + } + }, + replace: { + action(value) { + if(!isArray(value)) throw new Error('An array must be provided when replacing an array'); + }, + reducer(state, payload) { + return payload; + } + }, + reset: { + reducer(state, payload, initialState) { + return initialState; + } + }, +}; + +//TODO: All the array functionality! + +export function createArrayReducer(reducerShape: StructureType, { + locationString +}: ArrayReducerOptions = {}) { + return { + reducers: createReducer(reducerShape, createReducerBehaviors(DEFAULT_OBJECT_BEHAVIORS, locationString)), + actionsObject: createActions(DEFAULT_OBJECT_BEHAVIORS, locationString, {}), + }; +} + + +function calculateDefaults(reducerStructure) { + return reduce(reducerStructure, (memo, propValue, propName) => ({ + ...memo, + [propName]: propValue().defaultValue, + }), {}); +} + + +function createReducer(arrayStructure: StructureType, behaviors: ArrayReducerBehaviors): ArrayReducer { + const initialState = calculateDefaults(arrayStructure().structure); + return (state = initialState, { type, payload }: 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, validateArray(arrayStructure, payload), initialState); + } +} + + +function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string, defaultPayload: any): ArrayActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce(behaviorsConfig, (memo, behavior, name) => ({ + ...memo, + [name]: (value: Object) => ({ + type: `${locationString}.${name}`, + payload: (behavior.action || (() => defaultPayload))(value) || {} + }) + }), {}); +} diff --git a/src/redux-arg/reducers/objectReducer.js b/src/redux-arg/reducers/objectReducer.js index b546642..d7ea848 100644 --- a/src/redux-arg/reducers/objectReducer.js +++ b/src/redux-arg/reducers/objectReducer.js @@ -38,6 +38,7 @@ export type ObjectSelector = (state: Object) => Object; //============================== import { reduce } from 'lodash'; import { validateObject } from '../validatePayload'; +import { createReducerBehaviors } from '../reducers'; const DEFAULT_OBJECT_BEHAVIORS: ObjectReducerBehaviorsConfig = { update: { @@ -102,12 +103,3 @@ function createActions(behaviorsConfig: ObjectReducerBehaviorsConfig, locationSt } -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.reducer, - }), {}); -} - - diff --git a/src/redux-arg/utils/arrayUtils.js b/src/redux-arg/utils/arrayUtils.js new file mode 100644 index 0000000..b2eeb9a --- /dev/null +++ b/src/redux-arg/utils/arrayUtils.js @@ -0,0 +1,15 @@ +//@flow +export function updateAtIndex(array: Array, value: any, index: number): Array { + return [ + ...array.slice(0,index), + value, + ...array.slice(index + 1), + ]; +} + +export function removeAtIndex(array: Array, index: number): Array { + return [ + ...array.slice(0, index), + ...array.slice(index + 1), + ]; +}