diff --git a/.eslintrc.js b/.eslintrc.js index c2d15fe..863753f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, } }; \ No newline at end of file diff --git a/.flowconfig b/.flowconfig index b2c435d..7185812 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1 +1,2 @@ -[ignore] \ No newline at end of file +[ignore] +.*/__tests__/.* \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0095d81..c86b444 100644 --- a/src/index.js +++ b/src/index.js @@ -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)); diff --git a/src/redux-arg/__tests__/reducers.test.js b/src/redux-arg/__tests__/reducers.test.js new file mode 100644 index 0000000..9e63efa --- /dev/null +++ b/src/redux-arg/__tests__/reducers.test.js @@ -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' + } + }); + }); + }); + +}); \ No newline at end of file diff --git a/src/redux-arg/buildReducers.js b/src/redux-arg/buildReducers.js index 5334138..1aedb52 100644 --- a/src/redux-arg/buildReducers.js +++ b/src/redux-arg/buildReducers.js @@ -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 diff --git a/src/redux-arg/reducers.js b/src/redux-arg/reducers.js index 69aa14c..f17d0b9 100644 --- a/src/redux-arg/reducers.js +++ b/src/redux-arg/reducers.js @@ -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), }), {}); } diff --git a/src/redux-arg/reducers/arrayReducer.js b/src/redux-arg/reducers/arrayReducer.js index 7e55b7d..eefea15 100644 --- a/src/redux-arg/reducers/arrayReducer.js +++ b/src/redux-arg/reducers/arrayReducer.js @@ -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, action: ArrayReducerAction) => Array; export type ArrayReducerBehavior = (state: Array, payload: any, initialState: Array, index: number | void) => Array; @@ -35,13 +36,13 @@ export type ArraySelector = (state: Object) => Array; //============================== // 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 = [], { 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, index: ?number) => ({ type: `${locationString}.${name}`, - payload: (behavior.action || (() => defaultPayload))(value) || {} + payload: (behavior.action || (() => value))(value) || [], + index, }) }), {}); } diff --git a/src/redux-arg/structure.js b/src/redux-arg/structure.js index 5db89ca..db885fe 100644 --- a/src/redux-arg/structure.js +++ b/src/redux-arg/structure.js @@ -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, diff --git a/src/redux-arg/utils/arrayUtils.js b/src/redux-arg/utils/arrayUtils.js index b2eeb9a..c64fc91 100644 --- a/src/redux-arg/utils/arrayUtils.js +++ b/src/redux-arg/utils/arrayUtils.js @@ -1,5 +1,6 @@ //@flow -export function updateAtIndex(array: Array, value: any, index: number): Array { +export function updateAtIndex(array: Array, value: any, index: ?number): Array { + 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, value: any, index: number): Arr ]; } -export function removeAtIndex(array: Array, index: number): Array { +export function removeAtIndex(array: Array, index: ?number): Array { + if (index === undefined || index === null) throw new Error('Must provide an index to removeAtIndex'); return [ ...array.slice(0, index), ...array.slice(index + 1), diff --git a/src/redux-arg/validatePayload.js b/src/redux-arg/validatePayload.js index c065c65..4753e45 100644 --- a/src/redux-arg/validatePayload.js +++ b/src/redux-arg/validatePayload.js @@ -52,6 +52,7 @@ export function validatePrimitive(primitive: any, value: mixed): mixed { 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. + if (!Array.isArray(value)) return []; const elementStructure = arrayStructure().structure; const elementType = elementStructure().type; return value.map(element => getTypeValidation(elementType)(elementStructure, element));