validating component props

This commit is contained in:
michael shanks 2019-07-19 18:03:58 +01:00
parent a81af3f18b
commit ca74258c04
5 changed files with 296 additions and 15 deletions

View File

@ -1,11 +1,8 @@
import { import {
isString, isString,
isBoolean,
isNumber,
isArray,
isUndefined isUndefined
} from "lodash/fp"; } from "lodash/fp";
import { types } from "./types";
import { assign } from "lodash"; import { assign } from "lodash";
export const createDefaultProps = (propsDefinition, derivedFromProps) => { export const createDefaultProps = (propsDefinition, derivedFromProps) => {
@ -31,7 +28,7 @@ export const createDefaultProps = (propsDefinition, derivedFromProps) => {
} }
const parsePropDef = propDef => { const parsePropDef = propDef => {
const error = message => ({error:message}); const error = message => ({error:message, propDef});
if(isString(propDef)) { if(isString(propDef)) {
if(!types[propDef]) if(!types[propDef])
@ -56,13 +53,9 @@ const parsePropDef = propDef => {
return propDef.default; return propDef.default;
} }
const propType = (defaultValue, isOfType) => ({ /*
isOfType, default:defaultValue Allowed propDefOptions
}); - type: string, bool, number, array
- default: default value, when undefined
const types = { - required: field is required
string: propType(() => "", isString), */
bool: propType(() => false, isBoolean),
number: propType(() => 0, isNumber),
array: propType(() => [], isArray)
}

View File

@ -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"))
};

View File

@ -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]
}

View File

@ -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");
});
})