From 886e5c6b2da2e9a727e242bac77d3a363068e1df Mon Sep 17 00:00:00 2001 From: michael shanks Date: Sun, 21 Jul 2019 09:36:20 +0100 Subject: [PATCH] recursive validation of component heirarchy --- .../propsDefinitionParsing/validateProps.js | 90 +++++++++--- packages/builder/tests/validateProps.spec.js | 130 +++++++++++++++++- 2 files changed, 192 insertions(+), 28 deletions(-) diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js index dfceba1b7f..2078a5297b 100644 --- a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js +++ b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js @@ -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++; } } diff --git a/packages/builder/tests/validateProps.spec.js b/packages/builder/tests/validateProps.spec.js index 19e01cf0bd..d3a26f47a1 100644 --- a/packages/builder/tests/validateProps.spec.js +++ b/packages/builder/tests/validateProps.spec.js @@ -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"); }); -}) \ No newline at end of file +}); + +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"]); + }); + +}); \ No newline at end of file