From 0b943461049bd259209438a8dc43b838e1e7fd17 Mon Sep 17 00:00:00 2001 From: michael shanks Date: Sat, 20 Jul 2019 21:41:06 +0100 Subject: [PATCH] recursive validation of props --- .../{createDefaultProps.js => createProps.js} | 15 ++- .../propsDefinitionParsing/types.js | 10 +- .../propsDefinitionParsing/validateProps.js | 44 +++++++-- .../builder/tests/createDefaultProps.spec.js | 92 ++++++++++++++----- packages/builder/tests/validateProps.spec.js | 15 ++- 5 files changed, 129 insertions(+), 47 deletions(-) rename packages/builder/src/userInterface/propsDefinitionParsing/{createDefaultProps.js => createProps.js} (78%) diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/createProps.js similarity index 78% rename from packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js rename to packages/builder/src/userInterface/propsDefinitionParsing/createProps.js index 083fda3b27..b770c07b72 100644 --- a/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js +++ b/packages/builder/src/userInterface/propsDefinitionParsing/createProps.js @@ -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; } diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/types.js b/packages/builder/src/userInterface/propsDefinitionParsing/types.js index 30e1b77f0d..ee7232dada 100644 --- a/packages/builder/src/userInterface/propsDefinitionParsing/types.js +++ b/packages/builder/src/userInterface/propsDefinitionParsing/types.js @@ -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")) }; \ No newline at end of file diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js index 866daf2c6a..dfceba1b7f 100644 --- a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js +++ b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js @@ -1,5 +1,5 @@ import { types } from "./types"; -import { createDefaultProps } from "./createDefaultProps"; +import { createProps } from "./createProps"; import { isString } from "util"; import { includes, @@ -17,11 +17,40 @@ 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]; @@ -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 ]); diff --git a/packages/builder/tests/createDefaultProps.spec.js b/packages/builder/tests/createDefaultProps.spec.js index 13dc77c8a8..c3a09851bd 100644 --- a/packages/builder/tests/createDefaultProps.spec.js +++ b/packages/builder/tests/createDefaultProps.spec.js @@ -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"); }); diff --git a/packages/builder/tests/validateProps.spec.js b/packages/builder/tests/validateProps.spec.js index 9f63fc31ff..19e01cf0bd 100644 --- a/packages/builder/tests/validateProps.spec.js +++ b/packages/builder/tests/validateProps.spec.js @@ -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; }