validating component props
This commit is contained in:
parent
a81af3f18b
commit
ca74258c04
|
@ -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)
|
|
||||||
}
|
|
|
@ -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"))
|
||||||
|
};
|
|
@ -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]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
Loading…
Reference in New Issue