Add wildcardKey type

Being able to type a known structure ahead of time is super useful. Sometimes, however, this isn't possible. What if
your application has an entirely dynamic portion, where you use data from the server to build, day, a form? In this situation
it'd be great to still be able to type. This commit introduces the basic implementation of the 'wildcardKey' type, for use
in defining an object property. At the moment, every object can have one wildcardKey which specifies a specific type (including any()) which the reducer will happily accept, if an unknown property has it. The aim is to introduce regex to this type, and the ability to use multiple instances of the type, in order to specifically craft dynamic reducer structures (e.g. all properties with _date in them will be assigned to the Type.any(), but all others must be Type.string()).
This commit is contained in:
Kai Moseley 2017-02-23 09:56:43 +00:00
parent be42ab5958
commit bfa06ac17f
5 changed files with 104 additions and 8 deletions

View File

@ -51,7 +51,7 @@ describe('reducers', () => {
describe('determineReducerType', () => { describe('determineReducerType', () => {
it('should return the correct creator function for the default mapping', () => { it('should return the correct creator function for the default mapping', () => {
forEach(omit(Types, 'reducer'), structureType => { forEach(omit(Types, 'reducer', 'wildcardKey'), structureType => {
const returnVal = determineReducerType(Types.reducer(structureType()), { const returnVal = determineReducerType(Types.reducer(structureType()), {
name: 'toast', name: 'toast',
locationString: 'toasty', locationString: 'toasty',

View File

@ -3,7 +3,9 @@ import {
validatePrimitive, validatePrimitive,
validateShape, validateShape,
validateArray, validateArray,
getTypeValidation getTypeValidation,
hasWildcardKey,
getValueType,
} from '../validatePayload'; } from '../validatePayload';
describe('Validation functionality', () => { describe('Validation functionality', () => {
@ -91,7 +93,38 @@ describe('Validation functionality', () => {
test1: {}, test1: {},
test2: 'bar', 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',
});
});
}); });
describe('Non covered types', () => { describe('Non covered types', () => {
@ -100,4 +133,50 @@ describe('Validation functionality', () => {
}); });
}); });
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);
});
});
}); });

View File

@ -47,7 +47,7 @@ import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils';
import { PROP_TYPES } from '../structure'; import { PROP_TYPES } from '../structure';
function checkIndex(index: ?number, payload: any = '', behaviorName: string = ''): boolean { function checkIndex(index: ?number, payload: any = '', behaviorName: string): boolean {
if (!isNumber(index) || index === -1) { if (!isNumber(index) || index === -1) {
console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`); console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`);
return false; return false;

View File

@ -43,6 +43,7 @@ export const PROP_TYPES = {
_shape: '_shape', _shape: '_shape',
_array: '_array', _array: '_array',
_any: '_any', _any: '_any',
_wildcardKey: '_wildcardKey',
}; };
//The types objects are used in order to build up the structure of a store chunk, and provide/accept //The types objects are used in order to build up the structure of a store chunk, and provide/accept
@ -81,4 +82,5 @@ export const Types: TypesObject = {
type: PROP_TYPES._shape, type: PROP_TYPES._shape,
structure, structure,
}), }),
wildcardKey: () => PROP_TYPES._wildcardKey,
}; };

View File

@ -12,6 +12,18 @@ type validationFunction = (structure: StructureType | PrimitiveType | ShapeStruc
import reduce from 'lodash/reduce'; import reduce from 'lodash/reduce';
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import { PROP_TYPES } from './structure'; 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];
export function validateShape(objectStructure: any, value: mixed): Object { export function validateShape(objectStructure: any, value: mixed): Object {
if (!isObject(value)) { if (!isObject(value)) {
@ -19,20 +31,23 @@ export function validateShape(objectStructure: any, value: mixed): Object {
return {}; return {};
} }
const wildcardKeyPresent = hasWildcardKey(objectStructure);
return reduce(value, (memo, value, name) => { return reduce(value, (memo, value, name) => {
const valueType = objectStructure().structure[name]; const valueType = getValueType(objectStructure, name, wildcardKeyPresent);
//If the value type does not exist in the reducer structure, we don't want to include it in the payload. //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. //Display a console error for the developer, and skip the inclusion of this property in the payload.
if (!valueType) { if (!valueType) {
console.warn(`The property, ${name}, was not specified in the structure` + console.warn(`The property, ${name}, was not specified in the structure` +
' and was stripped out of the payload. Structure: ', objectStructure().structure); ` and was stripped out of the payload. Structure: ${ objectStructure().structure }`);
return memo; return memo;
} }
const validatedValue = getTypeValidation(valueType().type)(valueType, value); const validatedValue = getTypeValidation(valueType().type)(valueType, value);
if (validatedValue === undefined) { if (validatedValue === undefined) {
console.warn(`The property, ${name}, was populated with a type ${ typeof value } which does not` + console.warn(`The property, ${name}, was populated with a type ${ typeof value } which does not` +
' match that specified in the reducer configuration. It has been stripped from' + ` match that specified in the reducer configuration ${ wildcardKeyPresent ? ', nor did it match a wildcardKey': ''}. It has been stripped from` +
' the payload'); ' the payload');
return memo; return memo;
} }