Initial draft of array reducer

This commit is contained in:
Kai Moseley 2016-12-04 19:25:49 +00:00
parent 76c577ce6c
commit 0f7bd6efe4
10 changed files with 100 additions and 62 deletions

View File

@ -25,7 +25,6 @@ module.exports = {
'react/prop-types': ['off'],
'react/display-name': ['off'],
'no-console': ['off'],
//'arrow-parens': ['warn', 'as-needed'],
},
env: {
browser: true,
@ -33,12 +32,6 @@ module.exports = {
es6: true,
},
globals: {
_: true,
_fmt: true,
_misc: true,
_arr: true,
_cfg: true,
Promise: true,
RE_CHILD_INDEX_JS: true,
}
};

View File

@ -1 +1,2 @@
[ignore]
.*/__tests__/.*

View File

@ -7,6 +7,7 @@ const exampleReducer = {
form2: Types.reducer(Types.shape({
lowerLevel: Types.number(5),
lowerLevel2: Types.string('Blargle'),
lowerLevelArray: Types.arrayOf(Types.string()),
})),
form3: Types.reducer({
example2: Types.reducer(Types.shape({
@ -23,6 +24,9 @@ const exampleReducer = {
})
})
}),
arrayTest: Types.reducer(Types.arrayOf(
Types.number()
))
})
};
@ -39,4 +43,8 @@ store.dispatch(test.actionsObject.example.form2.replace({ toast: 'nommyNom' }));
store.dispatch(test.actionsObject.example.form2.reset());
console.log(111, test.selectorsObject.example.form2(store.getState()));
console.log(222, test.actionsObject);
store.dispatch(test.actionsObject.example.arrayTest.replace([1,2,3]));
store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex(5, 0));

View File

@ -0,0 +1,41 @@
//@flow
import { Types } from '../structure';
import { calculateDefaults } from '../reducers';
describe('reducers', () => {
describe('defaultValues', () => {
it('Should provide correct default values for a given primitive type', () => {
expect(calculateDefaults(Types.string('toast'))).toBe('toast');
expect(calculateDefaults(Types.number(3))).toBe(3);
expect(calculateDefaults(Types.string())).toBe('');
expect(calculateDefaults(Types.number())).toBe(0);
});
it('Should provide correct default values for an object', () => {
const objectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
});
expect(calculateDefaults(objectStructure)).toEqual({ test1: '', test2: 0 });
});
it('Should provide correct default values for nested object', () => {
const objectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
test3: Types.shape({
test4: Types.string('foo'),
})
});
expect(calculateDefaults(objectStructure)).toEqual({
test1: '',
test2: 0,
test3: {
test4: 'foo'
}
});
});
});
});

View File

@ -3,7 +3,7 @@
// Flow imports
//==============================
import type { StructureType, PrimitiveType } from './structure';
import type { PartialReducer, Selectors } from './reducers';
import type { PartialReducer } from './reducers';
import { combineReducers } from 'redux';
import { reduce, find } from 'lodash';
@ -14,7 +14,7 @@ export function buildReducers(name: string, structure: any, {
baseSelector = state => state,
locationString = '',
}: {
baseSelector: Selectors,
baseSelector: any,
locationString: string,
} = {}): PartialReducer {
@ -48,10 +48,10 @@ export function buildReducers(name: string, structure: any, {
let childReducer = containsReducers
? buildReducers(propName, propStructure, {
locationString: locationString ? `${locationString}.${propName}` : propName,
baseSelector: state => baseSelector(state)[propName],
baseSelector: (state: any) => baseSelector(state)[propName],
})
: createReducer(propValue, {
locationString,
locationString: `${locationString}.${propName}`,
});
//As the object is built up, we want to assign the reducers/actions created

View File

@ -2,13 +2,6 @@
//==============================
// Flow imports
//==============================
import type {
ObjectReducer,
ObjectAction,
ObjectSelector,
ObjectReducerBehaviorsConfig,
ObjectReducerBehaviors,
} from './reducers/objectReducer';
import type {
StructureType,
PrimitiveType,
@ -17,16 +10,11 @@ import type {
//==============================
// Flow types
//==============================
export type Selectors = ObjectSelector;
export type Actions = ObjectAction;
export type Reducers = ObjectReducer;
export type PartialReducer = {
reducers: { [key: string]: Reducers},
actionsObject: { [key: string]: Actions },
selectorsObject?: { [key: string]: Selectors },
reducers: { [key: string]: any },
actionsObject: { [key: string]: any },
selectorsObject?: { [key: string]: any },
};
type ReducerBehaviorsConfig = ObjectReducerBehaviorsConfig;
type ReducerBehaviors = ObjectReducerBehaviors;
import {
PROP_TYPES,
@ -34,18 +22,24 @@ import {
import { compose } from 'ramda';
import { reduce } from 'lodash';
import { createObjectReducer } from './reducers/objectReducer';
import { createArrayReducer } from './reducers/arrayReducer';
function determineReducerType(reducerDescriptor, {
locationString,
}) {
const REDUCERS = {
[PROP_TYPES._shape]: createObjectReducer,
[PROP_TYPES._array]: createArrayReducer,
[PROP_TYPES._boolean]: () => {},
[PROP_TYPES._string]: () => {},
[PROP_TYPES._number]: () => {},
};
const { structure } = reducerDescriptor();
const { type } = structure();
let reducerFn = null;
if (type === PROP_TYPES._shape) reducerFn = createObjectReducer;
return {
reducerFn,
reducerFn: REDUCERS[type],
reducerStructureDescriptor: structure,
locationString,
};
@ -59,7 +53,7 @@ function callReducer({ reducerFn, reducerStructureDescriptor, locationString } =
export const createReducer = compose(callReducer, determineReducerType);
export function createReducerBehaviors(behaviorsConfig: ReducerBehaviorsConfig, locationString: string): ReducerBehaviors {
export function createReducerBehaviors(behaviorsConfig: any, locationString: string): any {
//Take a reducer behavior config object, and create the reducer behaviors using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo,
@ -68,10 +62,13 @@ export function createReducerBehaviors(behaviorsConfig: ReducerBehaviorsConfig,
}
export function calculateDefaults(typeDescription: StructureType | PrimitiveType) {
const { type, structure = {}} = typeDescription;
if ([PROP_TYPES.array, PROP_TYPES.shape].find(type))
return reduce(reducerStructure, (memo, propValue, propName) => ({
const { type, structure = {}, defaultValue = '' } = typeDescription();
const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1;
if (!complex) return defaultValue;
return reduce(structure, (memo, propValue, propName) => ({
...memo,
[propName]: propValue().defaultValue,
[propName]: calculateDefaults(propValue),
}), {});
}

View File

@ -2,7 +2,7 @@
//==============================
// Flow imports
//==============================
import type { ShapeStructure, StructureType } from '../structure';
import type { StructureType } from '../structure';
//==============================
// Flow types
@ -10,6 +10,7 @@ import type { ShapeStructure, StructureType } from '../structure';
export type ArrayReducerAction = {
type: string,
payload: any,
index: number,
};
export type ArrayReducer = (state: Array<any>, action: ArrayReducerAction) => Array<any>;
export type ArrayReducerBehavior = (state: Array<any>, payload: any, initialState: Array<any>, index: number | void) => Array<any>;
@ -35,13 +36,13 @@ export type ArraySelector = (state: Object) => Array<any>;
//==============================
// JS imports
//==============================
import { reduce, isArray, isObject, isNull } from 'lodash';
import { validateArray } from '../validatePayload';
import { reduce, isArray, isNumber, isObject } 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)) {
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}.`);
}
return true;
@ -49,34 +50,34 @@ function checkIndex(state: Object, payload: any, behaviorName: string): boolean
const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
updateAtIndex: {
action(value) { return value },
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
reducer(state, payload, initialState, index = -1) {
checkIndex(index, payload, 'updateAtIndex');
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');
checkIndex(index, payload, 'resetAtIndex');
return updateAtIndex(state, initialState, index);
}
},
removeAtIndex: {
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
checkIndex(index, payload, 'removeAtIndex');
return removeAtIndex(state, index);
}
},
replaceAtIndex: {
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
checkIndex(index, payload, 'replaceAtIndex');
return updateAtIndex(state, payload, index);
}
},
replace: {
action(value) {
if(!isArray(value)) throw new Error('An array must be provided when replacing an array');
return value;
},
reducer(state, payload) {
return payload;
@ -89,8 +90,6 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
},
};
//TODO: All the array functionality!
export function createArrayReducer(reducerShape: StructureType, {
locationString
}: ArrayReducerOptions = {}) {
@ -100,30 +99,25 @@ export function createArrayReducer(reducerShape: StructureType, {
};
}
function createReducer(arrayTypeDescription: StructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
const initialState = calculateDefaults(arrayTypeDescription);
return (state = initialState, { type, payload }: ArrayReducerAction) => {
return (state: Array<any> = [], { 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, validateArray(arrayTypeDescription, payload), initialState);
return behaviors[type](state, payload, [], index);
}
}
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string, defaultPayload: any): ArrayActions {
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) => ({
...memo,
[name]: (value: Object) => ({
[name]: (value: Array<any>, index: ?number) => ({
type: `${locationString}.${name}`,
payload: (behavior.action || (() => defaultPayload))(value) || {}
payload: (behavior.action || (() => value))(value) || [],
index,
})
}), {});
}

View File

@ -8,7 +8,8 @@ export type ShapeStructure = {
}
export type StructureType = () => {
type: string,
structure: ShapeStructure | StructureType | PrimitiveType
structure: ShapeStructure | StructureType | PrimitiveType,
defaultValue?: any,
};
export type PrimitiveType = () => {
type: $Keys<typeof PROP_TYPES>,

View File

@ -1,5 +1,6 @@
//@flow
export function updateAtIndex(array: Array<any>, value: any, index: number): Array<any> {
export function updateAtIndex(array: Array<any>, value: any, index: ?number): Array<any> {
if (index === undefined || index === null) throw new Error('Must provide an index to updateAtIndex');
return [
...array.slice(0,index),
value,
@ -7,7 +8,8 @@ export function updateAtIndex(array: Array<any>, value: any, index: number): Arr
];
}
export function removeAtIndex(array: Array<any>, index: number): Array<any> {
export function removeAtIndex(array: Array<any>, index: ?number): Array<any> {
if (index === undefined || index === null) throw new Error('Must provide an index to removeAtIndex');
return [
...array.slice(0, index),
...array.slice(index + 1),

View File

@ -52,6 +52,7 @@ export function validatePrimitive(primitive: any, value: mixed): mixed {
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.
if (!Array.isArray(value)) return [];
const elementStructure = arrayStructure().structure;
const elementType = elementStructure().type;
return value.map(element => getTypeValidation(elementType)(elementStructure, element));