refactor(prettier): Apply prettier styling
This commit is contained in:
parent
13f5bf1649
commit
4925c77728
|
@ -1,164 +1,203 @@
|
|||
import {
|
||||
buildStoreChunk,
|
||||
} from '../buildStoreChunk';
|
||||
import {
|
||||
Types,
|
||||
} from '../structure';
|
||||
import {
|
||||
createCombinedAction,
|
||||
} from '../reducers/batchUpdates';
|
||||
import {
|
||||
createStore,
|
||||
combineReducers,
|
||||
} from 'redux';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import { buildStoreChunk } from "../buildStoreChunk";
|
||||
import { Types } from "../structure";
|
||||
import { createCombinedAction } from "../reducers/batchUpdates";
|
||||
import { createStore, combineReducers } from "redux";
|
||||
import isFunction from "lodash/isFunction";
|
||||
|
||||
describe("buildStoreChunk", () => {
|
||||
it("Will throw error if a structure is not defined", () => {
|
||||
expect(() => buildStoreChunk("toast")).toThrowError(/structure/);
|
||||
});
|
||||
it("Will accept a single reducer (no nesting)", () => {
|
||||
expect(
|
||||
Object.keys(buildStoreChunk("toast", Types.reducer(Types.string())))
|
||||
).toEqual(["reducers", "actions", "selectors"]);
|
||||
});
|
||||
it("Will return an object containing reducers, actions, and selectors as the result", () => {
|
||||
expect(
|
||||
Object.keys(
|
||||
buildStoreChunk("toast", {
|
||||
example: Types.reducer(Types.string())
|
||||
})
|
||||
)
|
||||
).toEqual(["reducers", "actions", "selectors"]);
|
||||
});
|
||||
|
||||
describe('buildStoreChunk', () => {
|
||||
it('Will throw error if a structure is not defined', () => {
|
||||
expect(() => buildStoreChunk('toast')).toThrowError(/structure/);
|
||||
describe("Resulting chunk", () => {
|
||||
const chunk = buildStoreChunk("example", {
|
||||
nested1: Types.reducer(Types.string("foo")),
|
||||
nested2: Types.reducer(
|
||||
Types.shape({
|
||||
foo: Types.number(),
|
||||
bar: Types.string()
|
||||
})
|
||||
),
|
||||
nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])),
|
||||
nested4: Types.reducer({
|
||||
innerNested1: Types.reducer(Types.string("bar")),
|
||||
innerNested2: Types.reducer({
|
||||
innerNested3: Types.reducer(Types.string("baz"))
|
||||
})
|
||||
}),
|
||||
nested5: Types.reducer(
|
||||
Types.shape({
|
||||
arrayExample: Types.arrayOf(Types.string())
|
||||
})
|
||||
)
|
||||
});
|
||||
it('Will accept a single reducer (no nesting)', () => {
|
||||
expect(Object.keys(buildStoreChunk('toast', Types.reducer(Types.string()) )) )
|
||||
.toEqual(['reducers', 'actions', 'selectors']);
|
||||
});
|
||||
it('Will return an object containing reducers, actions, and selectors as the result', () => {
|
||||
expect(Object.keys(buildStoreChunk('toast', {
|
||||
example: Types.reducer(Types.string()),
|
||||
}))).toEqual(['reducers', 'actions', 'selectors']);
|
||||
const nonNestedChunk = buildStoreChunk(
|
||||
"example2",
|
||||
Types.reducer(Types.string("foo"))
|
||||
);
|
||||
|
||||
describe("Selectors", () => {
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...chunk.reducers
|
||||
})
|
||||
);
|
||||
|
||||
it("Selectors object has the correct top level structure for a nested chunk", () => {
|
||||
expect(Object.keys(chunk.selectors)).toEqual([
|
||||
"nested1",
|
||||
"nested2",
|
||||
"nested3",
|
||||
"nested4",
|
||||
"nested5"
|
||||
]);
|
||||
});
|
||||
it("Selectors object is a function for a non-nested chunk", () => {
|
||||
expect(isFunction(nonNestedChunk.selectors)).toBe(true);
|
||||
});
|
||||
it("Nested selectors object has the correct structure for a defined reducer", () => {
|
||||
expect(Object.keys(chunk.selectors.nested4)).toEqual([
|
||||
"innerNested1",
|
||||
"innerNested2"
|
||||
]);
|
||||
});
|
||||
it("Selector returns correct value", () => {
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual("foo");
|
||||
});
|
||||
it("Nested selector returns correct value", () => {
|
||||
expect(chunk.selectors.nested4.innerNested1(store.getState())).toEqual(
|
||||
"bar"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resulting chunk', () => {
|
||||
const chunk = buildStoreChunk('example', {
|
||||
nested1: Types.reducer(Types.string('foo')),
|
||||
nested2: Types.reducer(Types.shape({
|
||||
foo: Types.number(),
|
||||
bar: Types.string(),
|
||||
})),
|
||||
nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])),
|
||||
nested4: Types.reducer({
|
||||
innerNested1: Types.reducer(Types.string('bar')),
|
||||
innerNested2: Types.reducer({
|
||||
innerNested3: Types.reducer(Types.string('baz')),
|
||||
}),
|
||||
}),
|
||||
nested5: Types.reducer(Types.shape({
|
||||
arrayExample: Types.arrayOf(Types.string()),
|
||||
})),
|
||||
});
|
||||
const nonNestedChunk = buildStoreChunk('example2', Types.reducer(Types.string('foo')));
|
||||
|
||||
describe('Selectors', () => {
|
||||
const store = createStore(combineReducers({
|
||||
...chunk.reducers,
|
||||
}));
|
||||
|
||||
it('Selectors object has the correct top level structure for a nested chunk', () => {
|
||||
expect(Object.keys(chunk.selectors)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']);
|
||||
});
|
||||
it('Selectors object is a function for a non-nested chunk', () => {
|
||||
expect(isFunction(nonNestedChunk.selectors)).toBe(true);
|
||||
});
|
||||
it('Nested selectors object has the correct structure for a defined reducer', () => {
|
||||
expect(Object.keys(chunk.selectors.nested4)).toEqual(['innerNested1', 'innerNested2']);
|
||||
});
|
||||
it('Selector returns correct value', () => {
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual('foo');
|
||||
});
|
||||
it('Nested selector returns correct value', () => {
|
||||
expect(chunk.selectors.nested4.innerNested1(store.getState())).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('Actions object has the correct top level structure for a nested chunk', () => {
|
||||
expect(Object.keys(chunk.actions)).toEqual(['nested1', 'nested2', 'nested3', 'nested4', 'nested5']);
|
||||
});
|
||||
it('Actions object has the correct top level structure for a non nested chunk', () => {
|
||||
expect(Object.keys(nonNestedChunk.actions)).toEqual(['replace', 'reset']);
|
||||
});
|
||||
it('Nested actions object has the correct structure for a chunk', () => {
|
||||
expect(Object.keys(chunk.actions.nested4)).toEqual(['innerNested1', 'innerNested2']);
|
||||
});
|
||||
it('Replace actions return an object that contains a type and payload', () => {
|
||||
expect(Object.keys(chunk.actions.nested1.replace('bar'))).toEqual(['type', 'payload']);
|
||||
expect(Object.keys(chunk.actions.nested2.replace({}))).toEqual(['type', 'payload']);
|
||||
expect(Object.keys(chunk.actions.nested3.replace([]))).toEqual(['type', 'payload', 'index']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Combined actions and selectors (nested chunk)', () => {
|
||||
const store = createStore(combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers,
|
||||
}));
|
||||
|
||||
it('Dispatching an action should correctly update the store', () => {
|
||||
store.dispatch(chunk.actions.nested1.replace('bar'));
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual('bar');
|
||||
|
||||
store.dispatch(chunk.actions.nested1.reset());
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual('foo');
|
||||
});
|
||||
|
||||
it('Dispatching an empty array property should replace existing array', () => {
|
||||
store.dispatch(chunk.actions.nested5.replace({ arrayExample: ['2'] }));
|
||||
store.dispatch(chunk.actions.nested5.update({ arrayExample: [] }));
|
||||
expect(chunk.selectors.nested5(store.getState())).toEqual({
|
||||
arrayExample: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Combined actions and selectors (non nested chunk)', () => {
|
||||
const store = createStore(combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers,
|
||||
}));
|
||||
|
||||
it('Dispatching an action should correctly update the store', () => {
|
||||
store.dispatch(nonNestedChunk.actions.replace('bar'));
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual('bar');
|
||||
|
||||
store.dispatch(nonNestedChunk.actions.reset());
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Combined actions', () => {
|
||||
const store = createStore(combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers,
|
||||
}));
|
||||
|
||||
it('Dispatching a createCombinedAction updates the store correctly', () => {
|
||||
store.dispatch(createCombinedAction({
|
||||
name: 'batchUpdateFunsies',
|
||||
actions: [
|
||||
nonNestedChunk.actions.replace('bar'),
|
||||
chunk.actions.nested2.update({
|
||||
foo: 4,
|
||||
}),
|
||||
chunk.actions.nested2.update({
|
||||
bar: 'boop!',
|
||||
}),
|
||||
chunk.actions.nested3.replace([
|
||||
4,5,6
|
||||
]),
|
||||
chunk.actions.nested3.removeAtIndex(1),
|
||||
],
|
||||
}));
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual('bar');
|
||||
expect(chunk.selectors.nested2(store.getState())).toEqual({
|
||||
foo: 4,
|
||||
bar: 'boop!',
|
||||
});
|
||||
expect(chunk.selectors.nested3(store.getState())).toEqual([
|
||||
4,6,
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("Actions", () => {
|
||||
it("Actions object has the correct top level structure for a nested chunk", () => {
|
||||
expect(Object.keys(chunk.actions)).toEqual([
|
||||
"nested1",
|
||||
"nested2",
|
||||
"nested3",
|
||||
"nested4",
|
||||
"nested5"
|
||||
]);
|
||||
});
|
||||
it("Actions object has the correct top level structure for a non nested chunk", () => {
|
||||
expect(Object.keys(nonNestedChunk.actions)).toEqual([
|
||||
"replace",
|
||||
"reset"
|
||||
]);
|
||||
});
|
||||
it("Nested actions object has the correct structure for a chunk", () => {
|
||||
expect(Object.keys(chunk.actions.nested4)).toEqual([
|
||||
"innerNested1",
|
||||
"innerNested2"
|
||||
]);
|
||||
});
|
||||
it("Replace actions return an object that contains a type and payload", () => {
|
||||
expect(Object.keys(chunk.actions.nested1.replace("bar"))).toEqual([
|
||||
"type",
|
||||
"payload"
|
||||
]);
|
||||
expect(Object.keys(chunk.actions.nested2.replace({}))).toEqual([
|
||||
"type",
|
||||
"payload"
|
||||
]);
|
||||
expect(Object.keys(chunk.actions.nested3.replace([]))).toEqual([
|
||||
"type",
|
||||
"payload",
|
||||
"index"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Combined actions and selectors (nested chunk)", () => {
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers
|
||||
})
|
||||
);
|
||||
|
||||
it("Dispatching an action should correctly update the store", () => {
|
||||
store.dispatch(chunk.actions.nested1.replace("bar"));
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual("bar");
|
||||
|
||||
store.dispatch(chunk.actions.nested1.reset());
|
||||
expect(chunk.selectors.nested1(store.getState())).toEqual("foo");
|
||||
});
|
||||
|
||||
it("Dispatching an empty array property should replace existing array", () => {
|
||||
store.dispatch(chunk.actions.nested5.replace({ arrayExample: ["2"] }));
|
||||
store.dispatch(chunk.actions.nested5.update({ arrayExample: [] }));
|
||||
expect(chunk.selectors.nested5(store.getState())).toEqual({
|
||||
arrayExample: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Combined actions and selectors (non nested chunk)", () => {
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers
|
||||
})
|
||||
);
|
||||
|
||||
it("Dispatching an action should correctly update the store", () => {
|
||||
store.dispatch(nonNestedChunk.actions.replace("bar"));
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual("bar");
|
||||
|
||||
store.dispatch(nonNestedChunk.actions.reset());
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual("foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Combined actions", () => {
|
||||
const store = createStore(
|
||||
combineReducers({
|
||||
...chunk.reducers,
|
||||
...nonNestedChunk.reducers
|
||||
})
|
||||
);
|
||||
|
||||
it("Dispatching a createCombinedAction updates the store correctly", () => {
|
||||
store.dispatch(
|
||||
createCombinedAction({
|
||||
name: "batchUpdateFunsies",
|
||||
actions: [
|
||||
nonNestedChunk.actions.replace("bar"),
|
||||
chunk.actions.nested2.update({
|
||||
foo: 4
|
||||
}),
|
||||
chunk.actions.nested2.update({
|
||||
bar: "boop!"
|
||||
}),
|
||||
chunk.actions.nested3.replace([4, 5, 6]),
|
||||
chunk.actions.nested3.removeAtIndex(1)
|
||||
]
|
||||
})
|
||||
);
|
||||
expect(nonNestedChunk.selectors(store.getState())).toEqual("bar");
|
||||
expect(chunk.selectors.nested2(store.getState())).toEqual({
|
||||
foo: 4,
|
||||
bar: "boop!"
|
||||
});
|
||||
expect(chunk.selectors.nested3(store.getState())).toEqual([4, 6]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,115 +1,138 @@
|
|||
//@flow
|
||||
import { Types, PROP_TYPES } from "../structure";
|
||||
import {
|
||||
Types,
|
||||
PROP_TYPES,
|
||||
} from '../structure';
|
||||
import {
|
||||
calculateDefaults,
|
||||
determineReducerType,
|
||||
callReducer,
|
||||
createReducerBehaviors,
|
||||
REDUCER_CREATOR_MAPPING,
|
||||
} from '../reducers';
|
||||
import forEach from 'lodash/forEach';
|
||||
import omit from 'lodash/omit';
|
||||
calculateDefaults,
|
||||
determineReducerType,
|
||||
callReducer,
|
||||
createReducerBehaviors,
|
||||
REDUCER_CREATOR_MAPPING
|
||||
} from "../reducers";
|
||||
import forEach from "lodash/forEach";
|
||||
import omit from "lodash/omit";
|
||||
|
||||
describe('reducers', () => {
|
||||
|
||||
describe('calculateDefaults', () => {
|
||||
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'
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("reducers", () => {
|
||||
describe("calculateDefaults", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
describe('determineReducerType', () => {
|
||||
it('should return the correct creator function for the default mapping', () => {
|
||||
forEach({ custom: Types.custom(), ...omit(Types, 'reducer', 'wildcardKey', 'custom') }, structureType => {
|
||||
const returnVal = determineReducerType(Types.reducer(structureType()), {
|
||||
name: 'toast',
|
||||
locationString: 'toasty',
|
||||
});
|
||||
|
||||
expect({
|
||||
...returnVal,
|
||||
reducerFn: returnVal.reducerFn.name,
|
||||
reducerStructureDescriptor: returnVal.reducerStructureDescriptor.name,
|
||||
}).toEqual({
|
||||
name: 'toast',
|
||||
reducerFn: REDUCER_CREATOR_MAPPING[structureType()().type].name,
|
||||
reducerStructureDescriptor: '', //The internal functions should be anonymous
|
||||
locationString: 'toasty',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the type provided does not match any in the mapping', () => {
|
||||
expect(() => determineReducerType(Types.reducer(Types.string()), {
|
||||
name: 'toast',
|
||||
locationString: 'toasty',
|
||||
reducerCreatorMapping: omit(REDUCER_CREATOR_MAPPING, PROP_TYPES._string),
|
||||
})).toThrowError(/createReducer/)
|
||||
});
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
describe('callReducer', () => {
|
||||
it('should call the provided reducer with the structure description, location string, and name', () => {
|
||||
expect(callReducer({
|
||||
reducerStructureDescriptor: 'foo',
|
||||
name: 'toast',
|
||||
locationString: 'toasty',
|
||||
reducerFn: (reducerStructureDescriptor, { locationString, name }) => ({ reducerStructureDescriptor, locationString, name })
|
||||
})).toEqual({
|
||||
reducerStructureDescriptor: 'foo',
|
||||
locationString: 'toasty',
|
||||
name: 'toast',
|
||||
})
|
||||
});
|
||||
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"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineReducerType", () => {
|
||||
it("should return the correct creator function for the default mapping", () => {
|
||||
forEach(
|
||||
{
|
||||
custom: Types.custom(),
|
||||
...omit(Types, "reducer", "wildcardKey", "custom")
|
||||
},
|
||||
structureType => {
|
||||
const returnVal = determineReducerType(
|
||||
Types.reducer(structureType()),
|
||||
{
|
||||
name: "toast",
|
||||
locationString: "toasty"
|
||||
}
|
||||
);
|
||||
|
||||
expect({
|
||||
...returnVal,
|
||||
reducerFn: returnVal.reducerFn.name,
|
||||
reducerStructureDescriptor:
|
||||
returnVal.reducerStructureDescriptor.name
|
||||
}).toEqual({
|
||||
name: "toast",
|
||||
reducerFn: REDUCER_CREATOR_MAPPING[structureType()().type].name,
|
||||
reducerStructureDescriptor: "", //The internal functions should be anonymous
|
||||
locationString: "toasty"
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('createReducerBehaviors', () => {
|
||||
it('Should return only the reducers of the behavior config and prepend the locationString', () => {
|
||||
expect(createReducerBehaviors({
|
||||
'toast': {
|
||||
reducer: 'foo',
|
||||
action: 'bar',
|
||||
validate: true,
|
||||
}
|
||||
}, 'location')).toEqual({
|
||||
'location.toast': {
|
||||
reducer: 'foo',
|
||||
action: 'bar',
|
||||
validate: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
it("should throw an error if the type provided does not match any in the mapping", () => {
|
||||
expect(() =>
|
||||
determineReducerType(Types.reducer(Types.string()), {
|
||||
name: "toast",
|
||||
locationString: "toasty",
|
||||
reducerCreatorMapping: omit(
|
||||
REDUCER_CREATOR_MAPPING,
|
||||
PROP_TYPES._string
|
||||
)
|
||||
})
|
||||
).toThrowError(/createReducer/);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
describe("callReducer", () => {
|
||||
it("should call the provided reducer with the structure description, location string, and name", () => {
|
||||
expect(
|
||||
callReducer({
|
||||
reducerStructureDescriptor: "foo",
|
||||
name: "toast",
|
||||
locationString: "toasty",
|
||||
reducerFn: (
|
||||
reducerStructureDescriptor,
|
||||
{ locationString, name }
|
||||
) => ({ reducerStructureDescriptor, locationString, name })
|
||||
})
|
||||
).toEqual({
|
||||
reducerStructureDescriptor: "foo",
|
||||
locationString: "toasty",
|
||||
name: "toast"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createReducerBehaviors", () => {
|
||||
it("Should return only the reducers of the behavior config and prepend the locationString", () => {
|
||||
expect(
|
||||
createReducerBehaviors(
|
||||
{
|
||||
toast: {
|
||||
reducer: "foo",
|
||||
action: "bar",
|
||||
validate: true
|
||||
}
|
||||
},
|
||||
"location"
|
||||
)
|
||||
).toEqual({
|
||||
"location.toast": {
|
||||
reducer: "foo",
|
||||
action: "bar",
|
||||
validate: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,64 +1,68 @@
|
|||
import { Types, PROP_TYPES } from '../structure';
|
||||
import { Types, PROP_TYPES } from "../structure";
|
||||
|
||||
function hasType(object, type) {
|
||||
return object.type === type;
|
||||
return object.type === type;
|
||||
}
|
||||
|
||||
describe('Type descriptions', () => {
|
||||
it('will return a function which subsequently returns an object with the correct type', () => {
|
||||
expect(hasType(Types.string()(), PROP_TYPES._string)).toBeTruthy();
|
||||
expect(hasType(Types.number()(), PROP_TYPES._number)).toBeTruthy();
|
||||
expect(hasType(Types.boolean()(), PROP_TYPES._boolean)).toBeTruthy();
|
||||
expect(hasType(Types.arrayOf()(), PROP_TYPES._array)).toBeTruthy();
|
||||
expect(hasType(Types.reducer()(), PROP_TYPES._reducer)).toBeTruthy();
|
||||
expect(hasType(Types.shape()(), PROP_TYPES._shape)).toBeTruthy();
|
||||
});
|
||||
describe("Type descriptions", () => {
|
||||
it("will return a function which subsequently returns an object with the correct type", () => {
|
||||
expect(hasType(Types.string()(), PROP_TYPES._string)).toBeTruthy();
|
||||
expect(hasType(Types.number()(), PROP_TYPES._number)).toBeTruthy();
|
||||
expect(hasType(Types.boolean()(), PROP_TYPES._boolean)).toBeTruthy();
|
||||
expect(hasType(Types.arrayOf()(), PROP_TYPES._array)).toBeTruthy();
|
||||
expect(hasType(Types.reducer()(), PROP_TYPES._reducer)).toBeTruthy();
|
||||
expect(hasType(Types.shape()(), PROP_TYPES._shape)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('will return the standard default values when none provided', () => {
|
||||
expect(Types.string()().defaultValue).toBe('');
|
||||
expect(Types.number()().defaultValue).toBe(0);
|
||||
expect(Types.boolean()().defaultValue).toBe(false);
|
||||
expect(Types.arrayOf()().defaultValue).toEqual([]);
|
||||
expect(Types.custom()()().defaultValue).toBeUndefined();
|
||||
});
|
||||
it("will return the standard default values when none provided", () => {
|
||||
expect(Types.string()().defaultValue).toBe("");
|
||||
expect(Types.number()().defaultValue).toBe(0);
|
||||
expect(Types.boolean()().defaultValue).toBe(false);
|
||||
expect(Types.arrayOf()().defaultValue).toEqual([]);
|
||||
expect(Types.custom()()().defaultValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('will return the default value provided (except for reducer and shape)', () => {
|
||||
expect(Types.string('foo')().defaultValue).toBe('foo');
|
||||
expect(Types.number(5)().defaultValue).toBe(5);
|
||||
expect(Types.boolean(true)().defaultValue).toBe(true);
|
||||
expect(Types.arrayOf(Types.number(), [1, 2, 3])().defaultValue).toEqual([1, 2, 3]);
|
||||
expect(Types.custom()('foo')().defaultValue).toBe('foo');
|
||||
});
|
||||
it("will return the default value provided (except for reducer and shape)", () => {
|
||||
expect(Types.string("foo")().defaultValue).toBe("foo");
|
||||
expect(Types.number(5)().defaultValue).toBe(5);
|
||||
expect(Types.boolean(true)().defaultValue).toBe(true);
|
||||
expect(Types.arrayOf(Types.number(), [1, 2, 3])().defaultValue).toEqual([
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]);
|
||||
expect(Types.custom()("foo")().defaultValue).toBe("foo");
|
||||
});
|
||||
|
||||
it('will return the correct typeofValue (for string, number, and boolean)', () => {
|
||||
expect(Types.string()().typeofValue).toBe('string');
|
||||
expect(Types.number()().typeofValue).toBe('number');
|
||||
expect(Types.boolean()().typeofValue).toBe('boolean');
|
||||
});
|
||||
it("will return the correct typeofValue (for string, number, and boolean)", () => {
|
||||
expect(Types.string()().typeofValue).toBe("string");
|
||||
expect(Types.number()().typeofValue).toBe("number");
|
||||
expect(Types.boolean()().typeofValue).toBe("boolean");
|
||||
});
|
||||
|
||||
it('will return the correct structure (for arrayOf, reducer, and shape)', () => {
|
||||
const structureTest = Types.string();
|
||||
expect(Types.shape(structureTest)().structure).toEqual(structureTest);
|
||||
expect(Types.arrayOf(structureTest)().structure).toEqual(structureTest);
|
||||
expect(Types.reducer(structureTest)().structure).toEqual(structureTest);
|
||||
});
|
||||
it("will return the correct structure (for arrayOf, reducer, and shape)", () => {
|
||||
const structureTest = Types.string();
|
||||
expect(Types.shape(structureTest)().structure).toEqual(structureTest);
|
||||
expect(Types.arrayOf(structureTest)().structure).toEqual(structureTest);
|
||||
expect(Types.reducer(structureTest)().structure).toEqual(structureTest);
|
||||
});
|
||||
|
||||
it('custom value correctly exposes the validator and validation error message properties', () => {
|
||||
const customValidator = () => false;
|
||||
const customErrorMessage = () => 'Hai!';
|
||||
const customType = Types.custom({
|
||||
validator: customValidator,
|
||||
validationErrorMessage: customErrorMessage,
|
||||
})()();
|
||||
it("custom value correctly exposes the validator and validation error message properties", () => {
|
||||
const customValidator = () => false;
|
||||
const customErrorMessage = () => "Hai!";
|
||||
const customType = Types.custom({
|
||||
validator: customValidator,
|
||||
validationErrorMessage: customErrorMessage
|
||||
})()();
|
||||
|
||||
expect(customType.validator).toEqual(customValidator);
|
||||
expect(customType.validationErrorMessage).toEqual(customErrorMessage);
|
||||
});
|
||||
expect(customType.validator).toEqual(customValidator);
|
||||
expect(customType.validationErrorMessage).toEqual(customErrorMessage);
|
||||
});
|
||||
|
||||
it('custom value acts the same as any when no configuration provided', () => {
|
||||
const unconfiguredCustom = Types.custom()()();
|
||||
expect(unconfiguredCustom.validator('toasty')).toBe(true);
|
||||
expect(unconfiguredCustom.validator()).toBe(true);
|
||||
expect(unconfiguredCustom.validator(null)).toBe(true);
|
||||
});
|
||||
it("custom value acts the same as any when no configuration provided", () => {
|
||||
const unconfiguredCustom = Types.custom()()();
|
||||
expect(unconfiguredCustom.validator("toasty")).toBe(true);
|
||||
expect(unconfiguredCustom.validator()).toBe(true);
|
||||
expect(unconfiguredCustom.validator(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,208 +1,245 @@
|
|||
import { Types } from '../structure';
|
||||
import { Types } from "../structure";
|
||||
import {
|
||||
validateValue,
|
||||
validateShape,
|
||||
validateArray,
|
||||
getTypeValidation,
|
||||
hasWildcardKey,
|
||||
getValueType,
|
||||
} from '../validatePayload';
|
||||
validateValue,
|
||||
validateShape,
|
||||
validateArray,
|
||||
getTypeValidation,
|
||||
hasWildcardKey,
|
||||
getValueType
|
||||
} from "../validatePayload";
|
||||
|
||||
describe('Validation functionality', () => {
|
||||
describe("Validation functionality", () => {
|
||||
describe("Primitives/Custom", () => {
|
||||
const customType = Types.custom({
|
||||
validator: value => value === 3,
|
||||
validationErrorMessage: value => `Oh noes! ${value}`
|
||||
})();
|
||||
|
||||
describe('Primitives/Custom', () => {
|
||||
const customType = Types.custom({
|
||||
validator: value => value === 3,
|
||||
validationErrorMessage: value => `Oh noes! ${ value }`,
|
||||
})();
|
||||
it("Number primitive should allow for numbers", () => {
|
||||
expect(validateValue(Types.number(), 3)).toBe(3);
|
||||
});
|
||||
it("String primitive should allow for string", () => {
|
||||
expect(validateValue(Types.string(), "toast")).toBe("toast");
|
||||
});
|
||||
it("Boolean primitive should allow for string", () => {
|
||||
expect(validateValue(Types.boolean(), true)).toBe(true);
|
||||
});
|
||||
it("Any should allow for anything", () => {
|
||||
const date = new Date();
|
||||
expect(validateValue(Types.any(), date)).toEqual(date);
|
||||
});
|
||||
it("should validate custom values using the custom validator", () => {
|
||||
expect(validateValue(customType, 3)).toBe(3);
|
||||
});
|
||||
it("should return undefined from custom validators which failed", () => {
|
||||
expect(validateValue(customType, 4)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('Number primitive should allow for numbers', () => {
|
||||
expect(validateValue(Types.number(), 3)).toBe(3);
|
||||
});
|
||||
it('String primitive should allow for string', () => {
|
||||
expect(validateValue(Types.string(), 'toast')).toBe('toast');
|
||||
});
|
||||
it('Boolean primitive should allow for string', () => {
|
||||
expect(validateValue(Types.boolean(), true)).toBe(true);
|
||||
});
|
||||
it('Any should allow for anything', () => {
|
||||
const date = new Date();
|
||||
expect(validateValue(Types.any(), date)).toEqual(date);
|
||||
});
|
||||
it('should validate custom values using the custom validator', () => {
|
||||
expect(validateValue(customType, 3)).toBe(3);
|
||||
});
|
||||
it('should return undefined from custom validators which failed', () => {
|
||||
expect(validateValue(customType, 4)).toBeUndefined();
|
||||
});
|
||||
describe("Arrays", () => {
|
||||
const testArrayStructure = Types.arrayOf(Types.string());
|
||||
it("Arrays should allow for primitives", () => {
|
||||
expect(validateArray(testArrayStructure, ["a", "b", "c", "d"])).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d"
|
||||
]);
|
||||
});
|
||||
it("Arrays should strip values for primitives which fail the test", () => {
|
||||
expect(validateArray(testArrayStructure, ["a", "b", 3, "d"])).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
"d"
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Arrays', () => {
|
||||
const testArrayStructure = Types.arrayOf(Types.string());
|
||||
it('Arrays should allow for primitives', () => {
|
||||
expect(validateArray(testArrayStructure, ['a','b','c','d']))
|
||||
.toEqual(['a','b','c','d']);
|
||||
});
|
||||
it('Arrays should strip values for primitives which fail the test', () => {
|
||||
expect(validateArray(testArrayStructure, ['a','b',3,'d']))
|
||||
.toEqual(['a','b','d']);
|
||||
});
|
||||
const testArrayStructure2 = Types.arrayOf(
|
||||
Types.shape({
|
||||
test1: Types.number()
|
||||
})
|
||||
);
|
||||
it("Arrays should allow for complex objects", () => {
|
||||
expect(
|
||||
validateArray(testArrayStructure2, [{ test1: 3 }, { test1: 4 }])
|
||||
).toEqual([{ test1: 3 }, { test1: 4 }]);
|
||||
});
|
||||
const testArrayStructure3 = Types.arrayOf(
|
||||
Types.shape({
|
||||
test1: Types.arrayOf(Types.number()),
|
||||
test2: Types.custom({ validator: value => value === "foo" })()
|
||||
})
|
||||
);
|
||||
it("Arrays should allow for complex objects - test 2", () => {
|
||||
expect(
|
||||
validateArray(testArrayStructure3, [{ test1: [3, 4, 5], test2: "foo" }])
|
||||
).toEqual([{ test1: [3, 4, 5], test2: "foo" }]);
|
||||
});
|
||||
it("Array should return an empty array if a non-array is passed", () => {
|
||||
expect(validateArray("foo")).toEqual([]);
|
||||
});
|
||||
it("Array should allow an empty array", () => {
|
||||
expect(validateArray([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const testArrayStructure2 = Types.arrayOf(Types.shape({
|
||||
test1: Types.number()
|
||||
}));
|
||||
it('Arrays should allow for complex objects', () => {
|
||||
expect(validateArray(testArrayStructure2, [{test1: 3},{test1: 4}]))
|
||||
.toEqual([{test1: 3},{test1: 4}]);
|
||||
});
|
||||
const testArrayStructure3 = Types.arrayOf(Types.shape({
|
||||
test1: Types.arrayOf(Types.number()),
|
||||
test2: Types.custom({ validator: value => value === 'foo' })(),
|
||||
}));
|
||||
it('Arrays should allow for complex objects - test 2', () => {
|
||||
expect(validateArray(testArrayStructure3, [{test1: [3,4,5], test2: 'foo' }]))
|
||||
.toEqual([{test1: [3,4,5], test2: 'foo' }]);
|
||||
});
|
||||
it('Array should return an empty array if a non-array is passed', () => {
|
||||
expect(validateArray('foo')).toEqual([]);
|
||||
});
|
||||
it('Array should allow an empty array', () => {
|
||||
expect(validateArray([])).toEqual([]);
|
||||
});
|
||||
describe("Objects", () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
test3: Types.custom({ validator: value => value !== 4 })()
|
||||
});
|
||||
it("Object of primitives should allow all props present in the structure", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure, {
|
||||
test1: "toast",
|
||||
test2: 3,
|
||||
test3: 1
|
||||
})
|
||||
).toEqual({ test1: "toast", test2: 3, test3: 1 });
|
||||
});
|
||||
it("Object of primitives should only allow for props with values which match their config", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure, { test1: 5, test2: 3, test3: 4 })
|
||||
).toEqual({ test2: 3 });
|
||||
});
|
||||
it("Object of primitives should strip any properties not part of the config", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure, {
|
||||
test1: "toast",
|
||||
test2: 3,
|
||||
toast: "bar"
|
||||
})
|
||||
).toEqual({ test1: "toast", test2: 3 });
|
||||
});
|
||||
|
||||
describe('Objects', () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
test3: Types.custom({ validator: value => value !== 4 })(),
|
||||
});
|
||||
it('Object of primitives should allow all props present in the structure', () => {
|
||||
expect(validateShape(testObjectStructure, { test1: 'toast', test2: 3, test3: 1 }))
|
||||
.toEqual({ test1: 'toast', test2: 3, test3: 1 });
|
||||
});
|
||||
it('Object of primitives should only allow for props with values which match their config', () => {
|
||||
expect(validateShape(testObjectStructure, { test1: 5, test2: 3, test3: 4 }))
|
||||
.toEqual({ test2: 3 });
|
||||
});
|
||||
it('Object of primitives should strip any properties not part of the config', () => {
|
||||
expect(validateShape(testObjectStructure, { test1: 'toast', test2: 3, toast: 'bar' }))
|
||||
.toEqual({ test1: 'toast', test2: 3 });
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: testObjectStructure,
|
||||
});
|
||||
it('Objects should allow for arbitrary nesting of objects', () => {
|
||||
expect(validateShape(testObjectStructure2, { test1: { test1: 'toast', test2: 3 } }))
|
||||
.toEqual({ test1: { test1: 'toast', test2: 3 } });
|
||||
});
|
||||
|
||||
const testObjectStructure3 = Types.shape({
|
||||
test1: Types.shape({
|
||||
test2: Types.string(),
|
||||
}),
|
||||
test2: Types.string(),
|
||||
});
|
||||
it('Objects containing objects should properly check if an object is provided', () => {
|
||||
expect(validateShape(testObjectStructure3, { test1: 'foo', test2: 'bar' })).toEqual({
|
||||
test1: {},
|
||||
test2: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const testObjectStructure4 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.string(),
|
||||
});
|
||||
const testObjectStructure5 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.any(),
|
||||
});
|
||||
it('Should, if a key is not specified, see if the key matches the wildcard type, and apply if true', () => {
|
||||
expect(validateShape(testObjectStructure4, { test1: 'foo', test2: 'bar' })).toEqual({
|
||||
test1: 'foo',
|
||||
test2: 'bar',
|
||||
});
|
||||
|
||||
expect(validateShape(testObjectStructure5, { test1: 0, test2: 'bar' })).toEqual({
|
||||
test1: 0,
|
||||
test2: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure6 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.string(),
|
||||
});
|
||||
it('Should, if a key is not specified, and does not match the wildcardKey, strip it out', () => {
|
||||
expect(validateShape(testObjectStructure6, { test1: 0, test2: 'bar' })).toEqual({
|
||||
test2: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure7 = Types.shape({
|
||||
test1: Types.arrayOf(Types.string()),
|
||||
});
|
||||
it('Should allow an empty array to be passed for an array property', () => {
|
||||
expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({
|
||||
test1: [],
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: testObjectStructure
|
||||
});
|
||||
it("Objects should allow for arbitrary nesting of objects", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure2, {
|
||||
test1: { test1: "toast", test2: 3 }
|
||||
})
|
||||
).toEqual({ test1: { test1: "toast", test2: 3 } });
|
||||
});
|
||||
|
||||
describe('Non covered types', () => {
|
||||
it('A type with no associated validation should throw an error', () => {
|
||||
expect(() => getTypeValidation('toast')).toThrowError(/validation/);
|
||||
});
|
||||
const testObjectStructure3 = Types.shape({
|
||||
test1: Types.shape({
|
||||
test2: Types.string()
|
||||
}),
|
||||
test2: Types.string()
|
||||
});
|
||||
|
||||
describe('Has wildcard value', () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
[Types.wildcardKey()]: Types.any(),
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
});
|
||||
|
||||
it('should return true if the objectStructure passed in has a wildcard key', () => {
|
||||
expect(hasWildcardKey(testObjectStructure)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if no wildcard key passed in', () => {
|
||||
expect(hasWildcardKey(testObjectStructure2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetValueType', () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
[Types.wildcardKey()]: Types.number(),
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
});
|
||||
|
||||
it('should return the correct type for a key that is present, if no wildcard present', () => {
|
||||
expect(getValueType(testObjectStructure, 'test1', false)().type).toEqual(Types.string()().type);
|
||||
});
|
||||
|
||||
it('should return the wildcard value if key not present and wildcard is', () => {
|
||||
expect(getValueType(testObjectStructure, 'test3', true)().type).toEqual(Types.number()().type);
|
||||
});
|
||||
|
||||
it('should return undefined if no wildcard or matching key', () => {
|
||||
expect(getValueType(testObjectStructure, 'test3', false)).toEqual(undefined);
|
||||
it("Objects containing objects should properly check if an object is provided", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure3, { test1: "foo", test2: "bar" })
|
||||
).toEqual({
|
||||
test1: {},
|
||||
test2: "bar"
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure4 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.string()
|
||||
});
|
||||
const testObjectStructure5 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.any()
|
||||
});
|
||||
it("Should, if a key is not specified, see if the key matches the wildcard type, and apply if true", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure4, { test1: "foo", test2: "bar" })
|
||||
).toEqual({
|
||||
test1: "foo",
|
||||
test2: "bar"
|
||||
});
|
||||
|
||||
expect(
|
||||
validateShape(testObjectStructure5, { test1: 0, test2: "bar" })
|
||||
).toEqual({
|
||||
test1: 0,
|
||||
test2: "bar"
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure6 = Types.shape({
|
||||
test2: Types.string(),
|
||||
[Types.wildcardKey()]: Types.string()
|
||||
});
|
||||
it("Should, if a key is not specified, and does not match the wildcardKey, strip it out", () => {
|
||||
expect(
|
||||
validateShape(testObjectStructure6, { test1: 0, test2: "bar" })
|
||||
).toEqual({
|
||||
test2: "bar"
|
||||
});
|
||||
});
|
||||
|
||||
const testObjectStructure7 = Types.shape({
|
||||
test1: Types.arrayOf(Types.string())
|
||||
});
|
||||
it("Should allow an empty array to be passed for an array property", () => {
|
||||
expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({
|
||||
test1: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Non covered types", () => {
|
||||
it("A type with no associated validation should throw an error", () => {
|
||||
expect(() => getTypeValidation("toast")).toThrowError(/validation/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Has wildcard value", () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
[Types.wildcardKey()]: Types.any()
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number()
|
||||
});
|
||||
|
||||
it("should return true if the objectStructure passed in has a wildcard key", () => {
|
||||
expect(hasWildcardKey(testObjectStructure)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if no wildcard key passed in", () => {
|
||||
expect(hasWildcardKey(testObjectStructure2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GetValueType", () => {
|
||||
const testObjectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
[Types.wildcardKey()]: Types.number()
|
||||
});
|
||||
|
||||
const testObjectStructure2 = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number()
|
||||
});
|
||||
|
||||
it("should return the correct type for a key that is present, if no wildcard present", () => {
|
||||
expect(getValueType(testObjectStructure, "test1", false)().type).toEqual(
|
||||
Types.string()().type
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the wildcard value if key not present and wildcard is", () => {
|
||||
expect(getValueType(testObjectStructure, "test3", true)().type).toEqual(
|
||||
Types.number()().type
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined if no wildcard or matching key", () => {
|
||||
expect(getValueType(testObjectStructure, "test3", false)).toEqual(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,127 +2,149 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { StructureType, PrimitiveType } from './structure';
|
||||
import type { PartialStoreChunk } from './reducers';
|
||||
import type { StructureType, PrimitiveType } from "./structure";
|
||||
import type { PartialStoreChunk } from "./reducers";
|
||||
|
||||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import { combineReducers } from 'redux';
|
||||
import reduce from 'lodash/reduce';
|
||||
import find from 'lodash/find';
|
||||
import omit from 'lodash/omit';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import { createReducer } from './reducers';
|
||||
import { PROP_TYPES } from './structure';
|
||||
import { combineReducers } from "redux";
|
||||
import reduce from "lodash/reduce";
|
||||
import find from "lodash/find";
|
||||
import omit from "lodash/omit";
|
||||
import isFunction from "lodash/isFunction";
|
||||
import { createReducer } from "./reducers";
|
||||
import { PROP_TYPES } from "./structure";
|
||||
|
||||
// Build a chunk of the eventual store. The selectors and actions
|
||||
// generated will specifically operate on the store chunk generated. Selectors will be
|
||||
// relative to the baseSelector provided or, if not specified, the root of the store, using
|
||||
// the name of the chunk as the base property.
|
||||
export function buildStoreChunk(name: string, structure: any, {
|
||||
export function buildStoreChunk(
|
||||
name: string,
|
||||
structure: any,
|
||||
{
|
||||
baseSelector = state => state[name],
|
||||
locationString = name,
|
||||
}: {
|
||||
locationString = name
|
||||
}: {
|
||||
baseSelector: any,
|
||||
locationString: string,
|
||||
} = {}): PartialStoreChunk {
|
||||
if (!structure) throw new Error(`The structure must be defined for a reducer! LocationString: ${ locationString }`);
|
||||
const initialMemo: PartialStoreChunk = {
|
||||
reducers: {
|
||||
[name]: {},
|
||||
},
|
||||
actions: {},
|
||||
selectors: {},
|
||||
baseSelector,
|
||||
locationString,
|
||||
name,
|
||||
};
|
||||
locationString: string
|
||||
} = {}
|
||||
): PartialStoreChunk {
|
||||
if (!structure)
|
||||
throw new Error(
|
||||
`The structure must be defined for a reducer! LocationString: ${locationString}`
|
||||
);
|
||||
const initialMemo: PartialStoreChunk = {
|
||||
reducers: {
|
||||
[name]: {}
|
||||
},
|
||||
actions: {},
|
||||
selectors: {},
|
||||
baseSelector,
|
||||
locationString,
|
||||
name
|
||||
};
|
||||
|
||||
//Build up the reducers, actions, and selectors for this level. Due to recursion,
|
||||
//these objects will be assigned to a property in the parent object, or simply
|
||||
//returned to the call site for use in the rest of the application.
|
||||
const processedStructure = determineStructureProcessing(structure, initialMemo, name);
|
||||
//Build up the reducers, actions, and selectors for this level. Due to recursion,
|
||||
//these objects will be assigned to a property in the parent object, or simply
|
||||
//returned to the call site for use in the rest of the application.
|
||||
const processedStructure = determineStructureProcessing(
|
||||
structure,
|
||||
initialMemo,
|
||||
name
|
||||
);
|
||||
|
||||
//If the location string is equal to the name passed to build store chunk, then we must be
|
||||
//at the top level. If the structure is a function (i.e. not nested reducers) then return
|
||||
//the actions, and selectors as the top level of their respective objects.
|
||||
if (isFunction(structure)) {
|
||||
return {
|
||||
reducers: processedStructure.reducers,
|
||||
actions: processedStructure.actions[name],
|
||||
selectors: processedStructure.selectors[name],
|
||||
};
|
||||
}
|
||||
|
||||
return processedStructure;
|
||||
}
|
||||
|
||||
|
||||
export function determineStructureProcessing(structure: any, initialMemo: PartialStoreChunk, name) {
|
||||
if (isFunction(structure)) return combineStoreChunkReducers(processStructure(initialMemo, structure, name));
|
||||
return combineStoreChunkReducers(reduce(structure, processStructure, initialMemo));
|
||||
}
|
||||
|
||||
|
||||
export function combineStoreChunkReducers(processedStoreChunk: PartialStoreChunk) {
|
||||
//The Redux 'combineReducers' helper function is used here to save a little bit of boilerplate.
|
||||
//This helper, if you're not aware, ensures that the correct store properties are passed to the
|
||||
//reducers assigned to those properties.
|
||||
return { ...omit(processedStoreChunk, ['baseSelector', 'locationString', 'name']), reducers: {
|
||||
[processedStoreChunk.name]: combineReducers(processedStoreChunk.reducers)
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
export function processStructure(memo: PartialStoreChunk, propValue: StructureType | PrimitiveType, propName: string) {
|
||||
//Get the structure from the propValue. In the case of 'StructureType' properties, this
|
||||
//will be some form of shape (or primitives in the case of arrays). At this point we
|
||||
//are only interested in whether or not the structure contains reducers, as that
|
||||
//has an impact on how we proceed with regards to calls.
|
||||
const { structure: propStructure } = propValue();
|
||||
const containsReducers = !!find(propStructure, v => v().type === PROP_TYPES._reducer);
|
||||
|
||||
//Create the child reducer. Depending on whether or not the current structure level contains
|
||||
//child reducers, we will either recursively call reducerBuilder, or we will call the
|
||||
//createReducer function, which will create the correct reducer for the given structure
|
||||
//(which can be either object, array, or primitive).
|
||||
let childReducer = containsReducers
|
||||
? buildStoreChunk(propName, propStructure, {
|
||||
locationString: `${memo.locationString}.${propName}`,
|
||||
baseSelector: (state: any) => memo.baseSelector(state)[propName],
|
||||
})
|
||||
: createReducer(propValue, {
|
||||
locationString: `${memo.locationString}.${propName}`,
|
||||
name: propName,
|
||||
});
|
||||
|
||||
//As the chunk is built up, we want to assign the reducers/actions created
|
||||
//by the child to a location on the reducers/actions object which will match up
|
||||
//to their location. Selectors are created at this level, as the child does not
|
||||
//need to know where it is located within the grand scheme of things.
|
||||
//If the location string is equal to the name passed to build store chunk, then we must be
|
||||
//at the top level. If the structure is a function (i.e. not nested reducers) then return
|
||||
//the actions, and selectors as the top level of their respective objects.
|
||||
if (isFunction(structure)) {
|
||||
return {
|
||||
...memo,
|
||||
reducers: {
|
||||
...memo.reducers,
|
||||
...childReducer.reducers
|
||||
},
|
||||
actions: {
|
||||
...memo.actions,
|
||||
[propName]: childReducer.actions,
|
||||
},
|
||||
selectors: {
|
||||
...memo.selectors,
|
||||
[propName]: containsReducers ? childReducer.selectors : state => memo.baseSelector(state)[propName],
|
||||
},
|
||||
reducers: processedStructure.reducers,
|
||||
actions: processedStructure.actions[name],
|
||||
selectors: processedStructure.selectors[name]
|
||||
};
|
||||
}
|
||||
|
||||
return processedStructure;
|
||||
}
|
||||
|
||||
export function determineStructureProcessing(
|
||||
structure: any,
|
||||
initialMemo: PartialStoreChunk,
|
||||
name
|
||||
) {
|
||||
if (isFunction(structure))
|
||||
return combineStoreChunkReducers(
|
||||
processStructure(initialMemo, structure, name)
|
||||
);
|
||||
return combineStoreChunkReducers(
|
||||
reduce(structure, processStructure, initialMemo)
|
||||
);
|
||||
}
|
||||
|
||||
export function combineStoreChunkReducers(
|
||||
processedStoreChunk: PartialStoreChunk
|
||||
) {
|
||||
//The Redux 'combineReducers' helper function is used here to save a little bit of boilerplate.
|
||||
//This helper, if you're not aware, ensures that the correct store properties are passed to the
|
||||
//reducers assigned to those properties.
|
||||
return {
|
||||
...omit(processedStoreChunk, ["baseSelector", "locationString", "name"]),
|
||||
reducers: {
|
||||
[processedStoreChunk.name]: combineReducers(processedStoreChunk.reducers)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function processStructure(
|
||||
memo: PartialStoreChunk,
|
||||
propValue: StructureType | PrimitiveType,
|
||||
propName: string
|
||||
) {
|
||||
//Get the structure from the propValue. In the case of 'StructureType' properties, this
|
||||
//will be some form of shape (or primitives in the case of arrays). At this point we
|
||||
//are only interested in whether or not the structure contains reducers, as that
|
||||
//has an impact on how we proceed with regards to calls.
|
||||
const { structure: propStructure } = propValue();
|
||||
const containsReducers = !!find(
|
||||
propStructure,
|
||||
v => v().type === PROP_TYPES._reducer
|
||||
);
|
||||
|
||||
//Create the child reducer. Depending on whether or not the current structure level contains
|
||||
//child reducers, we will either recursively call reducerBuilder, or we will call the
|
||||
//createReducer function, which will create the correct reducer for the given structure
|
||||
//(which can be either object, array, or primitive).
|
||||
let childReducer = containsReducers
|
||||
? buildStoreChunk(propName, propStructure, {
|
||||
locationString: `${memo.locationString}.${propName}`,
|
||||
baseSelector: (state: any) => memo.baseSelector(state)[propName]
|
||||
})
|
||||
: createReducer(propValue, {
|
||||
locationString: `${memo.locationString}.${propName}`,
|
||||
name: propName
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//As the chunk is built up, we want to assign the reducers/actions created
|
||||
//by the child to a location on the reducers/actions object which will match up
|
||||
//to their location. Selectors are created at this level, as the child does not
|
||||
//need to know where it is located within the grand scheme of things.
|
||||
return {
|
||||
...memo,
|
||||
reducers: {
|
||||
...memo.reducers,
|
||||
...childReducer.reducers
|
||||
},
|
||||
actions: {
|
||||
...memo.actions,
|
||||
[propName]: childReducer.actions
|
||||
},
|
||||
selectors: {
|
||||
...memo.selectors,
|
||||
[propName]: containsReducers
|
||||
? childReducer.selectors
|
||||
: state => memo.baseSelector(state)[propName]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
12
src/index.js
12
src/index.js
|
@ -1,9 +1,5 @@
|
|||
import { Types } from './structure';
|
||||
import { buildStoreChunk } from './buildStoreChunk';
|
||||
import { createCombinedAction } from './reducers/batchUpdates';
|
||||
import { Types } from "./structure";
|
||||
import { buildStoreChunk } from "./buildStoreChunk";
|
||||
import { createCombinedAction } from "./reducers/batchUpdates";
|
||||
|
||||
export {
|
||||
Types,
|
||||
buildStoreChunk,
|
||||
createCombinedAction,
|
||||
}
|
||||
export { Types, buildStoreChunk, createCombinedAction };
|
||||
|
|
183
src/reducers.js
183
src/reducers.js
|
@ -3,125 +3,140 @@
|
|||
// Flow imports
|
||||
//==============================
|
||||
import type {
|
||||
StructureType,
|
||||
PrimitiveType,
|
||||
ReducerType,
|
||||
PropTypeKeys,
|
||||
} from './structure';
|
||||
StructureType,
|
||||
PrimitiveType,
|
||||
ReducerType,
|
||||
PropTypeKeys
|
||||
} from "./structure";
|
||||
|
||||
//==============================
|
||||
// Flow types
|
||||
//==============================
|
||||
export type PartialStoreChunk = {
|
||||
reducers: { [key: string]: any },
|
||||
actions: { [key: string]: any },
|
||||
selectors: { [key: string]: any },
|
||||
locationString: string,
|
||||
baseSelector: () => {},
|
||||
name: string,
|
||||
reducers: { [key: string]: any },
|
||||
actions: { [key: string]: any },
|
||||
selectors: { [key: string]: any },
|
||||
locationString: string,
|
||||
baseSelector: () => {},
|
||||
name: string
|
||||
};
|
||||
|
||||
export type Selector = (state: Object) => any;
|
||||
|
||||
type CallReducerInterface = {
|
||||
name: string,
|
||||
reducerFn: () => {},
|
||||
reducerStructureDescriptor: StructureType | PrimitiveType,
|
||||
locationString: string,
|
||||
name: string,
|
||||
reducerFn: () => {},
|
||||
reducerStructureDescriptor: StructureType | PrimitiveType,
|
||||
locationString: string
|
||||
};
|
||||
|
||||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import {
|
||||
PROP_TYPES,
|
||||
} from './structure';
|
||||
import reduce from 'lodash/reduce';
|
||||
import flowRight from 'lodash/fp/flowRight';
|
||||
import { createShapeReducer } from './reducers/objectReducer';
|
||||
import { createArrayReducer } from './reducers/arrayReducer';
|
||||
import { createPrimitiveReducer } from './reducers/primitiveReducer';
|
||||
import { PROP_TYPES } from "./structure";
|
||||
import reduce from "lodash/reduce";
|
||||
import flowRight from "lodash/fp/flowRight";
|
||||
import { createShapeReducer } from "./reducers/objectReducer";
|
||||
import { createArrayReducer } from "./reducers/arrayReducer";
|
||||
import { createPrimitiveReducer } from "./reducers/primitiveReducer";
|
||||
|
||||
export const REDUCER_CREATOR_MAPPING: { [key: PropTypeKeys]: any } = {
|
||||
[PROP_TYPES._shape]: createShapeReducer,
|
||||
[PROP_TYPES._array]: createArrayReducer,
|
||||
[PROP_TYPES._boolean]: createPrimitiveReducer,
|
||||
[PROP_TYPES._string]: createPrimitiveReducer,
|
||||
[PROP_TYPES._number]: createPrimitiveReducer,
|
||||
[PROP_TYPES._any]: createPrimitiveReducer,
|
||||
[PROP_TYPES._custom]: createPrimitiveReducer,
|
||||
[PROP_TYPES._shape]: createShapeReducer,
|
||||
[PROP_TYPES._array]: createArrayReducer,
|
||||
[PROP_TYPES._boolean]: createPrimitiveReducer,
|
||||
[PROP_TYPES._string]: createPrimitiveReducer,
|
||||
[PROP_TYPES._number]: createPrimitiveReducer,
|
||||
[PROP_TYPES._any]: createPrimitiveReducer,
|
||||
[PROP_TYPES._custom]: createPrimitiveReducer
|
||||
};
|
||||
|
||||
export const createUniqueString = Math.random()
|
||||
.toString(36)
|
||||
.substring(7);
|
||||
|
||||
export const createUniqueString = Math.random().toString(36).substring(7);
|
||||
|
||||
|
||||
export function determineReducerType(reducerDescriptor: ReducerType, {
|
||||
export function determineReducerType(
|
||||
reducerDescriptor: ReducerType,
|
||||
{
|
||||
name,
|
||||
locationString,
|
||||
reducerCreatorMapping = REDUCER_CREATOR_MAPPING,
|
||||
}: {
|
||||
reducerCreatorMapping = REDUCER_CREATOR_MAPPING
|
||||
}: {
|
||||
name: string,
|
||||
locationString: string,
|
||||
reducerCreatorMapping: { [key: PropTypeKeys]: any },
|
||||
}): CallReducerInterface {
|
||||
reducerCreatorMapping: { [key: PropTypeKeys]: any }
|
||||
}
|
||||
): CallReducerInterface {
|
||||
const { structure } = reducerDescriptor();
|
||||
const { type } = structure();
|
||||
|
||||
const { structure } = reducerDescriptor();
|
||||
const { type } = structure();
|
||||
if (!reducerCreatorMapping[type])
|
||||
throw new Error(
|
||||
`Reducer type ${type} does not have a corresponding createReducer function`
|
||||
);
|
||||
|
||||
if (!reducerCreatorMapping[type]) throw new Error(`Reducer type ${type} does not have a corresponding createReducer function`);
|
||||
|
||||
return {
|
||||
name,
|
||||
reducerFn: reducerCreatorMapping[type],
|
||||
reducerStructureDescriptor: structure,
|
||||
locationString,
|
||||
};
|
||||
return {
|
||||
name,
|
||||
reducerFn: reducerCreatorMapping[type],
|
||||
reducerStructureDescriptor: structure,
|
||||
locationString
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function callReducer({
|
||||
name,
|
||||
reducerFn,
|
||||
reducerStructureDescriptor,
|
||||
locationString
|
||||
name,
|
||||
reducerFn,
|
||||
reducerStructureDescriptor,
|
||||
locationString
|
||||
}: CallReducerInterface) {
|
||||
return reducerFn(reducerStructureDescriptor, {
|
||||
locationString,
|
||||
name,
|
||||
});
|
||||
return reducerFn(reducerStructureDescriptor, {
|
||||
locationString,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function createReducerBehaviors(behaviorsConfig: { [key: string]: { reducer: () => {} } }, locationString: string): any {
|
||||
//Take a reducer behavior config object, and create the reducer behaviors using the location string.
|
||||
//This is necessary since all action types are effectively global when Redux processes an action
|
||||
//(i.e. every reducer will be ran using the action object). Therefore we need to ensure that all
|
||||
//actions only result in the specific reducer performing a change. Actions are also generated using
|
||||
//the location string/name combination, so will match up 1:1.
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
[`${locationString}.${name}`]: behavior,
|
||||
}), {});
|
||||
export function createReducerBehaviors(
|
||||
behaviorsConfig: { [key: string]: { reducer: () => {} } },
|
||||
locationString: string
|
||||
): any {
|
||||
//Take a reducer behavior config object, and create the reducer behaviors using the location string.
|
||||
//This is necessary since all action types are effectively global when Redux processes an action
|
||||
//(i.e. every reducer will be ran using the action object). Therefore we need to ensure that all
|
||||
//actions only result in the specific reducer performing a change. Actions are also generated using
|
||||
//the location string/name combination, so will match up 1:1.
|
||||
return reduce(
|
||||
behaviorsConfig,
|
||||
(memo, behavior, name) => ({
|
||||
...memo,
|
||||
[`${locationString}.${name}`]: behavior
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateDefaults(
|
||||
typeDescription: StructureType | PrimitiveType
|
||||
) {
|
||||
//Using the structure of a type, calculate the default for that type.
|
||||
//Types can take two forms; a 'StructureType' and a 'PrimitiveType'. The former
|
||||
//can (and usually does) contain nested type descriptions, so we need to recurse
|
||||
//through the definition until defaults are found, and build up the corresponding
|
||||
//structure.
|
||||
const { type, structure = {}, defaultValue = "" } = typeDescription();
|
||||
const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1;
|
||||
|
||||
export function calculateDefaults(typeDescription: StructureType | PrimitiveType) {
|
||||
//Using the structure of a type, calculate the default for that type.
|
||||
//Types can take two forms; a 'StructureType' and a 'PrimitiveType'. The former
|
||||
//can (and usually does) contain nested type descriptions, so we need to recurse
|
||||
//through the definition until defaults are found, and build up the corresponding
|
||||
//structure.
|
||||
const { type, structure = {}, defaultValue = '' } = typeDescription();
|
||||
const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1;
|
||||
if (!complex) return defaultValue;
|
||||
|
||||
if (!complex) return defaultValue;
|
||||
|
||||
return reduce(structure, (memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]: calculateDefaults(propValue),
|
||||
}), {});
|
||||
return reduce(
|
||||
structure,
|
||||
(memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]: calculateDefaults(propValue)
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const createReducer = flowRight(callReducer, determineReducerType);
|
||||
export const createReducer = flowRight(
|
||||
callReducer,
|
||||
determineReducerType
|
||||
);
|
||||
|
|
|
@ -2,65 +2,78 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { ArrayStructureType } from '../structure';
|
||||
import type { ArrayStructureType } from "../structure";
|
||||
|
||||
//==============================
|
||||
// Flow types
|
||||
//==============================
|
||||
export type ArrayReducerAction = {
|
||||
type: string,
|
||||
payload: any,
|
||||
index?: number,
|
||||
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) => Array<any>;
|
||||
export type ArrayReducer = (
|
||||
state: Array<any>,
|
||||
action: ArrayReducerAction
|
||||
) => Array<any>;
|
||||
export type ArrayReducerBehavior = (
|
||||
state: Array<any>,
|
||||
payload: any,
|
||||
initialState: Array<any>,
|
||||
index: number
|
||||
) => Array<any>;
|
||||
export type ArrayReducerBehaviorsConfig = {
|
||||
[key: string]: {
|
||||
action?: (value: any) => any,
|
||||
reducer: ArrayReducerBehavior,
|
||||
validate: boolean,
|
||||
}
|
||||
[key: string]: {
|
||||
action?: (value: any) => any,
|
||||
reducer: ArrayReducerBehavior,
|
||||
validate: boolean
|
||||
}
|
||||
};
|
||||
export type ArrayReducerBehaviors = {
|
||||
[key: string]: ArrayReducerBehavior,
|
||||
[key: string]: ArrayReducerBehavior
|
||||
};
|
||||
export type ArrayAction = (value: Array<any>) => { type: string, payload: any, index?: number };
|
||||
export type ArrayAction = (
|
||||
value: Array<any>
|
||||
) => { type: string, payload: any, index?: number };
|
||||
export type ArrayActions = {
|
||||
[key: string]: Array<any>
|
||||
[key: string]: Array<any>
|
||||
};
|
||||
export type ArrayReducerOptions = {
|
||||
behaviorsConfig: ArrayReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string,
|
||||
behaviorsConfig: ArrayReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string
|
||||
};
|
||||
export type ArraySelector = (state: Object) => Array<any>;
|
||||
|
||||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import isArray from 'lodash/isArray';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import { validateArray, validateShape, validateValue } from '../validatePayload';
|
||||
import { createReducerBehaviors } from '../reducers';
|
||||
import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils';
|
||||
import { PROP_TYPES } from '../structure';
|
||||
import isArray from "lodash/isArray";
|
||||
import isNumber from "lodash/isNumber";
|
||||
import {
|
||||
isCombinedAction,
|
||||
getApplicableCombinedActions
|
||||
} from './batchUpdates';
|
||||
validateArray,
|
||||
validateShape,
|
||||
validateValue
|
||||
} from "../validatePayload";
|
||||
import { createReducerBehaviors } from "../reducers";
|
||||
import { updateAtIndex, removeAtIndex } from "../utils/arrayUtils";
|
||||
import { PROP_TYPES } from "../structure";
|
||||
import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates";
|
||||
|
||||
const reduce = require('lodash/fp/reduce').convert({ cap: false });
|
||||
const reduce = require("lodash/fp/reduce").convert({ cap: false });
|
||||
|
||||
|
||||
function checkIndex(index: ?number, payload: any = '', behaviorName: string): boolean {
|
||||
if (!isNumber(index) || index === -1) {
|
||||
console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
function checkIndex(
|
||||
index: ?number,
|
||||
payload: any = "",
|
||||
behaviorName: string
|
||||
): boolean {
|
||||
if (!isNumber(index) || index === -1) {
|
||||
console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//==============================
|
||||
// Array behaviors
|
||||
// ----------------
|
||||
|
@ -71,144 +84,184 @@ function checkIndex(index: ?number, payload: any = '', behaviorName: string): bo
|
|||
// to create a few helper behaviors to aid with the most common array operations.
|
||||
//==============================
|
||||
export const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
||||
//Index specific behaviors.
|
||||
replaceAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
if (!checkIndex(index, payload, 'updateAtIndex')) return state;
|
||||
if (payload === undefined) return console.warn('Undefined was passed when updating index. Update not performed') || state;
|
||||
return updateAtIndex(state, payload, index);
|
||||
},
|
||||
validate: true,
|
||||
//Index specific behaviors.
|
||||
replaceAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
if (!checkIndex(index, payload, "updateAtIndex")) return state;
|
||||
if (payload === undefined)
|
||||
return (
|
||||
console.warn(
|
||||
"Undefined was passed when updating index. Update not performed"
|
||||
) || state
|
||||
);
|
||||
return updateAtIndex(state, payload, index);
|
||||
},
|
||||
resetAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
if (!checkIndex(index, payload, 'resetAtIndex')) return state;
|
||||
return updateAtIndex(state, initialState, index);
|
||||
},
|
||||
validate: false,
|
||||
validate: true
|
||||
},
|
||||
resetAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
if (!checkIndex(index, payload, "resetAtIndex")) return state;
|
||||
return updateAtIndex(state, initialState, index);
|
||||
},
|
||||
removeAtIndex: {
|
||||
reducer(state, index) {
|
||||
if (!checkIndex(index, '', 'removeAtIndex')) return state;
|
||||
return removeAtIndex(state, index);
|
||||
},
|
||||
validate: false,
|
||||
validate: false
|
||||
},
|
||||
removeAtIndex: {
|
||||
reducer(state, index) {
|
||||
if (!checkIndex(index, "", "removeAtIndex")) return state;
|
||||
return removeAtIndex(state, index);
|
||||
},
|
||||
//Whole array behaviors.
|
||||
replace: {
|
||||
reducer(state, payload) {
|
||||
if(!isArray(payload)) return console.warn('An array must be provided when replacing an array') || state;
|
||||
return payload;
|
||||
},
|
||||
validate: true,
|
||||
validate: false
|
||||
},
|
||||
//Whole array behaviors.
|
||||
replace: {
|
||||
reducer(state, payload) {
|
||||
if (!isArray(payload))
|
||||
return (
|
||||
console.warn("An array must be provided when replacing an array") ||
|
||||
state
|
||||
);
|
||||
return payload;
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
return initialState;
|
||||
},
|
||||
validate: false,
|
||||
validate: true
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
return initialState;
|
||||
},
|
||||
push: {
|
||||
reducer(state, payload) {
|
||||
return [...state, payload];
|
||||
},
|
||||
validate: true,
|
||||
validate: false
|
||||
},
|
||||
push: {
|
||||
reducer(state, payload) {
|
||||
return [...state, payload];
|
||||
},
|
||||
pop: {
|
||||
reducer(state) {
|
||||
return state.slice(0, -1);
|
||||
},
|
||||
validate: false,
|
||||
validate: true
|
||||
},
|
||||
pop: {
|
||||
reducer(state) {
|
||||
return state.slice(0, -1);
|
||||
},
|
||||
unshift: {
|
||||
reducer(state, payload) {
|
||||
return [payload, ...state];
|
||||
},
|
||||
validate: true,
|
||||
validate: false
|
||||
},
|
||||
unshift: {
|
||||
reducer(state, payload) {
|
||||
return [payload, ...state];
|
||||
},
|
||||
shift: {
|
||||
reducer(state) {
|
||||
return state.slice(1);
|
||||
},
|
||||
validate: false,
|
||||
}
|
||||
validate: true
|
||||
},
|
||||
shift: {
|
||||
reducer(state) {
|
||||
return state.slice(1);
|
||||
},
|
||||
validate: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function createArrayReducer(arrayTypeDescription: ArrayStructureType, {
|
||||
locationString,
|
||||
name,
|
||||
}: ArrayReducerOptions) {
|
||||
const uniqueId = Math.random().toString(36).substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(arrayTypeDescription, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, `${uniqueId}-${locationString}`))
|
||||
},
|
||||
actions: createActions(DEFAULT_ARRAY_BEHAVIORS, `${uniqueId}-${locationString}`, {}),
|
||||
};
|
||||
export function createArrayReducer(
|
||||
arrayTypeDescription: ArrayStructureType,
|
||||
{ locationString, name }: ArrayReducerOptions
|
||||
) {
|
||||
const uniqueId = Math.random()
|
||||
.toString(36)
|
||||
.substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(
|
||||
arrayTypeDescription,
|
||||
createReducerBehaviors(
|
||||
DEFAULT_ARRAY_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`
|
||||
)
|
||||
)
|
||||
},
|
||||
actions: createActions(
|
||||
DEFAULT_ARRAY_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`,
|
||||
{}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function createReducer(
|
||||
arrayTypeDescription: ArrayStructureType,
|
||||
behaviors: ArrayReducerBehaviors
|
||||
): ArrayReducer {
|
||||
//Take the initial value specified as the default for the array, then apply it, using the validation
|
||||
//when doing so. The initial value must be an array.
|
||||
const initialValue = validateArray(
|
||||
arrayTypeDescription,
|
||||
arrayTypeDescription().defaultValue
|
||||
);
|
||||
|
||||
export function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
|
||||
//Take the initial value specified as the default for the array, then apply it, using the validation
|
||||
//when doing so. The initial value must be an array.
|
||||
const initialValue = validateArray(arrayTypeDescription, arrayTypeDescription().defaultValue);
|
||||
//Return the array reducer.
|
||||
return (
|
||||
state: Array<any> = initialValue,
|
||||
{ type, payload, index }: ArrayReducerAction
|
||||
) => {
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
//Return the array reducer.
|
||||
return (state: Array<any> = initialValue, { type, payload, index }: ArrayReducerAction) => {
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
if (matchedBehaviors.length) {
|
||||
//Call every behaviour relevant to this reducer as part of this action call,
|
||||
//and merge the result (later actions take priority).
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? applyValidation(arrayTypeDescription, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialValue,
|
||||
index,
|
||||
), state)(matchedBehaviors);
|
||||
}
|
||||
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
return state;
|
||||
if (matchedBehaviors.length) {
|
||||
//Call every behaviour relevant to this reducer as part of this action call,
|
||||
//and merge the result (later actions take priority).
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce(
|
||||
(interimState, matchedBehavior) =>
|
||||
behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? applyValidation(arrayTypeDescription, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialValue,
|
||||
index
|
||||
),
|
||||
state
|
||||
)(matchedBehaviors);
|
||||
}
|
||||
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
export function applyValidation(
|
||||
arrayTypeDescription: ArrayStructureType,
|
||||
payload: any
|
||||
) {
|
||||
// Array validation is more tricky than object/primitive, as it is possible that the current
|
||||
// action may involve updating the contents of a specific array element, rather than the
|
||||
// whole array. As a result, some extra functionality is required to determine which
|
||||
// form of validation to apply.
|
||||
|
||||
export function applyValidation(arrayTypeDescription: ArrayStructureType, payload: any) {
|
||||
// Array validation is more tricky than object/primitive, as it is possible that the current
|
||||
// action may involve updating the contents of a specific array element, rather than the
|
||||
// whole array. As a result, some extra functionality is required to determine which
|
||||
// form of validation to apply.
|
||||
// First case is simple - if the action payload is an array, then we simply validate it against
|
||||
// the structure of this reducer.
|
||||
if (isArray(payload)) return validateArray(arrayTypeDescription, payload);
|
||||
|
||||
// First case is simple - if the action payload is an array, then we simply validate it against
|
||||
// the structure of this reducer.
|
||||
if (isArray(payload)) return validateArray(arrayTypeDescription, payload);
|
||||
|
||||
// If a non-array payload has been passed in, then we need to check which form of validation
|
||||
// to use, by checking the structure of the array.
|
||||
const { structure } = arrayTypeDescription();
|
||||
if (structure().type === PROP_TYPES._shape) return validateShape(structure, payload);
|
||||
return validateValue(structure, payload);
|
||||
// If a non-array payload has been passed in, then we need to check which form of validation
|
||||
// to use, by checking the structure of the array.
|
||||
const { structure } = arrayTypeDescription();
|
||||
if (structure().type === PROP_TYPES._shape)
|
||||
return validateShape(structure, payload);
|
||||
return validateValue(structure, payload);
|
||||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string): ArrayActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce((memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: Array<any>, index: ?number) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
index,
|
||||
})
|
||||
}), {})(behaviorsConfig);
|
||||
function createActions(
|
||||
behaviorsConfig: ArrayReducerBehaviorsConfig,
|
||||
locationString: string
|
||||
): ArrayActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(
|
||||
(memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: Array<any>, index: ?number) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
index
|
||||
})
|
||||
}),
|
||||
{}
|
||||
)(behaviorsConfig);
|
||||
}
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
//@flow
|
||||
import keys from 'lodash/fp/keys';
|
||||
import includes from 'lodash/fp/includes';
|
||||
import filter from 'lodash/fp/filter';
|
||||
|
||||
export const COMBINED_ACTION = '/@@redux-scc-combined-action';
|
||||
import keys from "lodash/fp/keys";
|
||||
import includes from "lodash/fp/includes";
|
||||
import filter from "lodash/fp/filter";
|
||||
|
||||
export const COMBINED_ACTION = "/@@redux-scc-combined-action";
|
||||
|
||||
type BatchUpdateInterface = {
|
||||
name?: string,
|
||||
actions: Array<{ type: string, payload: any, meta?: any }>,
|
||||
actions: Array<{ type: string, payload: any, meta?: any }>
|
||||
};
|
||||
type Behaviors = {
|
||||
[key: string]: {
|
||||
[key: string]: {
|
||||
reducer: (state: mixed, payload: mixed | void, initialState: mixed) => mixed,
|
||||
},
|
||||
};
|
||||
}
|
||||
reducer: (
|
||||
state: mixed,
|
||||
payload: mixed | void,
|
||||
initialState: mixed
|
||||
) => mixed
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createCombinedAction = ({
|
||||
name = '',
|
||||
actions,
|
||||
name = "",
|
||||
actions
|
||||
}: BatchUpdateInterface) => ({
|
||||
type: `${ name }${ COMBINED_ACTION }`,
|
||||
payload: actions,
|
||||
type: `${name}${COMBINED_ACTION}`,
|
||||
payload: actions
|
||||
});
|
||||
|
||||
|
||||
export const isCombinedAction = (actionType: string) => actionType
|
||||
? actionType.indexOf(COMBINED_ACTION) > -1
|
||||
: false;
|
||||
|
||||
export const isCombinedAction = (actionType: string) =>
|
||||
actionType ? actionType.indexOf(COMBINED_ACTION) > -1 : false;
|
||||
|
||||
export const getApplicableCombinedActions = (behaviors: Behaviors) =>
|
||||
filter(({ type }) => includes(type)(keys(behaviors)));
|
||||
filter(({ type }) => includes(type)(keys(behaviors)));
|
||||
|
|
|
@ -2,53 +2,55 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { StructureType } from '../structure';
|
||||
|
||||
import type { StructureType } from "../structure";
|
||||
|
||||
//==============================
|
||||
// Flow types
|
||||
//==============================
|
||||
export type ShapeReducerAction = {
|
||||
type: string,
|
||||
payload: Object,
|
||||
validate: boolean,
|
||||
type: string,
|
||||
payload: Object,
|
||||
validate: boolean
|
||||
};
|
||||
export type ShapeReducer = (state: Object, action: ShapeReducerAction) => Object;
|
||||
export type ShapeReducerBehavior = (state: {}, payload: Object | void, initialState: {}) => Object;
|
||||
export type ShapeReducer = (
|
||||
state: Object,
|
||||
action: ShapeReducerAction
|
||||
) => Object;
|
||||
export type ShapeReducerBehavior = (
|
||||
state: {},
|
||||
payload: Object | void,
|
||||
initialState: {}
|
||||
) => Object;
|
||||
export type ShapeReducerBehaviorsConfig = {
|
||||
[key: string]: {
|
||||
action?: (value: Object) => Object,
|
||||
reducer: ShapeReducerBehavior,
|
||||
}
|
||||
[key: string]: {
|
||||
action?: (value: Object) => Object,
|
||||
reducer: ShapeReducerBehavior
|
||||
}
|
||||
};
|
||||
export type ShapeReducerBehaviors = {
|
||||
[key: string]: ShapeReducerBehavior,
|
||||
[key: string]: ShapeReducerBehavior
|
||||
};
|
||||
export type ShapeAction = (value: Object) => { type: string, payload: Object };
|
||||
export type ShapeActions = {
|
||||
[key: string]: ShapeAction
|
||||
[key: string]: ShapeAction
|
||||
};
|
||||
export type ShapeReducerOptions = {
|
||||
behaviorsConfig: ShapeReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string,
|
||||
behaviorsConfig: ShapeReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string
|
||||
};
|
||||
|
||||
|
||||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import isObject from 'lodash/isObject';
|
||||
import omit from 'lodash/omit';
|
||||
import { validateShape } from '../validatePayload';
|
||||
import { createReducerBehaviors } from '../reducers';
|
||||
import { PROP_TYPES } from '../structure';
|
||||
import {
|
||||
isCombinedAction,
|
||||
getApplicableCombinedActions
|
||||
} from './batchUpdates';
|
||||
import isObject from "lodash/isObject";
|
||||
import omit from "lodash/omit";
|
||||
import { validateShape } from "../validatePayload";
|
||||
import { createReducerBehaviors } from "../reducers";
|
||||
import { PROP_TYPES } from "../structure";
|
||||
import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates";
|
||||
|
||||
const reduce = require('lodash/fp/reduce').convert({ cap: false });
|
||||
const reduce = require("lodash/fp/reduce").convert({ cap: false });
|
||||
|
||||
//==============================
|
||||
// Shape behaviors
|
||||
|
@ -58,93 +60,118 @@ const reduce = require('lodash/fp/reduce').convert({ cap: false });
|
|||
// behavior, which still replaces the previous state with the payload.
|
||||
//==============================
|
||||
export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
|
||||
update: {
|
||||
reducer(state, payload) {
|
||||
if (!isObject(payload)) return state;
|
||||
return { ...state, ...payload };
|
||||
},
|
||||
validate: true,
|
||||
update: {
|
||||
reducer(state, payload) {
|
||||
if (!isObject(payload)) return state;
|
||||
return { ...state, ...payload };
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
if (!isObject(payload)) return initialState;
|
||||
return { ...initialState, ...payload };
|
||||
},
|
||||
validate: false,
|
||||
validate: true
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
if (!isObject(payload)) return initialState;
|
||||
return { ...initialState, ...payload };
|
||||
},
|
||||
replace: {
|
||||
reducer(state, payload, initialState) {
|
||||
if (!payload) return state;
|
||||
return {
|
||||
...initialState,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
validate: true,
|
||||
}
|
||||
validate: false
|
||||
},
|
||||
replace: {
|
||||
reducer(state, payload, initialState) {
|
||||
if (!payload) return state;
|
||||
return {
|
||||
...initialState,
|
||||
...payload
|
||||
};
|
||||
},
|
||||
validate: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function createShapeReducer(reducerShape: StructureType, {
|
||||
locationString,
|
||||
name,
|
||||
}: ShapeReducerOptions) {
|
||||
const uniqueId = Math.random().toString(36).substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(reducerShape, createReducerBehaviors(DEFAULT_SHAPE_BEHAVIORS, `${uniqueId}-${locationString}`)),
|
||||
},
|
||||
actions: createActions(DEFAULT_SHAPE_BEHAVIORS, `${uniqueId}-${locationString}`),
|
||||
};
|
||||
export function createShapeReducer(
|
||||
reducerShape: StructureType,
|
||||
{ locationString, name }: ShapeReducerOptions
|
||||
) {
|
||||
const uniqueId = Math.random()
|
||||
.toString(36)
|
||||
.substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(
|
||||
reducerShape,
|
||||
createReducerBehaviors(
|
||||
DEFAULT_SHAPE_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`
|
||||
)
|
||||
)
|
||||
},
|
||||
actions: createActions(
|
||||
DEFAULT_SHAPE_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function calculateDefaults(reducerStructure: any) {
|
||||
return reduce((memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]: propValue().type === PROP_TYPES._shape
|
||||
? calculateDefaults(propValue().structure)
|
||||
: propValue().defaultValue,
|
||||
}), {})(omit(reducerStructure, ['_wildcardKey']));
|
||||
return reduce(
|
||||
(memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]:
|
||||
propValue().type === PROP_TYPES._shape
|
||||
? calculateDefaults(propValue().structure)
|
||||
: propValue().defaultValue
|
||||
}),
|
||||
{}
|
||||
)(omit(reducerStructure, ["_wildcardKey"]));
|
||||
}
|
||||
|
||||
export function createReducer(
|
||||
objectStructure: StructureType,
|
||||
behaviors: ShapeReducerBehaviors
|
||||
): ShapeReducer {
|
||||
const initialState: Object = validateShape(
|
||||
objectStructure,
|
||||
calculateDefaults(objectStructure().structure)
|
||||
);
|
||||
return (state = initialState, { type, payload }: ShapeReducerAction) => {
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
export function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBehaviors): ShapeReducer {
|
||||
const initialState: Object = validateShape(objectStructure, calculateDefaults(objectStructure().structure));
|
||||
return (state = initialState, { type, payload }: ShapeReducerAction) => {
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
if (matchedBehaviors.length) {
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? validateShape(objectStructure, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialState,
|
||||
), state)(matchedBehaviors);
|
||||
}
|
||||
|
||||
return state;
|
||||
if (matchedBehaviors.length) {
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce(
|
||||
(interimState, matchedBehavior) =>
|
||||
behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? validateShape(objectStructure, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialState
|
||||
),
|
||||
state
|
||||
)(matchedBehaviors);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: ShapeReducerBehaviorsConfig, locationString: string): ShapeActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce((memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: Object) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
})
|
||||
}), {})(behaviorsConfig);
|
||||
function createActions(
|
||||
behaviorsConfig: ShapeReducerBehaviorsConfig,
|
||||
locationString: string
|
||||
): ShapeActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(
|
||||
(memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: Object) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload)
|
||||
})
|
||||
}),
|
||||
{}
|
||||
)(behaviorsConfig);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2,49 +2,54 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { PrimitiveType } from '../structure';
|
||||
import type { PrimitiveType } from "../structure";
|
||||
|
||||
//==============================
|
||||
// Flow types
|
||||
//==============================
|
||||
export type PrimitiveReducerAction = {
|
||||
type: string,
|
||||
payload: mixed,
|
||||
type: string,
|
||||
payload: mixed
|
||||
};
|
||||
export type PrimitiveReducer = (state: mixed, action: PrimitiveReducerAction) => mixed;
|
||||
export type PrimitiveReducerBehavior = (state: mixed, payload: mixed | void, initialState: mixed) => mixed;
|
||||
export type PrimitiveReducer = (
|
||||
state: mixed,
|
||||
action: PrimitiveReducerAction
|
||||
) => mixed;
|
||||
export type PrimitiveReducerBehavior = (
|
||||
state: mixed,
|
||||
payload: mixed | void,
|
||||
initialState: mixed
|
||||
) => mixed;
|
||||
export type PrimitiveReducerBehaviorsConfig = {
|
||||
[key: string]: {
|
||||
action?: (value: mixed) => mixed,
|
||||
reducer: PrimitiveReducerBehavior,
|
||||
validate: boolean,
|
||||
}
|
||||
[key: string]: {
|
||||
action?: (value: mixed) => mixed,
|
||||
reducer: PrimitiveReducerBehavior,
|
||||
validate: boolean
|
||||
}
|
||||
};
|
||||
export type PrimitiveReducerBehaviors = {
|
||||
[key: string]: PrimitiveReducerBehavior,
|
||||
[key: string]: PrimitiveReducerBehavior
|
||||
};
|
||||
export type PrimitiveAction = (value: mixed) => { type: string, payload: mixed };
|
||||
export type PrimitiveAction = (
|
||||
value: mixed
|
||||
) => { type: string, payload: mixed };
|
||||
export type PrimitiveActions = {
|
||||
[key: string]: PrimitiveAction
|
||||
[key: string]: PrimitiveAction
|
||||
};
|
||||
export type PrimitiveReducerOptions = {
|
||||
behaviorsConfig: PrimitiveReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string,
|
||||
behaviorsConfig: PrimitiveReducerBehaviorsConfig,
|
||||
locationString: string,
|
||||
name: string
|
||||
};
|
||||
|
||||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import { validateValue } from '../validatePayload';
|
||||
import { createReducerBehaviors } from '../reducers';
|
||||
import {
|
||||
isCombinedAction,
|
||||
getApplicableCombinedActions
|
||||
} from './batchUpdates';
|
||||
|
||||
const reduce = require('lodash/fp/reduce').convert({ cap: false });
|
||||
import { validateValue } from "../validatePayload";
|
||||
import { createReducerBehaviors } from "../reducers";
|
||||
import { isCombinedAction, getApplicableCombinedActions } from "./batchUpdates";
|
||||
|
||||
const reduce = require("lodash/fp/reduce").convert({ cap: false });
|
||||
|
||||
//==============================
|
||||
// Primitive behaviors
|
||||
|
@ -54,76 +59,97 @@ const reduce = require('lodash/fp/reduce').convert({ cap: false });
|
|||
//==============================
|
||||
|
||||
export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
|
||||
replace: {
|
||||
reducer(state, payload) {
|
||||
if (payload === undefined) return state;
|
||||
return payload;
|
||||
},
|
||||
validate: true,
|
||||
replace: {
|
||||
reducer(state, payload) {
|
||||
if (payload === undefined) return state;
|
||||
return payload;
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
return initialState;
|
||||
},
|
||||
validate: false,
|
||||
validate: true
|
||||
},
|
||||
reset: {
|
||||
reducer(state, payload, initialState) {
|
||||
return initialState;
|
||||
},
|
||||
validate: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function createPrimitiveReducer(primitiveType: PrimitiveType, {
|
||||
locationString,
|
||||
name,
|
||||
}: PrimitiveReducerOptions) {
|
||||
const uniqueId = Math.random().toString(36).substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(primitiveType, createReducerBehaviors(DEFAULT_PRIMITIVE_BEHAVIORS, `${uniqueId}-${locationString}`)),
|
||||
},
|
||||
actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, `${uniqueId}-${locationString}`),
|
||||
};
|
||||
export function createPrimitiveReducer(
|
||||
primitiveType: PrimitiveType,
|
||||
{ locationString, name }: PrimitiveReducerOptions
|
||||
) {
|
||||
const uniqueId = Math.random()
|
||||
.toString(36)
|
||||
.substring(5);
|
||||
return {
|
||||
reducers: {
|
||||
[name]: createReducer(
|
||||
primitiveType,
|
||||
createReducerBehaviors(
|
||||
DEFAULT_PRIMITIVE_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`
|
||||
)
|
||||
)
|
||||
},
|
||||
actions: createActions(
|
||||
DEFAULT_PRIMITIVE_BEHAVIORS,
|
||||
`${uniqueId}-${locationString}`
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function createReducer(
|
||||
primitiveType: PrimitiveType,
|
||||
behaviors: PrimitiveReducerBehaviors
|
||||
): PrimitiveReducer {
|
||||
//Calculate and validate the initial state of the reducer
|
||||
const initialState: mixed = validateValue(
|
||||
primitiveType,
|
||||
primitiveType().defaultValue
|
||||
);
|
||||
return (state = initialState, { type, payload }: PrimitiveReducerAction) => {
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducerBehaviors): PrimitiveReducer {
|
||||
//Calculate and validate the initial state of the reducer
|
||||
const initialState: mixed = validateValue(primitiveType, primitiveType().defaultValue);
|
||||
return (state = initialState, { type, payload }: PrimitiveReducerAction) => {
|
||||
//If the action type does not match any of the specified behaviors, just return the current state.
|
||||
const matchedBehaviors = behaviors[type]
|
||||
? [{ type, payload }]
|
||||
: isCombinedAction(type)
|
||||
? getApplicableCombinedActions(behaviors)(payload)
|
||||
: [];
|
||||
|
||||
if (matchedBehaviors.length) {
|
||||
//Call every behaviour relevant to this reducer as part of this action call,
|
||||
//and merge the result (later actions take priority).
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce((interimState, matchedBehavior) => behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? validateValue(primitiveType, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialState
|
||||
), state)(matchedBehaviors);
|
||||
}
|
||||
|
||||
return state;
|
||||
if (matchedBehaviors.length) {
|
||||
//Call every behaviour relevant to this reducer as part of this action call,
|
||||
//and merge the result (later actions take priority).
|
||||
//Sanitize the payload using the reducer shape, then apply the sanitized
|
||||
//payload to the state using the behavior linked to this action type.
|
||||
return reduce(
|
||||
(interimState, matchedBehavior) =>
|
||||
behaviors[matchedBehavior.type].reducer(
|
||||
interimState,
|
||||
behaviors[matchedBehavior.type].validate
|
||||
? validateValue(primitiveType, matchedBehavior.payload)
|
||||
: matchedBehavior.payload,
|
||||
initialState
|
||||
),
|
||||
state
|
||||
)(matchedBehaviors);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string): PrimitiveActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce((memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: mixed) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload),
|
||||
})
|
||||
}), {})(behaviorsConfig);
|
||||
function createActions(
|
||||
behaviorsConfig: PrimitiveReducerBehaviorsConfig,
|
||||
locationString: string
|
||||
): PrimitiveActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(
|
||||
(memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (payload: mixed) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (payload => payload))(payload)
|
||||
})
|
||||
}),
|
||||
{}
|
||||
)(behaviorsConfig);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
146
src/structure.js
146
src/structure.js
|
@ -6,94 +6,98 @@
|
|||
export type PropTypeKeys = $Keys<typeof PROP_TYPES>;
|
||||
|
||||
export type ShapeStructure = {
|
||||
[key: string]: StructureType | PrimitiveType | ArrayStructureType,
|
||||
}
|
||||
[key: string]: StructureType | PrimitiveType | ArrayStructureType
|
||||
};
|
||||
export type StructureType = () => {
|
||||
type: PropTypeKeys,
|
||||
structure: ShapeStructure | StructureType | PrimitiveType,
|
||||
defaultValue?: any,
|
||||
type: PropTypeKeys,
|
||||
structure: ShapeStructure | StructureType | PrimitiveType,
|
||||
defaultValue?: any
|
||||
};
|
||||
export type ReducerType = () => {
|
||||
type: PropTypeKeys,
|
||||
structure: StructureType | PrimitiveType,
|
||||
type: PropTypeKeys,
|
||||
structure: StructureType | PrimitiveType
|
||||
};
|
||||
export type ArrayStructureType = () => {
|
||||
type: PropTypeKeys,
|
||||
structure: StructureType | PrimitiveType,
|
||||
defaultValue: any,
|
||||
}
|
||||
type: PropTypeKeys,
|
||||
structure: StructureType | PrimitiveType,
|
||||
defaultValue: any
|
||||
};
|
||||
export type PrimitiveType = () => {
|
||||
type: PropTypeKeys,
|
||||
defaultValue?: any,
|
||||
typeofValue: string,
|
||||
structure?: PrimitiveType,
|
||||
type: PropTypeKeys,
|
||||
defaultValue?: any,
|
||||
typeofValue: string,
|
||||
structure?: PrimitiveType
|
||||
};
|
||||
export type TypesObject = {
|
||||
[key: string]: any,
|
||||
[key: string]: any
|
||||
};
|
||||
|
||||
//==============================
|
||||
// Structure
|
||||
//==============================
|
||||
export const PROP_TYPES = {
|
||||
_string: '_string',
|
||||
_number: '_number',
|
||||
_boolean: '_boolean',
|
||||
_reducer: '_reducer',
|
||||
_shape: '_shape',
|
||||
_array: '_array',
|
||||
_any: '_any',
|
||||
_wildcardKey: '_wildcardKey',
|
||||
_custom: '_custom',
|
||||
_string: "_string",
|
||||
_number: "_number",
|
||||
_boolean: "_boolean",
|
||||
_reducer: "_reducer",
|
||||
_shape: "_shape",
|
||||
_array: "_array",
|
||||
_any: "_any",
|
||||
_wildcardKey: "_wildcardKey",
|
||||
_custom: "_custom"
|
||||
};
|
||||
|
||||
//The types objects are used in order to build up the structure of a store chunk, and provide/accept
|
||||
//default values whilst doing so.
|
||||
export const Types: TypesObject = {
|
||||
string: (defaultValue: string = '') => () => ({
|
||||
type: PROP_TYPES._string,
|
||||
defaultValue,
|
||||
typeofValue: 'string',
|
||||
}),
|
||||
number: (defaultValue: number = 0) => () => ({
|
||||
type: PROP_TYPES._number,
|
||||
defaultValue,
|
||||
typeofValue: 'number',
|
||||
}),
|
||||
boolean: (defaultValue: boolean = false) => () => ({
|
||||
type: PROP_TYPES._boolean,
|
||||
defaultValue,
|
||||
typeofValue: 'boolean',
|
||||
}),
|
||||
any: (defaultValue: any = null) => () => ({
|
||||
type: PROP_TYPES._any,
|
||||
defaultValue,
|
||||
typeofValue: 'any',
|
||||
}),
|
||||
arrayOf: (structure: StructureType | PrimitiveType, defaultValue = []) => () => ({
|
||||
type: PROP_TYPES._array,
|
||||
structure,
|
||||
defaultValue,
|
||||
}),
|
||||
reducer: (structure: ShapeStructure) => () => ({
|
||||
type: PROP_TYPES._reducer,
|
||||
structure,
|
||||
}),
|
||||
shape: (structure: ShapeStructure) => () => ({
|
||||
type: PROP_TYPES._shape,
|
||||
structure,
|
||||
}),
|
||||
custom: ({
|
||||
validator = () => true,
|
||||
validationErrorMessage = (value: any) => `${ value } failed custom type validation`,
|
||||
}: {
|
||||
validator: (value: any) => boolean,
|
||||
validationErrorMessage: (value: any) => string,
|
||||
} = {}) => (defaultValue: any) => () => ({
|
||||
type: PROP_TYPES._custom,
|
||||
defaultValue,
|
||||
validator,
|
||||
validationErrorMessage,
|
||||
}),
|
||||
wildcardKey: () => PROP_TYPES._wildcardKey,
|
||||
string: (defaultValue: string = "") => () => ({
|
||||
type: PROP_TYPES._string,
|
||||
defaultValue,
|
||||
typeofValue: "string"
|
||||
}),
|
||||
number: (defaultValue: number = 0) => () => ({
|
||||
type: PROP_TYPES._number,
|
||||
defaultValue,
|
||||
typeofValue: "number"
|
||||
}),
|
||||
boolean: (defaultValue: boolean = false) => () => ({
|
||||
type: PROP_TYPES._boolean,
|
||||
defaultValue,
|
||||
typeofValue: "boolean"
|
||||
}),
|
||||
any: (defaultValue: any = null) => () => ({
|
||||
type: PROP_TYPES._any,
|
||||
defaultValue,
|
||||
typeofValue: "any"
|
||||
}),
|
||||
arrayOf: (
|
||||
structure: StructureType | PrimitiveType,
|
||||
defaultValue = []
|
||||
) => () => ({
|
||||
type: PROP_TYPES._array,
|
||||
structure,
|
||||
defaultValue
|
||||
}),
|
||||
reducer: (structure: ShapeStructure) => () => ({
|
||||
type: PROP_TYPES._reducer,
|
||||
structure
|
||||
}),
|
||||
shape: (structure: ShapeStructure) => () => ({
|
||||
type: PROP_TYPES._shape,
|
||||
structure
|
||||
}),
|
||||
custom: ({
|
||||
validator = () => true,
|
||||
validationErrorMessage = (value: any) =>
|
||||
`${value} failed custom type validation`
|
||||
}: {
|
||||
validator: (value: any) => boolean,
|
||||
validationErrorMessage: (value: any) => string
|
||||
} = {}) => (defaultValue: any) => () => ({
|
||||
type: PROP_TYPES._custom,
|
||||
defaultValue,
|
||||
validator,
|
||||
validationErrorMessage
|
||||
}),
|
||||
wildcardKey: () => PROP_TYPES._wildcardKey
|
||||
};
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
//@flow
|
||||
export function updateAtIndex(array: Array<any>, value: any, index: number): Array<any> {
|
||||
if (typeof index !== 'number') throw new Error('Must provide a numeric index to updateAtIndex');
|
||||
if (index < 0 || index > array.length - 1) throw new Error(`The index ${index} is out of range for the array provided`);
|
||||
return [
|
||||
...array.slice(0,index),
|
||||
value,
|
||||
...array.slice(index + 1),
|
||||
];
|
||||
export function updateAtIndex(
|
||||
array: Array<any>,
|
||||
value: any,
|
||||
index: number
|
||||
): Array<any> {
|
||||
if (typeof index !== "number")
|
||||
throw new Error("Must provide a numeric index to updateAtIndex");
|
||||
if (index < 0 || index > array.length - 1)
|
||||
throw new Error(
|
||||
`The index ${index} is out of range for the array provided`
|
||||
);
|
||||
return [...array.slice(0, index), value, ...array.slice(index + 1)];
|
||||
}
|
||||
|
||||
|
||||
export function removeAtIndex(array: Array<any>, index: number): Array<any> {
|
||||
if (typeof index !== 'number') throw new Error('Must provide a numeric index to removeAtIndex');
|
||||
if (index < 0 || index > array.length - 1) throw new Error(`The index ${index} is out of range for the array provided`);
|
||||
return [
|
||||
...array.slice(0, index),
|
||||
...array.slice(index + 1),
|
||||
];
|
||||
if (typeof index !== "number")
|
||||
throw new Error("Must provide a numeric index to removeAtIndex");
|
||||
if (index < 0 || index > array.length - 1)
|
||||
throw new Error(
|
||||
`The index ${index} is out of range for the array provided`
|
||||
);
|
||||
return [...array.slice(0, index), ...array.slice(index + 1)];
|
||||
}
|
||||
|
|
|
@ -2,106 +2,144 @@
|
|||
//==============================
|
||||
// Flow imports and types
|
||||
//==============================
|
||||
import type { PrimitiveType, StructureType, ShapeStructure } from './structure';
|
||||
import type { PrimitiveType, StructureType, ShapeStructure } from "./structure";
|
||||
|
||||
type validationFunction = (structure: StructureType | PrimitiveType | ShapeStructure, value: any) => any;
|
||||
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';
|
||||
const find = require('lodash/fp/find').convert({ cap: false });
|
||||
|
||||
import reduce from "lodash/reduce";
|
||||
import isObject from "lodash/isObject";
|
||||
import { PROP_TYPES } from "./structure";
|
||||
const find = require("lodash/fp/find").convert({ cap: false });
|
||||
|
||||
export const hasWildcardKey = (objectStructure: any) =>
|
||||
!!find((prop, key) => key === PROP_TYPES._wildcardKey)(objectStructure().structure);
|
||||
|
||||
|
||||
export const getValueType = (objectStructure: any, key: string, wildcardKeyPresent: boolean) =>
|
||||
wildcardKeyPresent
|
||||
? objectStructure().structure[key] || objectStructure().structure[PROP_TYPES._wildcardKey]
|
||||
: objectStructure().structure[key];
|
||||
!!find((prop, key) => key === PROP_TYPES._wildcardKey)(
|
||||
objectStructure().structure
|
||||
);
|
||||
|
||||
export const getValueType = (
|
||||
objectStructure: any,
|
||||
key: string,
|
||||
wildcardKeyPresent: boolean
|
||||
) =>
|
||||
wildcardKeyPresent
|
||||
? objectStructure().structure[key] ||
|
||||
objectStructure().structure[PROP_TYPES._wildcardKey]
|
||||
: objectStructure().structure[key];
|
||||
|
||||
export function validateShape(objectStructure: any, value: mixed): Object {
|
||||
if (!isObject(value)) {
|
||||
console.error(`The value passed to validateObject() was not an object. Value: `, value);
|
||||
return {};
|
||||
}
|
||||
if (!isObject(value)) {
|
||||
console.error(
|
||||
`The value passed to validateObject() was not an object. Value: `,
|
||||
value
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
const wildcardKeyPresent = hasWildcardKey(objectStructure);
|
||||
const wildcardKeyPresent = hasWildcardKey(objectStructure);
|
||||
|
||||
return reduce(value, (memo, value, name) => {
|
||||
const valueType = getValueType(objectStructure, name, wildcardKeyPresent);
|
||||
//If the value type does not exist in the reducer structure, and there's no wildcard key, then
|
||||
//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.warn(`The property, ${name}, was not specified in the structure` +
|
||||
` and was stripped out of the payload. Structure: ${ objectStructure().structure }`);
|
||||
return memo;
|
||||
}
|
||||
return reduce(
|
||||
value,
|
||||
(memo, value, name) => {
|
||||
const valueType = getValueType(objectStructure, name, wildcardKeyPresent);
|
||||
//If the value type does not exist in the reducer structure, and there's no wildcard key, then
|
||||
//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.warn(
|
||||
`The property, ${name}, was not specified in the structure` +
|
||||
` and was stripped out of the payload. Structure: ${
|
||||
objectStructure().structure
|
||||
}`
|
||||
);
|
||||
return memo;
|
||||
}
|
||||
|
||||
const validatedValue = getTypeValidation(valueType().type)(valueType, value);
|
||||
const validatedValue = getTypeValidation(valueType().type)(
|
||||
valueType,
|
||||
value
|
||||
);
|
||||
|
||||
if (validatedValue === undefined) {
|
||||
console.warn(`The property, ${name}, was populated with a type ${ typeof value } which does not` +
|
||||
` match that specified in the reducer configuration ${ wildcardKeyPresent ? ', nor did it match a wildcardKey': ''}. It has been stripped from` +
|
||||
' the payload');
|
||||
return memo;
|
||||
}
|
||||
if (validatedValue === undefined) {
|
||||
console.warn(
|
||||
`The property, ${name}, was populated with a type ${typeof value} which does not` +
|
||||
` match that specified in the reducer configuration ${
|
||||
wildcardKeyPresent ? ", nor did it match a wildcardKey" : ""
|
||||
}. It has been stripped from` +
|
||||
" the payload"
|
||||
);
|
||||
return memo;
|
||||
}
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[name]: validatedValue,
|
||||
}
|
||||
}, {});
|
||||
return {
|
||||
...memo,
|
||||
[name]: validatedValue
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function validateValue(primitive: any, value: any): mixed {
|
||||
const evaluatedPrimitive = primitive();
|
||||
//If this value is a custom value, then we should apply it's custom validator!
|
||||
if (evaluatedPrimitive.type === PROP_TYPES._custom) {
|
||||
if (evaluatedPrimitive.validator(value)) return value;
|
||||
return console.warn(evaluatedPrimitive.validationErrorMessage(value));
|
||||
}
|
||||
const evaluatedPrimitive = primitive();
|
||||
//If this value is a custom value, then we should apply it's custom validator!
|
||||
if (evaluatedPrimitive.type === PROP_TYPES._custom) {
|
||||
if (evaluatedPrimitive.validator(value)) return value;
|
||||
return console.warn(evaluatedPrimitive.validationErrorMessage(value));
|
||||
}
|
||||
|
||||
//Otherwise we will use the standard, basic, typeof checks.
|
||||
if (typeof value === evaluatedPrimitive.typeofValue || evaluatedPrimitive.typeofValue === 'any') return value;
|
||||
return console.warn(`The value, ${value}, did not match the type specified (${evaluatedPrimitive.type}).`);
|
||||
//Otherwise we will use the standard, basic, typeof checks.
|
||||
if (
|
||||
typeof value === evaluatedPrimitive.typeofValue ||
|
||||
evaluatedPrimitive.typeofValue === "any"
|
||||
)
|
||||
return value;
|
||||
return console.warn(
|
||||
`The value, ${value}, did not match the type specified (${
|
||||
evaluatedPrimitive.type
|
||||
}).`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function validateArray(arrayStructure: any, value: Array<any>): 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)) {
|
||||
console.error(`The value passed to validateArray() was not an array. Value: `, value);
|
||||
return [];
|
||||
}
|
||||
const elementStructure = arrayStructure().structure;
|
||||
const elementType = elementStructure().type;
|
||||
return value.map(element => getTypeValidation(elementType)(elementStructure, element)).filter(e => e);
|
||||
export function validateArray(
|
||||
arrayStructure: any,
|
||||
value: Array<any>
|
||||
): 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)) {
|
||||
console.error(
|
||||
`The value passed to validateArray() was not an array. Value: `,
|
||||
value
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const elementStructure = arrayStructure().structure;
|
||||
const elementType = elementStructure().type;
|
||||
return value
|
||||
.map(element => getTypeValidation(elementType)(elementStructure, element))
|
||||
.filter(e => e);
|
||||
}
|
||||
|
||||
|
||||
export function getTypeValidation(type: string): validationFunction {
|
||||
const TYPE_VALIDATIONS = {
|
||||
[PROP_TYPES._string]: validateValue,
|
||||
[PROP_TYPES._number]: validateValue,
|
||||
[PROP_TYPES._boolean]: validateValue,
|
||||
[PROP_TYPES._array]: validateArray,
|
||||
[PROP_TYPES._shape]: validateShape,
|
||||
[PROP_TYPES._any]: validateValue,
|
||||
[PROP_TYPES._custom]: validateValue,
|
||||
};
|
||||
const typeValidation = TYPE_VALIDATIONS[type];
|
||||
if (!typeValidation) {
|
||||
throw new Error(`The type ${type} does not have a corresponding
|
||||
const TYPE_VALIDATIONS = {
|
||||
[PROP_TYPES._string]: validateValue,
|
||||
[PROP_TYPES._number]: validateValue,
|
||||
[PROP_TYPES._boolean]: validateValue,
|
||||
[PROP_TYPES._array]: validateArray,
|
||||
[PROP_TYPES._shape]: validateShape,
|
||||
[PROP_TYPES._any]: validateValue,
|
||||
[PROP_TYPES._custom]: validateValue
|
||||
};
|
||||
const typeValidation = TYPE_VALIDATIONS[type];
|
||||
if (!typeValidation) {
|
||||
throw new Error(`The type ${type} does not have a corresponding
|
||||
validation function!`);
|
||||
}
|
||||
return typeValidation;
|
||||
}
|
||||
}
|
||||
return typeValidation;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue