From ca74258c048fc5adcd5ecb83093fc2dc313f76c6 Mon Sep 17 00:00:00 2001 From: michael shanks Date: Fri, 19 Jul 2019 18:03:58 +0100 Subject: [PATCH] validating component props --- .../createDefaultProps.js | 23 +-- .../propsDefinitionParsing/types.js | 26 +++ .../propsDefinitionParsing/validateProps.js | 106 ++++++++++++ ...ing.spec.js => createDefaultProps.spec.js} | 0 packages/builder/tests/validateProps.spec.js | 156 ++++++++++++++++++ 5 files changed, 296 insertions(+), 15 deletions(-) create mode 100644 packages/builder/src/userInterface/propsDefinitionParsing/types.js create mode 100644 packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js rename packages/builder/tests/{propsParsing.spec.js => createDefaultProps.spec.js} (100%) create mode 100644 packages/builder/tests/validateProps.spec.js diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js index 1db0f33c2a..083fda3b27 100644 --- a/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js +++ b/packages/builder/src/userInterface/propsDefinitionParsing/createDefaultProps.js @@ -1,11 +1,8 @@ import { isString, - isBoolean, - isNumber, - isArray, isUndefined } from "lodash/fp"; - +import { types } from "./types"; import { assign } from "lodash"; export const createDefaultProps = (propsDefinition, derivedFromProps) => { @@ -31,7 +28,7 @@ export const createDefaultProps = (propsDefinition, derivedFromProps) => { } const parsePropDef = propDef => { - const error = message => ({error:message}); + const error = message => ({error:message, propDef}); if(isString(propDef)) { if(!types[propDef]) @@ -56,13 +53,9 @@ const parsePropDef = propDef => { return propDef.default; } -const propType = (defaultValue, isOfType) => ({ - isOfType, default:defaultValue -}); - -const types = { - string: propType(() => "", isString), - bool: propType(() => false, isBoolean), - number: propType(() => 0, isNumber), - array: propType(() => [], isArray) -} \ No newline at end of file +/* +Allowed propDefOptions +- type: string, bool, number, array +- default: default value, when undefined +- required: field is required +*/ \ No newline at end of file diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/types.js b/packages/builder/src/userInterface/propsDefinitionParsing/types.js new file mode 100644 index 0000000000..30e1b77f0d --- /dev/null +++ b/packages/builder/src/userInterface/propsDefinitionParsing/types.js @@ -0,0 +1,26 @@ +import { + isString, + isBoolean, + isNumber, + isArray +} from "lodash/fp"; + +const defaultDef = typeName => () => ({ + type: typeName, + required:false, + default:types[typeName].default, + options: typeName === "options" ? [] : undefined, + itemPropsDefinition: typeName === "array" ? "string" : undefined +}); + +const propType = (defaultValue, isOfType, defaultDefinition) => ({ + isOfType, default:defaultValue, defaultDefinition +}); + +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")) +}; \ No newline at end of file diff --git a/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js new file mode 100644 index 0000000000..866daf2c6a --- /dev/null +++ b/packages/builder/src/userInterface/propsDefinitionParsing/validateProps.js @@ -0,0 +1,106 @@ +import { types } from "./types"; +import { createDefaultProps } from "./createDefaultProps"; +import { isString } from "util"; +import { + includes, + filter, + map, + keys, + flatten, + each } from "lodash/fp"; +import { common } from "budibase-core"; + +const pipe = common.$; + +const makeError = (errors, propName) => (message) => + errors.push({ + propName, + error:message}); + +export const validateProps = (propsDefinition, props, isFinal=true) => { + + const errors = []; + + for(let propDefName in propsDefinition) { + + let propDef = propsDefinition[propDefName]; + + if(isString(propDef)) + propDef = types[propDef].defaultDefinition(); + + const type = types[propDef.type]; + + const error = makeError(errors, propDefName); + + const propValue = props[propDefName]; + if(isFinal && propDef.required && propValue) { + error(`Property ${propDefName} is required`); + continue; + } + + if(!type.isOfType(propValue)) { + error(`Property ${propDefName} is not of type ${propDef.type}. Actual value ${propValue}`) + continue; + } + + if(propDef.type === "array") { + for(let arrayItem of propValue) { + const arrayErrs = validateProps( + propDef.itemPropsDefinition, + arrayItem, + isFinal + ) + for(let arrErr of arrayErrs) { + errors.push(arrErr); + } + } + } + + if(propDef.type === "options" + && propValue + && !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); + + + // arrar props without itemPropsDefinition + pipe(propsDefinition, [ + keys, + map(k => ({ + propDef:propsDefinition[k], + propName:k + })), + filter(d => d.propDef.type === "array" && !d.propDef.itemPropsDefinition), + 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)), + flatten + ]); + + pipe(propsDefinition, [ + keys, + map(k => ({ + propDef:propsDefinition[k], + propName:k + })), + filter(d => d.propDef.type === "options" + && (!d.propDef.options || d.propDef.options.length === 0)), + each(d => makeError(errors, d.propName)(`${d.propName} does not have any options`)) + ]); + + return [...errors, ...arrayPropValidationErrors] + +} + diff --git a/packages/builder/tests/propsParsing.spec.js b/packages/builder/tests/createDefaultProps.spec.js similarity index 100% rename from packages/builder/tests/propsParsing.spec.js rename to packages/builder/tests/createDefaultProps.spec.js diff --git a/packages/builder/tests/validateProps.spec.js b/packages/builder/tests/validateProps.spec.js new file mode 100644 index 0000000000..9f63fc31ff --- /dev/null +++ b/packages/builder/tests/validateProps.spec.js @@ -0,0 +1,156 @@ +import { + validatePropsDefinition, + validateProps +} from "../src/userInterface/propsDefinitionParsing/validateProps"; +import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps"; +import { + keys, some +} from "lodash/fp"; + +// not that allot of this functionality is covered +// in createDefaultProps - as validate props uses that. + +describe("validatePropsDefinition", () => { + + it("should recursively validate array props and return no errors when valid", () => { + + const propsDef = { + columns : { + type: "array", + itemPropsDefinition: { + width: "number", + units: { + type: "string", + default: "px" + } + } + } + } + + const errors = validatePropsDefinition(propsDef); + + expect(errors).toEqual([]); + + }); + + it("should recursively validate array props and return errors when invalid", () => { + + const propsDef = { + columns : { + type: "array", + itemPropsDefinition: { + width: "invlid type", + units: { + type: "string", + default: "px" + } + } + } + } + + const errors = validatePropsDefinition(propsDef); + + expect(errors.length).toEqual(1); + expect(errors[0].propName).toBe("width"); + + }); + + it("should return error when no options for options field", () => { + + const propsDef = { + size: { + type: "options", + options: [] + } + } + + const errors = validatePropsDefinition(propsDef); + + expect(errors.length).toEqual(1); + expect(errors[0].propName).toBe("size"); + + }); + + it("should not return error when options field has options", () => { + + const propsDef = { + size: { + type: "options", + options: ["small", "medium", "large"] + } + } + + const errors = validatePropsDefinition(propsDef); + + expect(errors).toEqual([]); + + }); + +}); + +const validPropDef = { + size: { + type: "options", + options: ["small", "medium", "large"], + default:"medium" + }, + rowCount : "number", + columns : { + type: "array", + itemPropsDefinition: { + width: "number", + units: { + type: "string", + default: "px" + } + } + } + +}; + +const validProps = () => { + const { props } = createDefaultProps(validPropDef); + props.columns.push( + createDefaultProps(validPropDef.columns.itemPropsDefinition).props); + return props; +} + +describe("validateProps", () => { + + it("should have no errors with a big list of valid props", () => { + + const errors = validateProps(validPropDef, validProps(), true); + expect(errors).toEqual([]); + + }); + + it("should return error with invalid value", () => { + + const props = validProps(); + props.rowCount = "1"; + const errors = validateProps(validPropDef, props, true); + expect(errors.length).toEqual(1); + expect(errors[0].propName).toBe("rowCount"); + + }); + + it("should return error with invalid option", () => { + + const props = validProps(); + props.size = "really_small"; + const errors = validateProps(validPropDef, props, true); + expect(errors.length).toEqual(1); + expect(errors[0].propName).toBe("size"); + + }); + + it("should return error with invalid array item", () => { + + const props = validProps(); + props.columns[0].width = "seven"; + const errors = validateProps(validPropDef, props, true); + expect(errors.length).toEqual(1); + expect(errors[0].propName).toBe("width"); + + }); +}) \ No newline at end of file