recursive validation of props

This commit is contained in:
michael shanks 2019-07-20 21:41:06 +01:00
parent ca74258c04
commit 0b94346104
5 changed files with 129 additions and 47 deletions

View File

@ -5,15 +5,24 @@ import {
import { types } from "./types"; import { types } from "./types";
import { assign } from "lodash"; import { assign } from "lodash";
export const createDefaultProps = (propsDefinition, derivedFromProps) => { export const createProps = (componentName, propsDefinition, derivedFromProps) => {
const error = (propName, error) =>
errors.push({propName, error});
const props = {
_component: componentName
};
const props = {};
const errors = []; const errors = [];
if(!componentName)
error("_component", "Component name not supplied");
for(let propDef in propsDefinition) { for(let propDef in propsDefinition) {
const parsedPropDef = parsePropDef(propsDefinition[propDef]); const parsedPropDef = parsePropDef(propsDefinition[propDef]);
if(parsedPropDef.error) if(parsedPropDef.error)
errors.push({propName:propDef, error:parsedPropDef.error}); error(propDef, parsedPropDef.error);
else else
props[propDef] = parsedPropDef; props[propDef] = parsedPropDef;
} }

View File

@ -2,7 +2,8 @@ import {
isString, isString,
isBoolean, isBoolean,
isNumber, isNumber,
isArray isArray,
isObjectLike
} from "lodash/fp"; } from "lodash/fp";
const defaultDef = typeName => () => ({ const defaultDef = typeName => () => ({
@ -10,17 +11,20 @@ const defaultDef = typeName => () => ({
required:false, required:false,
default:types[typeName].default, default:types[typeName].default,
options: typeName === "options" ? [] : undefined, options: typeName === "options" ? [] : undefined,
itemPropsDefinition: typeName === "array" ? "string" : undefined elementDefinition: typeName === "array" ? "string" : undefined
}); });
const propType = (defaultValue, isOfType, defaultDefinition) => ({ const propType = (defaultValue, isOfType, defaultDefinition) => ({
isOfType, default:defaultValue, defaultDefinition isOfType, default:defaultValue, defaultDefinition
}); });
const isComponent = isObjectLike;
export const types = { export const types = {
string: propType(() => "", isString, defaultDef("string")), string: propType(() => "", isString, defaultDef("string")),
bool: propType(() => false, isBoolean, defaultDef("bool")), bool: propType(() => false, isBoolean, defaultDef("bool")),
number: propType(() => 0, isNumber, defaultDef("number")), number: propType(() => 0, isNumber, defaultDef("number")),
array: propType(() => [], isArray, defaultDef("array")), array: propType(() => [], isArray, defaultDef("array")),
options: propType(() => "", isString, defaultDef("options")) options: propType(() => "", isString, defaultDef("options")),
component: propType(() => ({_component:""}), isComponent, defaultDef("component"))
}; };

View File

@ -1,5 +1,5 @@
import { types } from "./types"; import { types } from "./types";
import { createDefaultProps } from "./createDefaultProps"; import { createProps } from "./createProps";
import { isString } from "util"; import { isString } from "util";
import { import {
includes, includes,
@ -17,11 +17,40 @@ const makeError = (errors, propName) => (message) =>
propName, propName,
error:message}); error:message});
export const recursivelyValidate = (rootProps, getComponentPropsDefinition, stack=[]) => {
const propsDef = getComponentPropsDefinition(
rootProps._component);
const errors = validateProps(
propsDef,
rootProps);
// adding name to object.... for ease
const childErrors = pipe(propsDef, [
keys,
map(k => ({...propsDef[k], name:k })),
filter(d => d.type === "component"),
map(d => ({
errs:recursivelyValidate(
rootProps[d.name],
d.def,
[...stack, propsDef]),
def:d
}))
]);
}
export const validateProps = (propsDefinition, props, isFinal=true) => { export const validateProps = (propsDefinition, props, isFinal=true) => {
const errors = []; const errors = [];
if(!props._component)
makeError(errors, "_component")("Component is not set");
for(let propDefName in propsDefinition) { for(let propDefName in propsDefinition) {
if(propDefName === "_component") continue;
let propDef = propsDefinition[propDefName]; let propDef = propsDefinition[propDefName];
@ -46,7 +75,7 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
if(propDef.type === "array") { if(propDef.type === "array") {
for(let arrayItem of propValue) { for(let arrayItem of propValue) {
const arrayErrs = validateProps( const arrayErrs = validateProps(
propDef.itemPropsDefinition, propDef.elementDefinition,
arrayItem, arrayItem,
isFinal isFinal
) )
@ -61,31 +90,32 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
&& !includes(propValue)(propDef.options)) { && !includes(propValue)(propDef.options)) {
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`); error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`);
} }
} }
return errors; return errors;
} }
export const validatePropsDefinition = (propsDefinition) => { export const validatePropsDefinition = (propsDefinition) => {
const { errors } = createDefaultProps(propsDefinition); const { errors } = createProps("dummy_component_name", propsDefinition);
// arrar props without itemPropsDefinition // arrar props without elementDefinition
pipe(propsDefinition, [ pipe(propsDefinition, [
keys, keys,
map(k => ({ map(k => ({
propDef:propsDefinition[k], propDef:propsDefinition[k],
propName:k propName:k
})), })),
filter(d => d.propDef.type === "array" && !d.propDef.itemPropsDefinition), filter(d => d.propDef.type === "array" && !d.propDef.elementDefinition),
each(d => makeError(errors, d.propName)(`${d.propName} does not have a definition for it's item props`)) each(d => makeError(errors, d.propName)(`${d.propName} does not have a definition for it's item props`))
]); ]);
const arrayPropValidationErrors = pipe(propsDefinition, [ const arrayPropValidationErrors = pipe(propsDefinition, [
keys, keys,
map(k => propsDefinition[k]), map(k => propsDefinition[k]),
filter(d => d.type === "array" && d.itemPropsDefinition), filter(d => d.type === "array" && d.elementDefinition),
map(d => validatePropsDefinition(d.itemPropsDefinition)), map(d => validatePropsDefinition(d.elementDefinition)),
flatten flatten
]); ]);

View File

@ -1,4 +1,4 @@
import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps"; import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps";
import { import {
keys, some keys, some
} from "lodash/fp"; } from "lodash/fp";
@ -10,12 +10,33 @@ describe("createDefaultProps", () => {
fieldName: {type:"string", default:"something"} fieldName: {type:"string", default:"something"}
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.fieldName).toBeDefined();
expect(props.fieldName).toBe("something"); expect(props.fieldName).toBe("something");
expect(keys(props).length).toBe(1); expect(keys(props).length).toBe(2);
});
it("should set component name", () => {
const propDef = {
fieldName: {type:"string", default:"something"}
};
const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]);
expect(props._component).toBe("some_component");
});
it("should return error when component name not supplied", () => {
const propDef = {
fieldName: {type:"string", default:"something"}
};
const { errors } = createProps("",propDef);
expect(errors.length).toEqual(1);
}); });
it("should create a object with single blank string value, when no default", () => { it("should create a object with single blank string value, when no default", () => {
@ -23,7 +44,7 @@ describe("createDefaultProps", () => {
fieldName: {type:"string"} fieldName: {type:"string"}
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.fieldName).toBeDefined();
@ -35,7 +56,7 @@ describe("createDefaultProps", () => {
fieldName: "string" fieldName: "string"
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.fieldName).toBeDefined();
@ -44,38 +65,50 @@ describe("createDefaultProps", () => {
it("should create a object with single fals value, when prop definition is 'bool' ", () => { it("should create a object with single fals value, when prop definition is 'bool' ", () => {
const propDef = { const propDef = {
fieldName: "bool" isVisible: "bool"
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.isVisible).toBeDefined();
expect(props.fieldName).toBe(false); expect(props.isVisible).toBe(false);
}); });
it("should create a object with single 0 value, when prop definition is 'number' ", () => { it("should create a object with single 0 value, when prop definition is 'number' ", () => {
const propDef = { const propDef = {
fieldName: "number" width: "number"
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.width).toBeDefined();
expect(props.fieldName).toBe(0); expect(props.width).toBe(0);
}); });
it("should create a object with single 0 value, when prop definition is 'array' ", () => { it("should create a object with single empty array, when prop definition is 'array' ", () => {
const propDef = { const propDef = {
fieldName: "array" columns: "array"
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.columns).toBeDefined();
expect(props.fieldName).toEqual([]); expect(props.columns).toEqual([]);
});
it("should create a object with single empty component props, when prop definition is 'component' ", () => {
const propDef = {
content: "component"
};
const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]);
expect(props.content).toBeDefined();
expect(props.content).toEqual({_component:""});
}); });
it("should create an object with multiple prop names", () => { it("should create an object with multiple prop names", () => {
@ -84,14 +117,14 @@ describe("createDefaultProps", () => {
fieldLength: { type: "number", default: 500 } fieldLength: { type: "number", default: 500 }
}; };
const { props, errors } = createDefaultProps(propDef); const { props, errors } = createProps("some_component",propDef);
expect(errors).toEqual([]); expect(errors).toEqual([]);
expect(props.fieldName).toBeDefined(); expect(props.fieldName).toBeDefined();
expect(props.fieldName).toBe(""); expect(props.fieldName).toBe("");
expect(props.fieldLength).toBeDefined(); expect(props.fieldLength).toBeDefined();
expect(props.fieldLength).toBe(500); expect(props.fieldLength).toBe(500);
expect(keys(props).length).toBe(2); expect(keys(props).length).toBe(3);
}) })
it("should return error when invalid type", () => { it("should return error when invalid type", () => {
@ -100,7 +133,7 @@ describe("createDefaultProps", () => {
fieldLength: { type: "invalid type name "} fieldLength: { type: "invalid type name "}
}; };
const { errors } = createDefaultProps(propDef); const { errors } = createProps("some_component",propDef);
expect(errors.length).toBe(2); expect(errors.length).toBe(2);
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy(); expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
@ -112,7 +145,7 @@ describe("createDefaultProps", () => {
fieldName: {type:"string", default: 1} fieldName: {type:"string", default: 1}
}; };
const { errors } = createDefaultProps(propDef); const { errors } = createProps("some_component",propDef);
expect(errors.length).toBe(1); expect(errors.length).toBe(1);
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy(); expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
@ -125,10 +158,11 @@ describe("createDefaultProps", () => {
}; };
const derivedFrom = { const derivedFrom = {
_component:"root",
fieldName: "surname" fieldName: "surname"
}; };
const { props, errors } = createDefaultProps(propDef, [derivedFrom]); const { props, errors } = createProps("some_component",propDef, [derivedFrom]);
expect(errors.length).toBe(0); expect(errors.length).toBe(0);
expect(props.fieldName).toBe("surname"); expect(props.fieldName).toBe("surname");
@ -139,23 +173,31 @@ describe("createDefaultProps", () => {
it("should merge in derived props, last in list taking priority", () => { it("should merge in derived props, last in list taking priority", () => {
const propDef = { const propDef = {
fieldName: "string", fieldName: "string",
fieldLength: { type: "number", default: 500} fieldLength: { type: "number", default: 500},
header: "component",
content: {
type: "component",
default: { _component: "childcomponent", wdith: 500 }
}
}; };
const derivedFrom1 = { const derivedFrom1 = {
_component:"root",
fieldName: "surname", fieldName: "surname",
fieldLength: 200 fieldLength: 200
}; };
const derivedFrom2 = { const derivedFrom2 = {
_component:"child",
fieldName: "forename" fieldName: "forename"
}; };
const { props, errors } = createDefaultProps(propDef, [derivedFrom1, derivedFrom2]); const { props, errors } = createProps("some_component",propDef, [derivedFrom1, derivedFrom2]);
expect(errors.length).toBe(0); expect(errors.length).toBe(0);
expect(props.fieldName).toBe("forename"); expect(props.fieldName).toBe("forename");
expect(props.fieldLength).toBe(200); expect(props.fieldLength).toBe(200);
expect(props._component).toBe("child");
}); });

View File

@ -2,10 +2,7 @@ import {
validatePropsDefinition, validatePropsDefinition,
validateProps validateProps
} from "../src/userInterface/propsDefinitionParsing/validateProps"; } from "../src/userInterface/propsDefinitionParsing/validateProps";
import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps"; import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps";
import {
keys, some
} from "lodash/fp";
// not that allot of this functionality is covered // not that allot of this functionality is covered
// in createDefaultProps - as validate props uses that. // in createDefaultProps - as validate props uses that.
@ -17,7 +14,7 @@ describe("validatePropsDefinition", () => {
const propsDef = { const propsDef = {
columns : { columns : {
type: "array", type: "array",
itemPropsDefinition: { elementDefinition: {
width: "number", width: "number",
units: { units: {
type: "string", type: "string",
@ -38,7 +35,7 @@ describe("validatePropsDefinition", () => {
const propsDef = { const propsDef = {
columns : { columns : {
type: "array", type: "array",
itemPropsDefinition: { elementDefinition: {
width: "invlid type", width: "invlid type",
units: { units: {
type: "string", type: "string",
@ -97,7 +94,7 @@ const validPropDef = {
rowCount : "number", rowCount : "number",
columns : { columns : {
type: "array", type: "array",
itemPropsDefinition: { elementDefinition: {
width: "number", width: "number",
units: { units: {
type: "string", type: "string",
@ -109,9 +106,9 @@ const validPropDef = {
}; };
const validProps = () => { const validProps = () => {
const { props } = createDefaultProps(validPropDef); const { props } = createProps("some_component", validPropDef);
props.columns.push( props.columns.push(
createDefaultProps(validPropDef.columns.itemPropsDefinition).props); createProps("childcomponent", validPropDef.columns.elementDefinition).props);
return props; return props;
} }