refactor(prettier): Apply prettier styling

This commit is contained in:
Kai Moseley 2018-06-13 08:29:54 +01:00
parent 13f5bf1649
commit 4925c77728
14 changed files with 1514 additions and 1226 deletions

View File

@ -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/);
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 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']);
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('Resulting chunk', () => {
const chunk = buildStoreChunk('example', {
nested1: Types.reducer(Types.string('foo')),
nested2: Types.reducer(Types.shape({
describe("Resulting chunk", () => {
const chunk = buildStoreChunk("example", {
nested1: Types.reducer(Types.string("foo")),
nested2: Types.reducer(
Types.shape({
foo: Types.number(),
bar: Types.string(),
})),
bar: Types.string()
})
),
nested3: Types.reducer(Types.arrayOf(Types.number(), [1, 2, 3])),
nested4: Types.reducer({
innerNested1: Types.reducer(Types.string('bar')),
innerNested1: Types.reducer(Types.string("bar")),
innerNested2: Types.reducer({
innerNested3: Types.reducer(Types.string('baz')),
innerNested3: Types.reducer(Types.string("baz"))
})
}),
}),
nested5: Types.reducer(Types.shape({
arrayExample: Types.arrayOf(Types.string()),
})),
nested5: Types.reducer(
Types.shape({
arrayExample: Types.arrayOf(Types.string())
})
)
});
const nonNestedChunk = buildStoreChunk('example2', Types.reducer(Types.string('foo')));
const nonNestedChunk = buildStoreChunk(
"example2",
Types.reducer(Types.string("foo"))
);
describe('Selectors', () => {
const store = createStore(combineReducers({
...chunk.reducers,
}));
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 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', () => {
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("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("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');
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']);
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("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("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,
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]);
});
});
});
});

View File

@ -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';
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');
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.string())).toBe("");
expect(calculateDefaults(Types.number())).toBe(0);
});
it('Should provide correct default values for an object', () => {
it("Should provide correct default values for an object", () => {
const objectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
test2: Types.number()
});
expect(calculateDefaults(objectStructure)).toEqual({
test1: "",
test2: 0
});
expect(calculateDefaults(objectStructure)).toEqual({ test1: '', test2: 0 });
});
it('Should provide correct default values for nested object', () => {
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'),
test4: Types.string("foo")
})
});
expect(calculateDefaults(objectStructure)).toEqual({
test1: '',
test1: "",
test2: 0,
test3: {
test4: 'foo'
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',
});
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,
reducerStructureDescriptor:
returnVal.reducerStructureDescriptor.name
}).toEqual({
name: 'toast',
name: "toast",
reducerFn: REDUCER_CREATOR_MAPPING[structureType()().type].name,
reducerStructureDescriptor: '', //The internal functions should be anonymous
locationString: 'toasty',
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/)
});
});
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,
},
);
});
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
}
});
});
});
});

View File

@ -1,11 +1,11 @@
import { Types, PROP_TYPES } from '../structure';
import { Types, PROP_TYPES } from "../structure";
function hasType(object, type) {
return object.type === type;
}
describe('Type descriptions', () => {
it('will return a function which subsequently returns an object with the correct 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();
@ -14,50 +14,54 @@ describe('Type descriptions', () => {
expect(hasType(Types.shape()(), PROP_TYPES._shape)).toBeTruthy();
});
it('will return the standard default values when none provided', () => {
expect(Types.string()().defaultValue).toBe('');
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');
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');
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)', () => {
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', () => {
it("custom value correctly exposes the validator and validation error message properties", () => {
const customValidator = () => false;
const customErrorMessage = () => 'Hai!';
const customErrorMessage = () => "Hai!";
const customType = Types.custom({
validator: customValidator,
validationErrorMessage: customErrorMessage,
validationErrorMessage: customErrorMessage
})()();
expect(customType.validator).toEqual(customValidator);
expect(customType.validationErrorMessage).toEqual(customErrorMessage);
});
it('custom value acts the same as any when no configuration provided', () => {
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("toasty")).toBe(true);
expect(unconfiguredCustom.validator()).toBe(true);
expect(unconfiguredCustom.validator(null)).toBe(true);
});

View File

@ -1,208 +1,245 @@
import { Types } from '../structure';
import { Types } from "../structure";
import {
validateValue,
validateShape,
validateArray,
getTypeValidation,
hasWildcardKey,
getValueType,
} from '../validatePayload';
getValueType
} from "../validatePayload";
describe('Validation functionality', () => {
describe('Primitives/Custom', () => {
describe("Validation functionality", () => {
describe("Primitives/Custom", () => {
const customType = Types.custom({
validator: value => value === 3,
validationErrorMessage: value => `Oh noes! ${ value }`,
validationErrorMessage: value => `Oh noes! ${value}`
})();
it('Number primitive should allow for numbers', () => {
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("String primitive should allow for string", () => {
expect(validateValue(Types.string(), "toast")).toBe("toast");
});
it('Boolean primitive should allow for string', () => {
it("Boolean primitive should allow for string", () => {
expect(validateValue(Types.boolean(), true)).toBe(true);
});
it('Any should allow for anything', () => {
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', () => {
it("should validate custom values using the custom validator", () => {
expect(validateValue(customType, 3)).toBe(3);
});
it('should return undefined from custom validators which failed', () => {
it("should return undefined from custom validators which failed", () => {
expect(validateValue(customType, 4)).toBeUndefined();
});
});
describe('Arrays', () => {
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 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']);
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({
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}]);
})
);
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({
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' }]);
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 return an empty array if a non-array is passed", () => {
expect(validateArray("foo")).toEqual([]);
});
it('Array should allow an empty array', () => {
it("Array should allow an empty array", () => {
expect(validateArray([])).toEqual([]);
});
});
describe('Objects', () => {
describe("Objects", () => {
const testObjectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
test3: Types.custom({ validator: value => value !== 4 })(),
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 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 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 });
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,
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 } });
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()
}),
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({
it("Objects containing objects should properly check if an object is provided", () => {
expect(
validateShape(testObjectStructure3, { test1: "foo", test2: "bar" })
).toEqual({
test1: {},
test2: 'bar',
test2: "bar"
});
});
const testObjectStructure4 = Types.shape({
test2: Types.string(),
[Types.wildcardKey()]: Types.string(),
[Types.wildcardKey()]: Types.string()
});
const testObjectStructure5 = Types.shape({
test2: Types.string(),
[Types.wildcardKey()]: Types.any(),
[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',
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({
expect(
validateShape(testObjectStructure5, { test1: 0, test2: "bar" })
).toEqual({
test1: 0,
test2: 'bar',
test2: "bar"
});
});
const testObjectStructure6 = Types.shape({
test2: Types.string(),
[Types.wildcardKey()]: 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',
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()),
test1: Types.arrayOf(Types.string())
});
it('Should allow an empty array to be passed for an array property', () => {
it("Should allow an empty array to be passed for an array property", () => {
expect(validateShape(testObjectStructure7, { test1: [] })).toEqual({
test1: [],
test1: []
});
});
});
});
describe('Non covered types', () => {
it('A type with no associated validation should throw an error', () => {
expect(() => getTypeValidation('toast')).toThrowError(/validation/);
describe("Non covered types", () => {
it("A type with no associated validation should throw an error", () => {
expect(() => getTypeValidation("toast")).toThrowError(/validation/);
});
});
describe('Has wildcard value', () => {
describe("Has wildcard value", () => {
const testObjectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
[Types.wildcardKey()]: Types.any(),
[Types.wildcardKey()]: Types.any()
});
const testObjectStructure2 = Types.shape({
test1: Types.string(),
test2: Types.number(),
test2: Types.number()
});
it('should return true if the objectStructure passed in has a wildcard key', () => {
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', () => {
it("should return false if no wildcard key passed in", () => {
expect(hasWildcardKey(testObjectStructure2)).toBe(false);
});
});
describe('GetValueType', () => {
describe("GetValueType", () => {
const testObjectStructure = Types.shape({
test1: Types.string(),
test2: Types.number(),
[Types.wildcardKey()]: Types.number(),
[Types.wildcardKey()]: Types.number()
});
const testObjectStructure2 = Types.shape({
test1: Types.string(),
test2: Types.number(),
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 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 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("should return undefined if no wildcard or matching key", () => {
expect(getValueType(testObjectStructure, "test3", false)).toEqual(
undefined
);
});
});
});

View File

@ -2,47 +2,58 @@
//==============================
// 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 }`);
locationString: string
} = {}
): PartialStoreChunk {
if (!structure)
throw new Error(
`The structure must be defined for a reducer! LocationString: ${locationString}`
);
const initialMemo: PartialStoreChunk = {
reducers: {
[name]: {},
[name]: {}
},
actions: {},
selectors: {},
baseSelector,
locationString,
name,
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);
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
@ -51,37 +62,55 @@ export function buildStoreChunk(name: string, structure: any, {
return {
reducers: processedStructure.reducers,
actions: processedStructure.actions[name],
selectors: processedStructure.selectors[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 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) {
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: {
return {
...omit(processedStoreChunk, ["baseSelector", "locationString", "name"]),
reducers: {
[processedStoreChunk.name]: combineReducers(processedStoreChunk.reducers)
}};
}
};
}
export function processStructure(memo: PartialStoreChunk, propValue: StructureType | PrimitiveType, propName: string) {
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);
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
@ -90,11 +119,11 @@ export function processStructure(memo: PartialStoreChunk, propValue: StructureTy
let childReducer = containsReducers
? buildStoreChunk(propName, propStructure, {
locationString: `${memo.locationString}.${propName}`,
baseSelector: (state: any) => memo.baseSelector(state)[propName],
baseSelector: (state: any) => memo.baseSelector(state)[propName]
})
: createReducer(propValue, {
locationString: `${memo.locationString}.${propName}`,
name: propName,
name: propName
});
//As the chunk is built up, we want to assign the reducers/actions created
@ -109,20 +138,13 @@ export function processStructure(memo: PartialStoreChunk, propValue: StructureTy
},
actions: {
...memo.actions,
[propName]: childReducer.actions,
[propName]: childReducer.actions
},
selectors: {
...memo.selectors,
[propName]: containsReducers ? childReducer.selectors : state => memo.baseSelector(state)[propName],
},
[propName]: containsReducers
? childReducer.selectors
: state => memo.baseSelector(state)[propName]
}
};
}

View File

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

View File

@ -6,8 +6,8 @@ import type {
StructureType,
PrimitiveType,
ReducerType,
PropTypeKeys,
} from './structure';
PropTypeKeys
} from "./structure";
//==============================
// Flow types
@ -18,7 +18,7 @@ export type PartialStoreChunk = {
selectors: { [key: string]: any },
locationString: string,
baseSelector: () => {},
name: string,
name: string
};
export type Selector = (state: Object) => any;
@ -27,20 +27,18 @@ type CallReducerInterface = {
name: string,
reducerFn: () => {},
reducerStructureDescriptor: StructureType | PrimitiveType,
locationString: string,
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,
@ -49,37 +47,41 @@ export const REDUCER_CREATOR_MAPPING: { [key: PropTypeKeys]: any } = {
[PROP_TYPES._string]: createPrimitiveReducer,
[PROP_TYPES._number]: createPrimitiveReducer,
[PROP_TYPES._any]: createPrimitiveReducer,
[PROP_TYPES._custom]: 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();
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,
locationString
};
}
export function callReducer({
name,
reducerFn,
@ -88,40 +90,53 @@ export function callReducer({
}: CallReducerInterface) {
return reducerFn(reducerStructureDescriptor, {
locationString,
name,
name
});
}
export function createReducerBehaviors(behaviorsConfig: { [key: string]: { reducer: () => {} } }, locationString: string): any {
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) => ({
return reduce(
behaviorsConfig,
(memo, behavior, name) => ({
...memo,
[`${locationString}.${name}`]: behavior,
}), {});
[`${locationString}.${name}`]: behavior
}),
{}
);
}
export function calculateDefaults(typeDescription: StructureType | PrimitiveType) {
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 { type, structure = {}, defaultValue = "" } = typeDescription();
const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1;
if (!complex) return defaultValue;
return reduce(structure, (memo, propValue, propName) => ({
return reduce(
structure,
(memo, propValue, propName) => ({
...memo,
[propName]: calculateDefaults(propValue),
}), {});
[propName]: calculateDefaults(propValue)
}),
{}
);
}
export const createReducer = flowRight(callReducer, determineReducerType);
export const createReducer = flowRight(
callReducer,
determineReducerType
);

View File

@ -2,7 +2,7 @@
//==============================
// Flow imports
//==============================
import type { ArrayStructureType } from '../structure';
import type { ArrayStructureType } from "../structure";
//==============================
// Flow types
@ -10,49 +10,63 @@ import type { ArrayStructureType } from '../structure';
export type ArrayReducerAction = {
type: string,
payload: any,
index?: number,
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,
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>
};
export type ArrayReducerOptions = {
behaviorsConfig: ArrayReducerBehaviorsConfig,
locationString: string,
name: 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 {
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;
@ -60,7 +74,6 @@ function checkIndex(index: ?number, payload: any = '', behaviorName: string): bo
return true;
}
//==============================
// Array behaviors
// ----------------
@ -74,88 +87,116 @@ 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;
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,
validate: true
},
resetAtIndex: {
reducer(state, payload, initialState, index) {
if (!checkIndex(index, payload, 'resetAtIndex')) return state;
if (!checkIndex(index, payload, "resetAtIndex")) return state;
return updateAtIndex(state, initialState, index);
},
validate: false,
validate: false
},
removeAtIndex: {
reducer(state, index) {
if (!checkIndex(index, '', 'removeAtIndex')) return state;
if (!checkIndex(index, "", "removeAtIndex")) return state;
return removeAtIndex(state, index);
},
validate: false,
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;
if (!isArray(payload))
return (
console.warn("An array must be provided when replacing an array") ||
state
);
return payload;
},
validate: true,
validate: true
},
reset: {
reducer(state, payload, initialState) {
return initialState;
},
validate: false,
validate: false
},
push: {
reducer(state, payload) {
return [...state, payload];
},
validate: true,
validate: true
},
pop: {
reducer(state) {
return state.slice(0, -1);
},
validate: false,
validate: false
},
unshift: {
reducer(state, payload) {
return [payload, ...state];
},
validate: true,
validate: true
},
shift: {
reducer(state) {
return state.slice(1);
},
validate: false,
validate: false
}
};
export function createArrayReducer(arrayTypeDescription: ArrayStructureType, {
locationString,
name,
}: ArrayReducerOptions) {
const uniqueId = Math.random().toString(36).substring(5);
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}`))
[name]: createReducer(
arrayTypeDescription,
createReducerBehaviors(
DEFAULT_ARRAY_BEHAVIORS,
`${uniqueId}-${locationString}`
)
)
},
actions: createActions(DEFAULT_ARRAY_BEHAVIORS, `${uniqueId}-${locationString}`, {}),
actions: createActions(
DEFAULT_ARRAY_BEHAVIORS,
`${uniqueId}-${locationString}`,
{}
)
};
}
export function createReducer(arrayTypeDescription: ArrayStructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
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);
const initialValue = validateArray(
arrayTypeDescription,
arrayTypeDescription().defaultValue
);
//Return the array reducer.
return (state: Array<any> = initialValue, { type, payload, index }: ArrayReducerAction) => {
return (
state: Array<any> = initialValue,
{ type, payload, index }: ArrayReducerAction
) => {
const matchedBehaviors = behaviors[type]
? [{ type, payload }]
: isCombinedAction(type)
@ -167,23 +208,29 @@ export function createReducer(arrayTypeDescription: ArrayStructureType, behavior
//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(
return reduce(
(interimState, matchedBehavior) =>
behaviors[matchedBehavior.type].reducer(
interimState,
behaviors[matchedBehavior.type].validate
? applyValidation(arrayTypeDescription, matchedBehavior.payload)
: matchedBehavior.payload,
initialValue,
index,
), state)(matchedBehaviors);
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) {
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
@ -196,19 +243,25 @@ export function applyValidation(arrayTypeDescription: ArrayStructureType, payloa
// 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);
if (structure().type === PROP_TYPES._shape)
return validateShape(structure, payload);
return validateValue(structure, payload);
}
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string): ArrayActions {
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) => ({
return reduce(
(memo, behavior, name) => ({
...memo,
[name]: (payload: Array<any>, index: ?number) => ({
type: `${locationString}.${name}`,
payload: (behavior.action || (payload => payload))(payload),
index,
index
})
}), {})(behaviorsConfig);
}),
{}
)(behaviorsConfig);
}

View File

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

View File

@ -2,8 +2,7 @@
//==============================
// Flow imports
//==============================
import type { StructureType } from '../structure';
import type { StructureType } from "../structure";
//==============================
// Flow types
@ -11,18 +10,25 @@ import type { StructureType } from '../structure';
export type ShapeReducerAction = {
type: string,
payload: Object,
validate: boolean,
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,
reducer: ShapeReducerBehavior
}
};
export type ShapeReducerBehaviors = {
[key: string]: ShapeReducerBehavior,
[key: string]: ShapeReducerBehavior
};
export type ShapeAction = (value: Object) => { type: string, payload: Object };
export type ShapeActions = {
@ -31,24 +37,20 @@ export type ShapeActions = {
export type ShapeReducerOptions = {
behaviorsConfig: ShapeReducerBehaviorsConfig,
locationString: string,
name: 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
@ -63,54 +65,72 @@ export const DEFAULT_SHAPE_BEHAVIORS: ShapeReducerBehaviorsConfig = {
if (!isObject(payload)) return state;
return { ...state, ...payload };
},
validate: true,
validate: true
},
reset: {
reducer(state, payload, initialState) {
if (!isObject(payload)) return initialState;
return { ...initialState, ...payload };
},
validate: false,
validate: false
},
replace: {
reducer(state, payload, initialState) {
if (!payload) return state;
return {
...initialState,
...payload,
...payload
};
},
validate: true,
validate: true
}
};
export function createShapeReducer(reducerShape: StructureType, {
locationString,
name,
}: ShapeReducerOptions) {
const uniqueId = Math.random().toString(36).substring(5);
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}`)),
[name]: createReducer(
reducerShape,
createReducerBehaviors(
DEFAULT_SHAPE_BEHAVIORS,
`${uniqueId}-${locationString}`
)
)
},
actions: createActions(DEFAULT_SHAPE_BEHAVIORS, `${uniqueId}-${locationString}`),
actions: createActions(
DEFAULT_SHAPE_BEHAVIORS,
`${uniqueId}-${locationString}`
)
};
}
export function calculateDefaults(reducerStructure: any) {
return reduce((memo, propValue, propName) => ({
return reduce(
(memo, propValue, propName) => ({
...memo,
[propName]: propValue().type === PROP_TYPES._shape
[propName]:
propValue().type === PROP_TYPES._shape
? calculateDefaults(propValue().structure)
: propValue().defaultValue,
}), {})(omit(reducerStructure, ['_wildcardKey']));
: propValue().defaultValue
}),
{}
)(omit(reducerStructure, ["_wildcardKey"]));
}
export function createReducer(objectStructure: StructureType, behaviors: ShapeReducerBehaviors): ShapeReducer {
const initialState: Object = validateShape(objectStructure, calculateDefaults(objectStructure().structure));
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]
@ -122,29 +142,36 @@ export function createReducer(objectStructure: StructureType, behaviors: ShapeRe
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(
return reduce(
(interimState, matchedBehavior) =>
behaviors[matchedBehavior.type].reducer(
interimState,
behaviors[matchedBehavior.type].validate
? validateShape(objectStructure, matchedBehavior.payload)
: matchedBehavior.payload,
initialState,
), state)(matchedBehaviors);
initialState
),
state
)(matchedBehaviors);
}
return state;
}
};
}
function createActions(behaviorsConfig: ShapeReducerBehaviorsConfig, locationString: string): ShapeActions {
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) => ({
return reduce(
(memo, behavior, name) => ({
...memo,
[name]: (payload: Object) => ({
type: `${locationString}.${name}`,
payload: (behavior.action || (payload => payload))(payload),
payload: (behavior.action || (payload => payload))(payload)
})
}), {})(behaviorsConfig);
}),
{}
)(behaviorsConfig);
}

View File

@ -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,
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,
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
};
export type PrimitiveReducerOptions = {
behaviorsConfig: PrimitiveReducerBehaviorsConfig,
locationString: string,
name: 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
@ -59,34 +64,49 @@ export const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = {
if (payload === undefined) return state;
return payload;
},
validate: true,
validate: true
},
reset: {
reducer(state, payload, initialState) {
return initialState;
},
validate: false,
},
validate: false
}
};
export function createPrimitiveReducer(primitiveType: PrimitiveType, {
locationString,
name,
}: PrimitiveReducerOptions) {
const uniqueId = Math.random().toString(36).substring(5);
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}`)),
[name]: createReducer(
primitiveType,
createReducerBehaviors(
DEFAULT_PRIMITIVE_BEHAVIORS,
`${uniqueId}-${locationString}`
)
)
},
actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, `${uniqueId}-${locationString}`),
actions: createActions(
DEFAULT_PRIMITIVE_BEHAVIORS,
`${uniqueId}-${locationString}`
)
};
}
function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducerBehaviors): PrimitiveReducer {
function createReducer(
primitiveType: PrimitiveType,
behaviors: PrimitiveReducerBehaviors
): PrimitiveReducer {
//Calculate and validate the initial state of the reducer
const initialState: mixed = validateValue(primitiveType, primitiveType().defaultValue);
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]
@ -100,30 +120,36 @@ function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducer
//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(
return reduce(
(interimState, matchedBehavior) =>
behaviors[matchedBehavior.type].reducer(
interimState,
behaviors[matchedBehavior.type].validate
? validateValue(primitiveType, matchedBehavior.payload)
: matchedBehavior.payload,
initialState
), state)(matchedBehaviors);
),
state
)(matchedBehaviors);
}
return state;
}
};
}
function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string): PrimitiveActions {
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) => ({
return reduce(
(memo, behavior, name) => ({
...memo,
[name]: (payload: mixed) => ({
type: `${locationString}.${name}`,
payload: (behavior.action || (payload => payload))(payload),
payload: (behavior.action || (payload => payload))(payload)
})
}), {})(behaviorsConfig);
}),
{}
)(behaviorsConfig);
}

View File

@ -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,
defaultValue?: any
};
export type ReducerType = () => {
type: PropTypeKeys,
structure: StructureType | PrimitiveType,
structure: StructureType | PrimitiveType
};
export type ArrayStructureType = () => {
type: PropTypeKeys,
structure: StructureType | PrimitiveType,
defaultValue: any,
}
defaultValue: any
};
export type PrimitiveType = () => {
type: PropTypeKeys,
defaultValue?: any,
typeofValue: string,
structure?: PrimitiveType,
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 = '') => () => ({
string: (defaultValue: string = "") => () => ({
type: PROP_TYPES._string,
defaultValue,
typeofValue: 'string',
typeofValue: "string"
}),
number: (defaultValue: number = 0) => () => ({
type: PROP_TYPES._number,
defaultValue,
typeofValue: 'number',
typeofValue: "number"
}),
boolean: (defaultValue: boolean = false) => () => ({
type: PROP_TYPES._boolean,
defaultValue,
typeofValue: 'boolean',
typeofValue: "boolean"
}),
any: (defaultValue: any = null) => () => ({
type: PROP_TYPES._any,
defaultValue,
typeofValue: 'any',
typeofValue: "any"
}),
arrayOf: (structure: StructureType | PrimitiveType, defaultValue = []) => () => ({
arrayOf: (
structure: StructureType | PrimitiveType,
defaultValue = []
) => () => ({
type: PROP_TYPES._array,
structure,
defaultValue,
defaultValue
}),
reducer: (structure: ShapeStructure) => () => ({
type: PROP_TYPES._reducer,
structure,
structure
}),
shape: (structure: ShapeStructure) => () => ({
type: PROP_TYPES._shape,
structure,
structure
}),
custom: ({
validator = () => true,
validationErrorMessage = (value: any) => `${ value } failed custom type validation`,
validationErrorMessage = (value: any) =>
`${value} failed custom type validation`
}: {
validator: (value: any) => boolean,
validationErrorMessage: (value: any) => string,
validationErrorMessage: (value: any) => string
} = {}) => (defaultValue: any) => () => ({
type: PROP_TYPES._custom,
defaultValue,
validator,
validationErrorMessage,
validationErrorMessage
}),
wildcardKey: () => PROP_TYPES._wildcardKey,
wildcardKey: () => PROP_TYPES._wildcardKey
};

View File

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

View File

@ -2,65 +2,89 @@
//==============================
// 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);
!!find((prop, key) => key === PROP_TYPES._wildcardKey)(
objectStructure().structure
);
export const getValueType = (objectStructure: any, key: string, wildcardKeyPresent: boolean) =>
export const getValueType = (
objectStructure: any,
key: string,
wildcardKeyPresent: boolean
) =>
wildcardKeyPresent
? objectStructure().structure[key] || objectStructure().structure[PROP_TYPES._wildcardKey]
? 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);
console.error(
`The value passed to validateObject() was not an object. Value: `,
value
);
return {};
}
const wildcardKeyPresent = hasWildcardKey(objectStructure);
return reduce(value, (memo, value, name) => {
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 }`);
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');
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,
}
}, {});
[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!
@ -70,24 +94,38 @@ export function validateValue(primitive: any, value: any): mixed {
}
//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}).`);
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> {
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);
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);
return value
.map(element => getTypeValidation(elementType)(elementStructure, element))
.filter(e => e);
}
export function getTypeValidation(type: string): validationFunction {
const TYPE_VALIDATIONS = {
[PROP_TYPES._string]: validateValue,
@ -96,7 +134,7 @@ export function getTypeValidation(type: string): validationFunction {
[PROP_TYPES._array]: validateArray,
[PROP_TYPES._shape]: validateShape,
[PROP_TYPES._any]: validateValue,
[PROP_TYPES._custom]: validateValue,
[PROP_TYPES._custom]: validateValue
};
const typeValidation = TYPE_VALIDATIONS[type];
if (!typeValidation) {