recursive validation of component heirarchy

This commit is contained in:
michael shanks 2019-07-21 09:36:20 +01:00
parent 0b94346104
commit 886e5c6b2d
2 changed files with 192 additions and 28 deletions

View File

@ -7,59 +7,101 @@ import {
map,
keys,
flatten,
each } from "lodash/fp";
flattenDeep,
each,
indexOf } from "lodash/fp";
import { common } from "budibase-core";
const pipe = common.$;
const makeError = (errors, propName) => (message) =>
const makeError = (errors, propName, stack) => (message) =>
errors.push({
stack,
propName,
error:message});
export const recursivelyValidate = (rootProps, getComponentPropsDefinition, stack=[]) => {
export const recursivelyValidate = (rootProps, getComponent, stack=[]) => {
const getComponentPropsDefinition = componentName => {
if(componentName.includes(":")) {
const [parentComponent, arrayProp] = componentName.split(":");
return getComponent(parentComponent)[arrayProp].elementDefinition;
}
return getComponent(componentName);
}
const propsDef = getComponentPropsDefinition(
rootProps._component);
const getPropsDefArray = (def) => pipe(def, [
keys,
map(k => def[k].name
? expandPropDef(def[k])
: ({
...expandPropDef(def[k]),
name:k }))
]);
const propsDefArray = getPropsDefArray(propsDef);
const errors = validateProps(
propsDef,
rootProps);
rootProps,
stack,
true);
// adding name to object.... for ease
const childErrors = pipe(propsDef, [
keys,
map(k => ({...propsDef[k], name:k })),
const validateChildren = (_defArray, _props, _stack) => pipe(_defArray, [
filter(d => d.type === "component"),
map(d => ({
errs:recursivelyValidate(
rootProps[d.name],
d.def,
[...stack, propsDef]),
def:d
}))
map(d => recursivelyValidate(
_props[d.name],
getComponentPropsDefinition,
[..._stack, d.name])),
flatten
]);
const childErrors = validateChildren(
propsDefArray, rootProps, stack);
const childArrayErrors = pipe(propsDefArray, [
filter(d => d.type === "array"),
map(d => pipe(rootProps[d.name], [
map(elementProps => pipe(elementProps._component, [
getComponentPropsDefinition,
getPropsDefArray,
arr => validateChildren(
arr,
elementProps,
[...stack,
`${d.name}[${indexOf(elementProps)(rootProps[d.name])}]`])
]))
]))
]);
return flattenDeep([errors, ...childErrors, ...childArrayErrors]);
}
export const validateProps = (propsDefinition, props, isFinal=true) => {
const expandPropDef = propDef =>
isString(propDef)
? types[propDef].defaultDefinition()
: propDef;
export const validateProps = (propsDefinition, props, stack=[], isFinal=true) => {
const errors = [];
if(!props._component)
makeError(errors, "_component")("Component is not set");
makeError(errors, "_component", stack)("Component is not set");
for(let propDefName in propsDefinition) {
if(propDefName === "_component") continue;
let propDef = propsDefinition[propDefName];
if(isString(propDef))
propDef = types[propDef].defaultDefinition();
const propDef = expandPropDef(propsDefinition[propDefName]);
const type = types[propDef.type];
const error = makeError(errors, propDefName);
const error = makeError(errors, propDefName, stack);
const propValue = props[propDefName];
if(isFinal && propDef.required && propValue) {
@ -73,15 +115,19 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
}
if(propDef.type === "array") {
let index = 0;
for(let arrayItem of propValue) {
arrayItem._component = `${props._component}:${propDefName}`
const arrayErrs = validateProps(
propDef.elementDefinition,
arrayItem,
[...stack, `${propDefName}[${index}]`],
isFinal
)
for(let arrErr of arrayErrs) {
errors.push(arrErr);
}
index++;
}
}

View File

@ -1,6 +1,7 @@
import {
validatePropsDefinition,
validateProps
validateProps,
recursivelyValidate
} from "../src/userInterface/propsDefinitionParsing/validateProps";
import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps";
@ -116,7 +117,7 @@ describe("validateProps", () => {
it("should have no errors with a big list of valid props", () => {
const errors = validateProps(validPropDef, validProps(), true);
const errors = validateProps(validPropDef, validProps(), [], true);
expect(errors).toEqual([]);
});
@ -125,7 +126,7 @@ describe("validateProps", () => {
const props = validProps();
props.rowCount = "1";
const errors = validateProps(validPropDef, props, true);
const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("rowCount");
@ -135,7 +136,7 @@ describe("validateProps", () => {
const props = validProps();
props.size = "really_small";
const errors = validateProps(validPropDef, props, true);
const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("size");
@ -145,9 +146,126 @@ describe("validateProps", () => {
const props = validProps();
props.columns[0].width = "seven";
const errors = validateProps(validPropDef, props, true);
const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("width");
});
})
});
describe("recursivelyValidateProps", () => {
const rootComponent = {
width: "number",
child: "component",
navitems: {
type: "array",
elementDefinition: {
name: "string",
icon: "component"
}
}
};
const todoListComponent = {
showTitle: "bool",
header: "component"
};
const headerComponent = {
text: "string"
}
const iconComponent = {
iconName: "string"
}
const getComponent = name => ({
rootComponent,
todoListComponent,
headerComponent,
iconComponent
})[name];
const rootProps = () => ({
_component: "rootComponent",
width: 100,
child: {
_component: "todoListComponent",
showTitle: true,
header: {
_component: "headerComponent",
text: "Your todo list"
}
},
navitems: [
{
name: "Main",
icon: {
_component: "iconComponent",
iconName:"fa fa-list"
}
},
{
name: "Settings",
icon: {
_component: "iconComponent",
iconName:"fa fa-cog"
}
}
]
});
it("should return no errors for valid structure", () => {
const result = recursivelyValidate(
rootProps(),
getComponent);
expect(result).toEqual([]);
});
it("should return error on root component", () => {
const root = rootProps();
root.width = "yeeeoooo";
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].propName).toBe("width");
});
it("should return error on first nested child component", () => {
const root = rootProps();
root.child.showTitle = "yeeeoooo";
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].stack).toEqual(["child"]);
expect(result[0].propName).toBe("showTitle");
});
it("should return error on second nested child component", () => {
const root = rootProps();
root.child.header.text = false;
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].stack).toEqual(["child", "header"]);
expect(result[0].propName).toBe("text");
});
it("should return error on invalid array prop", () => {
const root = rootProps();
root.navitems[1].name = false;
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].propName).toBe("name");
expect(result[0].stack).toEqual(["navitems[1]"]);
});
it("should return error on invalid array child", () => {
const root = rootProps();
root.navitems[1].icon.iconName = false;
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].propName).toBe("iconName");
expect(result[0].stack).toEqual(["navitems[1]", "icon"]);
});
});