recursive validation of props
This commit is contained in:
parent
ca74258c04
commit
0b94346104
|
@ -5,15 +5,24 @@ import {
|
||||||
import { types } from "./types";
|
import { types } from "./types";
|
||||||
import { assign } from "lodash";
|
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 = [];
|
const errors = [];
|
||||||
|
|
||||||
|
if(!componentName)
|
||||||
|
error("_component", "Component name not supplied");
|
||||||
|
|
||||||
for(let propDef in propsDefinition) {
|
for(let propDef in propsDefinition) {
|
||||||
const parsedPropDef = parsePropDef(propsDefinition[propDef]);
|
const parsedPropDef = parsePropDef(propsDefinition[propDef]);
|
||||||
if(parsedPropDef.error)
|
if(parsedPropDef.error)
|
||||||
errors.push({propName:propDef, error:parsedPropDef.error});
|
error(propDef, parsedPropDef.error);
|
||||||
else
|
else
|
||||||
props[propDef] = parsedPropDef;
|
props[propDef] = parsedPropDef;
|
||||||
}
|
}
|
|
@ -2,7 +2,8 @@ import {
|
||||||
isString,
|
isString,
|
||||||
isBoolean,
|
isBoolean,
|
||||||
isNumber,
|
isNumber,
|
||||||
isArray
|
isArray,
|
||||||
|
isObjectLike
|
||||||
} from "lodash/fp";
|
} from "lodash/fp";
|
||||||
|
|
||||||
const defaultDef = typeName => () => ({
|
const defaultDef = typeName => () => ({
|
||||||
|
@ -10,17 +11,20 @@ const defaultDef = typeName => () => ({
|
||||||
required:false,
|
required:false,
|
||||||
default:types[typeName].default,
|
default:types[typeName].default,
|
||||||
options: typeName === "options" ? [] : undefined,
|
options: typeName === "options" ? [] : undefined,
|
||||||
itemPropsDefinition: typeName === "array" ? "string" : undefined
|
elementDefinition: typeName === "array" ? "string" : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const propType = (defaultValue, isOfType, defaultDefinition) => ({
|
const propType = (defaultValue, isOfType, defaultDefinition) => ({
|
||||||
isOfType, default:defaultValue, defaultDefinition
|
isOfType, default:defaultValue, defaultDefinition
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isComponent = isObjectLike;
|
||||||
|
|
||||||
export const types = {
|
export const types = {
|
||||||
string: propType(() => "", isString, defaultDef("string")),
|
string: propType(() => "", isString, defaultDef("string")),
|
||||||
bool: propType(() => false, isBoolean, defaultDef("bool")),
|
bool: propType(() => false, isBoolean, defaultDef("bool")),
|
||||||
number: propType(() => 0, isNumber, defaultDef("number")),
|
number: propType(() => 0, isNumber, defaultDef("number")),
|
||||||
array: propType(() => [], isArray, defaultDef("array")),
|
array: propType(() => [], isArray, defaultDef("array")),
|
||||||
options: propType(() => "", isString, defaultDef("options"))
|
options: propType(() => "", isString, defaultDef("options")),
|
||||||
|
component: propType(() => ({_component:""}), isComponent, defaultDef("component"))
|
||||||
};
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import { types } from "./types";
|
import { types } from "./types";
|
||||||
import { createDefaultProps } from "./createDefaultProps";
|
import { createProps } from "./createProps";
|
||||||
import { isString } from "util";
|
import { isString } from "util";
|
||||||
import {
|
import {
|
||||||
includes,
|
includes,
|
||||||
|
@ -17,11 +17,40 @@ const makeError = (errors, propName) => (message) =>
|
||||||
propName,
|
propName,
|
||||||
error:message});
|
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) => {
|
export const validateProps = (propsDefinition, props, isFinal=true) => {
|
||||||
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
|
if(!props._component)
|
||||||
|
makeError(errors, "_component")("Component is not set");
|
||||||
|
|
||||||
for(let propDefName in propsDefinition) {
|
for(let propDefName in propsDefinition) {
|
||||||
|
|
||||||
|
if(propDefName === "_component") continue;
|
||||||
|
|
||||||
let propDef = propsDefinition[propDefName];
|
let propDef = propsDefinition[propDefName];
|
||||||
|
|
||||||
|
@ -46,7 +75,7 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
|
||||||
if(propDef.type === "array") {
|
if(propDef.type === "array") {
|
||||||
for(let arrayItem of propValue) {
|
for(let arrayItem of propValue) {
|
||||||
const arrayErrs = validateProps(
|
const arrayErrs = validateProps(
|
||||||
propDef.itemPropsDefinition,
|
propDef.elementDefinition,
|
||||||
arrayItem,
|
arrayItem,
|
||||||
isFinal
|
isFinal
|
||||||
)
|
)
|
||||||
|
@ -61,31 +90,32 @@ export const validateProps = (propsDefinition, props, isFinal=true) => {
|
||||||
&& !includes(propValue)(propDef.options)) {
|
&& !includes(propValue)(propDef.options)) {
|
||||||
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`);
|
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validatePropsDefinition = (propsDefinition) => {
|
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, [
|
pipe(propsDefinition, [
|
||||||
keys,
|
keys,
|
||||||
map(k => ({
|
map(k => ({
|
||||||
propDef:propsDefinition[k],
|
propDef:propsDefinition[k],
|
||||||
propName: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`))
|
each(d => makeError(errors, d.propName)(`${d.propName} does not have a definition for it's item props`))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const arrayPropValidationErrors = pipe(propsDefinition, [
|
const arrayPropValidationErrors = pipe(propsDefinition, [
|
||||||
keys,
|
keys,
|
||||||
map(k => propsDefinition[k]),
|
map(k => propsDefinition[k]),
|
||||||
filter(d => d.type === "array" && d.itemPropsDefinition),
|
filter(d => d.type === "array" && d.elementDefinition),
|
||||||
map(d => validatePropsDefinition(d.itemPropsDefinition)),
|
map(d => validatePropsDefinition(d.elementDefinition)),
|
||||||
flatten
|
flatten
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps";
|
import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps";
|
||||||
import {
|
import {
|
||||||
keys, some
|
keys, some
|
||||||
} from "lodash/fp";
|
} from "lodash/fp";
|
||||||
|
@ -10,12 +10,33 @@ describe("createDefaultProps", () => {
|
||||||
fieldName: {type:"string", default:"something"}
|
fieldName: {type:"string", default:"something"}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.fieldName).toBeDefined();
|
||||||
expect(props.fieldName).toBe("something");
|
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", () => {
|
it("should create a object with single blank string value, when no default", () => {
|
||||||
|
@ -23,7 +44,7 @@ describe("createDefaultProps", () => {
|
||||||
fieldName: {type:"string"}
|
fieldName: {type:"string"}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.fieldName).toBeDefined();
|
||||||
|
@ -35,7 +56,7 @@ describe("createDefaultProps", () => {
|
||||||
fieldName: "string"
|
fieldName: "string"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.fieldName).toBeDefined();
|
||||||
|
@ -44,38 +65,50 @@ describe("createDefaultProps", () => {
|
||||||
|
|
||||||
it("should create a object with single fals value, when prop definition is 'bool' ", () => {
|
it("should create a object with single fals value, when prop definition is 'bool' ", () => {
|
||||||
const propDef = {
|
const propDef = {
|
||||||
fieldName: "bool"
|
isVisible: "bool"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.isVisible).toBeDefined();
|
||||||
expect(props.fieldName).toBe(false);
|
expect(props.isVisible).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create a object with single 0 value, when prop definition is 'number' ", () => {
|
it("should create a object with single 0 value, when prop definition is 'number' ", () => {
|
||||||
const propDef = {
|
const propDef = {
|
||||||
fieldName: "number"
|
width: "number"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.width).toBeDefined();
|
||||||
expect(props.fieldName).toBe(0);
|
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 = {
|
const propDef = {
|
||||||
fieldName: "array"
|
columns: "array"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.columns).toBeDefined();
|
||||||
expect(props.fieldName).toEqual([]);
|
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", () => {
|
it("should create an object with multiple prop names", () => {
|
||||||
|
@ -84,14 +117,14 @@ describe("createDefaultProps", () => {
|
||||||
fieldLength: { type: "number", default: 500 }
|
fieldLength: { type: "number", default: 500 }
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef);
|
const { props, errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
expect(props.fieldName).toBeDefined();
|
expect(props.fieldName).toBeDefined();
|
||||||
expect(props.fieldName).toBe("");
|
expect(props.fieldName).toBe("");
|
||||||
expect(props.fieldLength).toBeDefined();
|
expect(props.fieldLength).toBeDefined();
|
||||||
expect(props.fieldLength).toBe(500);
|
expect(props.fieldLength).toBe(500);
|
||||||
expect(keys(props).length).toBe(2);
|
expect(keys(props).length).toBe(3);
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return error when invalid type", () => {
|
it("should return error when invalid type", () => {
|
||||||
|
@ -100,7 +133,7 @@ describe("createDefaultProps", () => {
|
||||||
fieldLength: { type: "invalid type name "}
|
fieldLength: { type: "invalid type name "}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { errors } = createDefaultProps(propDef);
|
const { errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors.length).toBe(2);
|
expect(errors.length).toBe(2);
|
||||||
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
|
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
|
||||||
|
@ -112,7 +145,7 @@ describe("createDefaultProps", () => {
|
||||||
fieldName: {type:"string", default: 1}
|
fieldName: {type:"string", default: 1}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { errors } = createDefaultProps(propDef);
|
const { errors } = createProps("some_component",propDef);
|
||||||
|
|
||||||
expect(errors.length).toBe(1);
|
expect(errors.length).toBe(1);
|
||||||
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
|
expect(some(e => e.propName === "fieldName")(errors)).toBeTruthy();
|
||||||
|
@ -125,10 +158,11 @@ describe("createDefaultProps", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const derivedFrom = {
|
const derivedFrom = {
|
||||||
|
_component:"root",
|
||||||
fieldName: "surname"
|
fieldName: "surname"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef, [derivedFrom]);
|
const { props, errors } = createProps("some_component",propDef, [derivedFrom]);
|
||||||
|
|
||||||
expect(errors.length).toBe(0);
|
expect(errors.length).toBe(0);
|
||||||
expect(props.fieldName).toBe("surname");
|
expect(props.fieldName).toBe("surname");
|
||||||
|
@ -139,23 +173,31 @@ describe("createDefaultProps", () => {
|
||||||
it("should merge in derived props, last in list taking priority", () => {
|
it("should merge in derived props, last in list taking priority", () => {
|
||||||
const propDef = {
|
const propDef = {
|
||||||
fieldName: "string",
|
fieldName: "string",
|
||||||
fieldLength: { type: "number", default: 500}
|
fieldLength: { type: "number", default: 500},
|
||||||
|
header: "component",
|
||||||
|
content: {
|
||||||
|
type: "component",
|
||||||
|
default: { _component: "childcomponent", wdith: 500 }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const derivedFrom1 = {
|
const derivedFrom1 = {
|
||||||
|
_component:"root",
|
||||||
fieldName: "surname",
|
fieldName: "surname",
|
||||||
fieldLength: 200
|
fieldLength: 200
|
||||||
};
|
};
|
||||||
|
|
||||||
const derivedFrom2 = {
|
const derivedFrom2 = {
|
||||||
|
_component:"child",
|
||||||
fieldName: "forename"
|
fieldName: "forename"
|
||||||
};
|
};
|
||||||
|
|
||||||
const { props, errors } = createDefaultProps(propDef, [derivedFrom1, derivedFrom2]);
|
const { props, errors } = createProps("some_component",propDef, [derivedFrom1, derivedFrom2]);
|
||||||
|
|
||||||
expect(errors.length).toBe(0);
|
expect(errors.length).toBe(0);
|
||||||
expect(props.fieldName).toBe("forename");
|
expect(props.fieldName).toBe("forename");
|
||||||
expect(props.fieldLength).toBe(200);
|
expect(props.fieldLength).toBe(200);
|
||||||
|
expect(props._component).toBe("child");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,7 @@ import {
|
||||||
validatePropsDefinition,
|
validatePropsDefinition,
|
||||||
validateProps
|
validateProps
|
||||||
} from "../src/userInterface/propsDefinitionParsing/validateProps";
|
} from "../src/userInterface/propsDefinitionParsing/validateProps";
|
||||||
import { createDefaultProps } from "../src/userInterface/propsDefinitionParsing/createDefaultProps";
|
import { createProps } from "../src/userInterface/propsDefinitionParsing/createProps";
|
||||||
import {
|
|
||||||
keys, some
|
|
||||||
} from "lodash/fp";
|
|
||||||
|
|
||||||
// not that allot of this functionality is covered
|
// not that allot of this functionality is covered
|
||||||
// in createDefaultProps - as validate props uses that.
|
// in createDefaultProps - as validate props uses that.
|
||||||
|
@ -17,7 +14,7 @@ describe("validatePropsDefinition", () => {
|
||||||
const propsDef = {
|
const propsDef = {
|
||||||
columns : {
|
columns : {
|
||||||
type: "array",
|
type: "array",
|
||||||
itemPropsDefinition: {
|
elementDefinition: {
|
||||||
width: "number",
|
width: "number",
|
||||||
units: {
|
units: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -38,7 +35,7 @@ describe("validatePropsDefinition", () => {
|
||||||
const propsDef = {
|
const propsDef = {
|
||||||
columns : {
|
columns : {
|
||||||
type: "array",
|
type: "array",
|
||||||
itemPropsDefinition: {
|
elementDefinition: {
|
||||||
width: "invlid type",
|
width: "invlid type",
|
||||||
units: {
|
units: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -97,7 +94,7 @@ const validPropDef = {
|
||||||
rowCount : "number",
|
rowCount : "number",
|
||||||
columns : {
|
columns : {
|
||||||
type: "array",
|
type: "array",
|
||||||
itemPropsDefinition: {
|
elementDefinition: {
|
||||||
width: "number",
|
width: "number",
|
||||||
units: {
|
units: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -109,9 +106,9 @@ const validPropDef = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const validProps = () => {
|
const validProps = () => {
|
||||||
const { props } = createDefaultProps(validPropDef);
|
const { props } = createProps("some_component", validPropDef);
|
||||||
props.columns.push(
|
props.columns.push(
|
||||||
createDefaultProps(validPropDef.columns.itemPropsDefinition).props);
|
createProps("childcomponent", validPropDef.columns.elementDefinition).props);
|
||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue