From bfa06ac17fe44232455683c92447eb55f5d60f79 Mon Sep 17 00:00:00 2001 From: Kai Moseley Date: Thu, 23 Feb 2017 09:56:43 +0000 Subject: [PATCH] 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()). --- src/__tests__/reducers.test.js | 2 +- src/__tests__/validatePayload.test.js | 83 ++++++++++++++++++++++++++- src/reducers/arrayReducer.js | 2 +- src/structure.js | 2 + src/validatePayload.js | 23 ++++++-- 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/__tests__/reducers.test.js b/src/__tests__/reducers.test.js index f43dbc5..951e6e3 100644 --- a/src/__tests__/reducers.test.js +++ b/src/__tests__/reducers.test.js @@ -51,7 +51,7 @@ describe('reducers', () => { describe('determineReducerType', () => { 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()), { name: 'toast', locationString: 'toasty', diff --git a/src/__tests__/validatePayload.test.js b/src/__tests__/validatePayload.test.js index f4879ae..2dc7152 100644 --- a/src/__tests__/validatePayload.test.js +++ b/src/__tests__/validatePayload.test.js @@ -3,7 +3,9 @@ import { validatePrimitive, validateShape, validateArray, - getTypeValidation + getTypeValidation, + hasWildcardKey, + getValueType, } from '../validatePayload'; describe('Validation functionality', () => { @@ -91,7 +93,38 @@ describe('Validation functionality', () => { 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', + }); + }); }); 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); + }); + }); + }); diff --git a/src/reducers/arrayReducer.js b/src/reducers/arrayReducer.js index bfa76b2..1bf7254 100644 --- a/src/reducers/arrayReducer.js +++ b/src/reducers/arrayReducer.js @@ -47,7 +47,7 @@ import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils'; 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) { console.warn(`Index not passed to ${behaviorName} for payload ${payload}.`); return false; diff --git a/src/structure.js b/src/structure.js index f67ffef..3c87948 100644 --- a/src/structure.js +++ b/src/structure.js @@ -43,6 +43,7 @@ export const PROP_TYPES = { _shape: '_shape', _array: '_array', _any: '_any', + _wildcardKey: '_wildcardKey', }; //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, structure, }), + wildcardKey: () => PROP_TYPES._wildcardKey, }; diff --git a/src/validatePayload.js b/src/validatePayload.js index 7b15b36..00dd0f4 100644 --- a/src/validatePayload.js +++ b/src/validatePayload.js @@ -12,6 +12,18 @@ type validationFunction = (structure: StructureType | PrimitiveType | ShapeStruc 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]; + export function validateShape(objectStructure: any, value: mixed): Object { if (!isObject(value)) { @@ -19,20 +31,23 @@ export function validateShape(objectStructure: any, value: mixed): Object { return {}; } + const wildcardKeyPresent = hasWildcardKey(objectStructure); + return reduce(value, (memo, value, name) => { - const valueType = objectStructure().structure[name]; - //If the value type does not exist in the reducer structure, we don't want to include it in the payload. + 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); + ` and was stripped out of the payload. Structure: ${ objectStructure().structure }`); return memo; } 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. 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'); return memo; }