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

View File

@ -1,6 +1,7 @@
import { import {
validatePropsDefinition, validatePropsDefinition,
validateProps validateProps,
recursivelyValidate
} from "../src/userInterface/propsDefinitionParsing/validateProps"; } from "../src/userInterface/propsDefinitionParsing/validateProps";
import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps"; 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", () => { 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([]); expect(errors).toEqual([]);
}); });
@ -125,7 +126,7 @@ describe("validateProps", () => {
const props = validProps(); const props = validProps();
props.rowCount = "1"; props.rowCount = "1";
const errors = validateProps(validPropDef, props, true); const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1); expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("rowCount"); expect(errors[0].propName).toBe("rowCount");
@ -135,7 +136,7 @@ describe("validateProps", () => {
const props = validProps(); const props = validProps();
props.size = "really_small"; props.size = "really_small";
const errors = validateProps(validPropDef, props, true); const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1); expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("size"); expect(errors[0].propName).toBe("size");
@ -145,9 +146,126 @@ describe("validateProps", () => {
const props = validProps(); const props = validProps();
props.columns[0].width = "seven"; props.columns[0].width = "seven";
const errors = validateProps(validPropDef, props, true); const errors = validateProps(validPropDef, props, [], true);
expect(errors.length).toEqual(1); expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("width"); 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"]);
});
});