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 { 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 = [];
if(!componentName)
error("_component", "Component name not supplied");
for(let propDef in propsDefinition) {
const parsedPropDef = parsePropDef(propsDefinition[propDef]);
if(parsedPropDef.error)
errors.push({propName:propDef, error:parsedPropDef.error});
error(propDef, parsedPropDef.error);
else
props[propDef] = parsedPropDef;
}

View File

@ -2,7 +2,8 @@ import {
isString,
isBoolean,
isNumber,
isArray
isArray,
isObjectLike
} from "lodash/fp";
const defaultDef = typeName => () => ({
@ -10,17 +11,20 @@ const defaultDef = typeName => () => ({
required:false,
default:types[typeName].default,
options: typeName === "options" ? [] : undefined,
itemPropsDefinition: typeName === "array" ? "string" : undefined
elementDefinition: typeName === "array" ? "string" : undefined
});
const propType = (defaultValue, isOfType, defaultDefinition) => ({
isOfType, default:defaultValue, defaultDefinition
});
const isComponent = isObjectLike;
export const types = {
string: propType(() => "", isString, defaultDef("string")),
bool: propType(() => false, isBoolean, defaultDef("bool")),
number: propType(() => 0, isNumber, defaultDef("number")),
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 { createDefaultProps } from "./createDefaultProps";
import { createProps } from "./createProps";
import { isString } from "util";
import {
includes,
@ -17,12 +17,41 @@ const makeError = (errors, propName) => (message) =>
propName,
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) => {
const errors = [];
if(!props._component)
makeError(errors, "_component")("Component is not set");
for(let propDefName in propsDefinition) {
if(propDefName === "_component") continue;
let propDef = propsDefinition[propDefName];
if(isString(propDef))
@ -46,7 +75,7 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
if(propDef.type === "array") {
for(let arrayItem of propValue) {
const arrayErrs = validateProps(
propDef.itemPropsDefinition,
propDef.elementDefinition,
arrayItem,
isFinal
)
@ -61,31 +90,32 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
&& !includes(propValue)(propDef.options)) {
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`);
}
}
return errors;
}
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, [
keys,
map(k => ({
propDef:propsDefinition[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`))
]);
const arrayPropValidationErrors = pipe(propsDefinition, [
keys,
map(k => propsDefinition[k]),
filter(d => d.type === "array" && d.itemPropsDefinition),
map(d => validatePropsDefinition(d.itemPropsDefinition)),
filter(d => d.type === "array" && d.elementDefinition),
map(d => validatePropsDefinition(d.elementDefinition)),
flatten
]);

View File

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

View File

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