mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: rewrite props validation code
This commit reworks the props validation code in order to extract a generic validate utility function. The validation should be more robust, with better error messages, and at the same time, it supports `*` in a shape object. And as a bonus, it is now typesafe, and the static props object is now typed. closes #1190
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { BDom, multi, text, toggler, createCatcher } from "../blockdom";
|
import { BDom, multi, text, toggler, createCatcher } from "../blockdom";
|
||||||
import { validateProps } from "../component/props_validation";
|
|
||||||
import { Markup } from "../utils";
|
import { Markup } from "../utils";
|
||||||
import { html } from "../blockdom/index";
|
import { html } from "../blockdom/index";
|
||||||
|
import { isOptional, validateSchema } from "../validation";
|
||||||
|
import { ComponentConstructor } from "../component/component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains utility functions that will be injected in each template,
|
* This file contains utility functions that will be injected in each template,
|
||||||
@@ -182,6 +183,49 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the component props (or next props) against the (static) props
|
||||||
|
* description. This is potentially an expensive operation: it may needs to
|
||||||
|
* visit recursively the props and all the children to check if they are valid.
|
||||||
|
* This is why it is only done in 'dev' mode.
|
||||||
|
*/
|
||||||
|
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, parent?: any) {
|
||||||
|
const ComponentClass =
|
||||||
|
typeof name !== "string"
|
||||||
|
? name
|
||||||
|
: (parent.constructor.components[name] as ComponentConstructor<P> | undefined);
|
||||||
|
|
||||||
|
if (!ComponentClass) {
|
||||||
|
// this is an error, wrong component. We silently return here instead so the
|
||||||
|
// error is triggered by the usual path ('component' function)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = ComponentClass.props;
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const defaultProps = ComponentClass.defaultProps;
|
||||||
|
if (defaultProps) {
|
||||||
|
let isMandatory = (name: string) =>
|
||||||
|
Array.isArray(schema)
|
||||||
|
? schema.includes(name)
|
||||||
|
: name in schema && !("*" in schema) && !isOptional(schema[name]);
|
||||||
|
for (let p in defaultProps) {
|
||||||
|
if (isMandatory(p)) {
|
||||||
|
throw new Error(
|
||||||
|
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = validateSchema(props, schema);
|
||||||
|
if (errors.length) {
|
||||||
|
throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const helpers = {
|
export const helpers = {
|
||||||
withDefault,
|
withDefault,
|
||||||
zero: Symbol("zero"),
|
zero: Symbol("zero"),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Schema } from "../validation";
|
||||||
import type { ComponentNode } from "./component_node";
|
import type { ComponentNode } from "./component_node";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -9,7 +10,7 @@ type Props = { [key: string]: any };
|
|||||||
interface StaticComponentProperties {
|
interface StaticComponentProperties {
|
||||||
template: string;
|
template: string;
|
||||||
defaultProps?: any;
|
defaultProps?: any;
|
||||||
props?: any;
|
props?: Schema;
|
||||||
components?: { [componentName: string]: ComponentConstructor };
|
components?: { [componentName: string]: ComponentConstructor };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { batched, Callback } from "../utils";
|
|||||||
import { Component, ComponentConstructor } from "./component";
|
import { Component, ComponentConstructor } from "./component";
|
||||||
import { fibersInError, handleError } from "./error_handling";
|
import { fibersInError, handleError } from "./error_handling";
|
||||||
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||||
import { applyDefaultProps } from "./props_validation";
|
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
let currentNode: ComponentNode | null = null;
|
||||||
@@ -29,6 +28,18 @@ export function useComponent(): Component {
|
|||||||
return currentNode!.component;
|
return currentNode!.component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply default props (only top level).
|
||||||
|
*
|
||||||
|
* Note that this method does modify in place the props
|
||||||
|
*/
|
||||||
|
function applyDefaultProps<P>(props: P, defaultProps: Partial<P>) {
|
||||||
|
for (let propName in defaultProps) {
|
||||||
|
if ((props as any)[propName] === undefined) {
|
||||||
|
(props as any)[propName] = defaultProps[propName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Integration with reactivity system (useState)
|
// Integration with reactivity system (useState)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -165,7 +176,10 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.parentKey = parentKey;
|
this.parentKey = parentKey;
|
||||||
this.level = parent ? parent.level + 1 : 0;
|
this.level = parent ? parent.level + 1 : 0;
|
||||||
applyDefaultProps(props, C);
|
const defaultProps = C.defaultProps;
|
||||||
|
if (defaultProps) {
|
||||||
|
applyDefaultProps(props, defaultProps);
|
||||||
|
}
|
||||||
const env = (parent && parent.childEnv) || app.env;
|
const env = (parent && parent.childEnv) || app.env;
|
||||||
this.childEnv = env;
|
this.childEnv = env;
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
@@ -284,7 +298,10 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
const fiber = makeChildFiber(this, parentFiber);
|
const fiber = makeChildFiber(this, parentFiber);
|
||||||
this.fiber = fiber;
|
this.fiber = fiber;
|
||||||
const component = this.component;
|
const component = this.component;
|
||||||
applyDefaultProps(props, component.constructor as any);
|
const defaultProps = (component.constructor as any).defaultProps;
|
||||||
|
if (defaultProps) {
|
||||||
|
applyDefaultProps(props, defaultProps);
|
||||||
|
}
|
||||||
|
|
||||||
currentNode = this;
|
currentNode = this;
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
import { ComponentConstructor } from "./component";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply default props (only top level).
|
|
||||||
*
|
|
||||||
* Note that this method does modify in place the props
|
|
||||||
*/
|
|
||||||
export function applyDefaultProps<P>(props: P, ComponentClass: ComponentConstructor<P>) {
|
|
||||||
const defaultProps = ComponentClass.defaultProps;
|
|
||||||
if (defaultProps) {
|
|
||||||
for (let propName in defaultProps) {
|
|
||||||
if ((props as any)[propName] === undefined) {
|
|
||||||
(props as any)[propName] = defaultProps[propName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Prop validation helper
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
function getPropDescription(staticProps: any) {
|
|
||||||
if (staticProps instanceof Array) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return staticProps || { "*": true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the component props (or next props) against the (static) props
|
|
||||||
* description. This is potentially an expensive operation: it may needs to
|
|
||||||
* visit recursively the props and all the children to check if they are valid.
|
|
||||||
* This is why it is only done in 'dev' mode.
|
|
||||||
*/
|
|
||||||
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, parent?: any) {
|
|
||||||
const ComponentClass =
|
|
||||||
typeof name !== "string"
|
|
||||||
? name
|
|
||||||
: (parent.constructor.components[name] as ComponentConstructor<P> | undefined);
|
|
||||||
|
|
||||||
if (!ComponentClass) {
|
|
||||||
// this is an error, wrong component. We silently return here instead so the
|
|
||||||
// error is triggered by the usual path ('component' function)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyDefaultProps(props, ComponentClass);
|
|
||||||
|
|
||||||
const defaultProps = ComponentClass.defaultProps || {};
|
|
||||||
let propsDef = getPropDescription(ComponentClass.props);
|
|
||||||
const allowAdditionalProps = "*" in propsDef;
|
|
||||||
|
|
||||||
for (let propName in propsDef) {
|
|
||||||
if (propName === "*") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const propDef = propsDef[propName];
|
|
||||||
let isMandatory = !!propDef;
|
|
||||||
if (typeof propDef === "object" && "optional" in propDef) {
|
|
||||||
isMandatory = !propDef.optional;
|
|
||||||
}
|
|
||||||
if (isMandatory && propName in defaultProps) {
|
|
||||||
throw new Error(
|
|
||||||
`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ((props as any)[propName] === undefined) {
|
|
||||||
if (isMandatory) {
|
|
||||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let whyInvalid;
|
|
||||||
try {
|
|
||||||
whyInvalid = whyInvalidProp((props as any)[propName], propDef);
|
|
||||||
} catch (e) {
|
|
||||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
|
||||||
(e as Error).message
|
|
||||||
})`;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (whyInvalid !== null) {
|
|
||||||
whyInvalid = whyInvalid.replace(/\${propName}/g, propName);
|
|
||||||
throw new Error(
|
|
||||||
`Invalid Prop '${propName}' in component '${ComponentClass.name}': ${whyInvalid}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!allowAdditionalProps) {
|
|
||||||
for (let propName in props) {
|
|
||||||
if (!(propName in propsDef)) {
|
|
||||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check why an invidual prop value doesn't match its (static) prop definition
|
|
||||||
*/
|
|
||||||
function whyInvalidProp(prop: any, propDef: any): string | null {
|
|
||||||
if (propDef === true) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof propDef === "function") {
|
|
||||||
// Check if a value is constructed by some Constructor. Note that there is a
|
|
||||||
// slight abuse of language: we want to consider primitive values as well.
|
|
||||||
//
|
|
||||||
// So, even though 1 is not an instance of Number, we want to consider that
|
|
||||||
// it is valid.
|
|
||||||
if (typeof prop === "object") {
|
|
||||||
if (prop instanceof propDef) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `\${propName} is not an instance of ${propDef.name}`;
|
|
||||||
}
|
|
||||||
if (typeof prop === propDef.name.toLowerCase()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `type of \${propName} is not ${propDef.name}`;
|
|
||||||
} else if (propDef instanceof Array) {
|
|
||||||
// If this code is executed, this means that we want to check if a prop
|
|
||||||
// matches at least one of its descriptor.
|
|
||||||
let reasons: string[] = [];
|
|
||||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
|
||||||
const why = whyInvalidProp(prop, propDef[i]);
|
|
||||||
if (why === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
reasons.push(why);
|
|
||||||
}
|
|
||||||
if (reasons.length > 1) {
|
|
||||||
return reasons.slice(0, -1).join(", ") + " and " + reasons[reasons.length - 1];
|
|
||||||
} else {
|
|
||||||
return reasons[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// propsDef is an object
|
|
||||||
if (propDef.optional && prop === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (propDef.type) {
|
|
||||||
const why = whyInvalidProp(prop, propDef.type);
|
|
||||||
if (why !== null) {
|
|
||||||
return why;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (propDef.validate && !propDef.validate(prop)) {
|
|
||||||
return "${propName} could not be validated by `validate` function";
|
|
||||||
}
|
|
||||||
if (propDef.type === Array && propDef.element) {
|
|
||||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
|
||||||
const why = whyInvalidProp(prop[i], propDef.element);
|
|
||||||
if (why !== null) {
|
|
||||||
return why.replace(/\${propName}/g, `\${propName}[${i}]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (propDef.type === Object && propDef.shape) {
|
|
||||||
const shape = propDef.shape;
|
|
||||||
for (let key in shape) {
|
|
||||||
const why = whyInvalidProp(prop[key], shape[key]);
|
|
||||||
if (why !== null) {
|
|
||||||
return why.replace(/\${propName}/g, `\${propName}['${key}']`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let propName in prop) {
|
|
||||||
if (!(propName in shape)) {
|
|
||||||
return `unknown prop \${propName}['${propName}']`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
type BaseType =
|
||||||
|
| typeof String
|
||||||
|
| typeof Boolean
|
||||||
|
| typeof Number
|
||||||
|
| typeof Date
|
||||||
|
| typeof Object
|
||||||
|
| typeof Array
|
||||||
|
| true
|
||||||
|
| "*";
|
||||||
|
|
||||||
|
type UnionType = (TypeInfo | BaseType)[];
|
||||||
|
type TypeDescription = BaseType | UnionType | TypeInfo;
|
||||||
|
|
||||||
|
interface TypeInfo {
|
||||||
|
type?: TypeDescription;
|
||||||
|
optional?: boolean;
|
||||||
|
validate?: Function;
|
||||||
|
shape?: Schema;
|
||||||
|
element?: TypeDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimplifiedSchema = string[];
|
||||||
|
type NormalizedSchema = { [key: string]: TypeDescription };
|
||||||
|
export type Schema = SimplifiedSchema | NormalizedSchema;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const isUnionType = (t: TypeDescription): t is UnionType => Array.isArray(t);
|
||||||
|
const isBaseType = (t: TypeDescription): t is BaseType => typeof t !== "object";
|
||||||
|
|
||||||
|
export function isOptional(t: TypeDescription): Boolean {
|
||||||
|
return typeof t === "object" && "optional" in t ? t.optional || false : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeType(type: BaseType): string {
|
||||||
|
return type === "*" || type === true ? "value" : type.name.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function describe(info: TypeDescription): string {
|
||||||
|
if (isBaseType(info)) {
|
||||||
|
return describeType(info);
|
||||||
|
} else if (isUnionType(info)) {
|
||||||
|
return info.map(describe).join(" or ");
|
||||||
|
}
|
||||||
|
if ("element" in info) {
|
||||||
|
return `list of ${describe({ type: info.element, optional: false })}s`;
|
||||||
|
}
|
||||||
|
if ("shape" in (info as TypeInfo)) {
|
||||||
|
return `object`;
|
||||||
|
}
|
||||||
|
return describe(info.type || "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSchema(spec: SimplifiedSchema): NormalizedSchema {
|
||||||
|
return Object.fromEntries(
|
||||||
|
spec.map((e) =>
|
||||||
|
e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main validate function
|
||||||
|
*/
|
||||||
|
export function validate(obj: { [key: string]: any }, spec: Schema) {
|
||||||
|
let errors = validateSchema(obj, spec);
|
||||||
|
if (errors.length) {
|
||||||
|
throw new Error("Invalid object: " + errors.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper validate function, to get the list of errors. useful if one want to
|
||||||
|
* manipulate the errors without parsing an error object
|
||||||
|
*/
|
||||||
|
export function validateSchema(obj: { [key: string]: any }, schema: Schema): string[] {
|
||||||
|
if (Array.isArray(schema)) {
|
||||||
|
schema = toSchema(schema);
|
||||||
|
}
|
||||||
|
let errors = [];
|
||||||
|
// check if each value in obj has correct shape
|
||||||
|
for (let key in obj) {
|
||||||
|
if (key in schema) {
|
||||||
|
let result = validateType(key, obj[key], schema[key]);
|
||||||
|
if (result) {
|
||||||
|
errors.push(result);
|
||||||
|
}
|
||||||
|
} else if (!("*" in schema)) {
|
||||||
|
errors.push(`unknown key '${key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check that all specified keys are defined in obj
|
||||||
|
for (let key in schema) {
|
||||||
|
const spec = schema[key];
|
||||||
|
if (key !== "*" && !isOptional(spec) && !(key in obj)) {
|
||||||
|
const isObj = typeof spec === "object" && !Array.isArray(spec);
|
||||||
|
const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
|
||||||
|
let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
|
||||||
|
errors.push(`'${key}' is missing${detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBaseType(key: string, value: any, type: BaseType): string | null {
|
||||||
|
if (typeof type === "function") {
|
||||||
|
if (typeof value === "object") {
|
||||||
|
if (!(value instanceof type)) {
|
||||||
|
return `'${key}' is not a ${describeType(type)}`;
|
||||||
|
}
|
||||||
|
} else if (typeof value !== type.name.toLowerCase()) {
|
||||||
|
return `'${key}' is not a ${describeType(type)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateArrayType(key: string, value: any, descr: TypeDescription): string | null {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return `'${key}' is not a list of ${describe(descr)}s`;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const error = validateType(`${key}[${i}]`, value[i], descr);
|
||||||
|
if (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateType(key: string, value: any, descr: TypeDescription): string | null {
|
||||||
|
if (value === undefined) {
|
||||||
|
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
|
||||||
|
} else if (isBaseType(descr)) {
|
||||||
|
return validateBaseType(key, value, descr);
|
||||||
|
} else if (isUnionType(descr)) {
|
||||||
|
let validDescr = descr.find((p) => !validateType(key, value, p));
|
||||||
|
return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
|
||||||
|
}
|
||||||
|
let result: string | null = null;
|
||||||
|
if ("element" in descr) {
|
||||||
|
result = validateArrayType(key, value, descr.element!);
|
||||||
|
} else if ("shape" in descr && !result) {
|
||||||
|
if (typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
result = `'${key}' is not an object`;
|
||||||
|
} else {
|
||||||
|
const errors = validateSchema(value, descr.shape!);
|
||||||
|
if (errors.length) {
|
||||||
|
result = `'${key}' has not the correct shape (${errors.join(", ")})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("type" in descr && !result) {
|
||||||
|
result = validateType(key, value, descr.type!);
|
||||||
|
}
|
||||||
|
if ("validate" in descr && !result) {
|
||||||
|
result = !descr.validate!(value) ? `'${key}' is not valid` : null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
||||||
import { Component, onError, xml, mount } from "../../src";
|
import { Component, onError, xml, mount } from "../../src";
|
||||||
import { DEV_MSG } from "../../src/app/app";
|
import { DEV_MSG } from "../../src/app/app";
|
||||||
import { validateProps } from "../../src/component/props_validation";
|
import { validateProps } from "../../src/app/template_helpers";
|
||||||
|
import { Schema } from "../../src/validation";
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
|
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
|
||||||
error = undefined;
|
error = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +84,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(`Missing props 'message' (component 'SubComp')`);
|
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validate simple types", async () => {
|
test("validate simple types", async () => {
|
||||||
@@ -118,7 +119,9 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
|
expect(error!.message).toBe(
|
||||||
|
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
|
||||||
|
);
|
||||||
error = undefined;
|
error = undefined;
|
||||||
props = { p: test.ok };
|
props = { p: test.ok };
|
||||||
try {
|
try {
|
||||||
@@ -135,7 +138,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
`Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}`
|
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -170,7 +173,9 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(`Missing props 'p' (component '_a')`);
|
expect(error!.message).toBe(
|
||||||
|
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
|
||||||
|
);
|
||||||
error = undefined;
|
error = undefined;
|
||||||
props = { p: test.ok };
|
props = { p: test.ok };
|
||||||
try {
|
try {
|
||||||
@@ -187,7 +192,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
`Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}`
|
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -228,7 +233,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'p' in component 'SubComp': type of p is not String and type of p is not Boolean"
|
"Invalid props for component 'SubComp': 'p' is not a string or boolean"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,7 +272,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp': type of p is not String");
|
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is not a string");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate an array with given primitive type", async () => {
|
test("can validate an array with given primitive type", async () => {
|
||||||
@@ -357,7 +362,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'p' in component 'SubComp': type of p[1] is not String and type of p[1] is not Boolean"
|
"Invalid props for component 'SubComp': 'p[1]' is not a string or boolean"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -391,7 +396,9 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp': unknown prop p['extra']");
|
expect(error!.message).toBe(
|
||||||
|
"Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')"
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
props = { p: { id: "1", url: "url" } };
|
props = { p: { id: "1", url: "url" } };
|
||||||
await mount(Parent, fixture, { dev: true });
|
await mount(Parent, fixture, { dev: true });
|
||||||
@@ -400,7 +407,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'p' in component 'SubComp': type of p['id'] is not Number"
|
"Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)"
|
||||||
);
|
);
|
||||||
error = undefined;
|
error = undefined;
|
||||||
try {
|
try {
|
||||||
@@ -411,7 +418,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'p' in component 'SubComp': type of p['url'] is not String"
|
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -459,7 +466,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'p' in component 'SubComp': p['url'] is not an instance of Boolean and type of p['url'][1] is not Number"
|
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -491,7 +498,7 @@ describe("props validation", () => {
|
|||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe(
|
||||||
"Invalid Prop 'myprop' in component 'TestComponent': unknown prop myprop[0]['a']"
|
"Invalid props for component 'TestComponent': 'myprop[0]' has not the correct shape (unknown key 'a')"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -516,9 +523,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe("Invalid props for component 'TestComponent': 'size' is not valid");
|
||||||
"Invalid Prop 'size' in component 'TestComponent': size could not be validated by `validate` function"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can validate with a custom validator, and a type", () => {
|
test("can validate with a custom validator, and a type", () => {
|
||||||
@@ -545,9 +550,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe("Invalid props for component 'TestComponent': 'n' is not a number");
|
||||||
"Invalid Prop 'n' in component 'TestComponent': type of n is not Number"
|
|
||||||
);
|
|
||||||
expect(validator).toBeCalledTimes(1);
|
expect(validator).toBeCalledTimes(1);
|
||||||
error = undefined;
|
error = undefined;
|
||||||
try {
|
try {
|
||||||
@@ -556,9 +559,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe(
|
expect(error!.message).toBe("Invalid props for component 'TestComponent': 'n' is not valid");
|
||||||
"Invalid Prop 'n' in component 'TestComponent': n could not be validated by `validate` function"
|
|
||||||
);
|
|
||||||
expect(validator).toBeCalledTimes(2);
|
expect(validator).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,7 +606,7 @@ describe("props validation", () => {
|
|||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateProps(SubComp as any, { myprop: 1 });
|
validateProps(SubComp as any, { myprop: 1 });
|
||||||
}).toThrow(`Invalid Prop 'myprop' in component 'SubComp'`);
|
}).toThrow("Invalid props for component 'SubComp': 'myprop' is not a array");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props with type object, and no shape", async () => {
|
test("props with type object, and no shape", async () => {
|
||||||
@@ -617,7 +618,7 @@ describe("props validation", () => {
|
|||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateProps(SubComp as any, { myprop: false });
|
validateProps(SubComp as any, { myprop: false });
|
||||||
}).toThrow(`Invalid Prop 'myprop' in component 'SubComp'`);
|
}).toThrow("Invalid props for component 'SubComp': 'myprop' is not a object");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props: extra props cause an error", async () => {
|
test("props: extra props cause an error", async () => {
|
||||||
@@ -675,7 +676,7 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
|
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props are validated whenever component is updated", async () => {
|
test("props are validated whenever component is updated", async () => {
|
||||||
@@ -698,7 +699,9 @@ describe("props validation", () => {
|
|||||||
parent.render();
|
parent.render();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Missing props 'p' (component 'SubComp')");
|
expect(error!.message).toBe(
|
||||||
|
"Invalid props for component 'SubComp': 'p' is undefined (should be a number)"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("default values are applied before validating props at update", async () => {
|
test("default values are applied before validating props at update", async () => {
|
||||||
@@ -741,7 +744,9 @@ describe("props validation", () => {
|
|||||||
error = e as Error;
|
error = e as Error;
|
||||||
}
|
}
|
||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Missing props 'mandatory' (component 'Child')");
|
expect(error!.message).toBe(
|
||||||
|
"Invalid props for component 'Child': 'mandatory' is missing (should be a number)"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can specify that additional props are allowed (array)", async () => {
|
test("can specify that additional props are allowed (array)", async () => {
|
||||||
@@ -759,7 +764,7 @@ describe("props validation", () => {
|
|||||||
|
|
||||||
test("can specify that additional props are allowed (object)", async () => {
|
test("can specify that additional props are allowed (object)", async () => {
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static props = {
|
static props: Schema = {
|
||||||
message: { type: String },
|
message: { type: String },
|
||||||
"*": true,
|
"*": true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { Schema, validateSchema } from "../src/validation";
|
||||||
|
|
||||||
|
describe("validateSchema", () => {
|
||||||
|
test("simple use", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: String })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 1 }, { a: Boolean })).toEqual(["'a' is not a boolean"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple use, alternate form", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: { type: String } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 1 }, { a: { type: Boolean } })).toEqual(["'a' is not a boolean"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("some particular edgecases as key name", () => {
|
||||||
|
expect(validateSchema({ shape: "hey" }, { shape: String })).toEqual([]);
|
||||||
|
expect(validateSchema({ shape: 1 }, { shape: Boolean })).toEqual(["'shape' is not a boolean"]);
|
||||||
|
expect(validateSchema({ element: "hey" }, { element: String })).toEqual([]);
|
||||||
|
expect(validateSchema({ element: 1 }, { element: Boolean })).toEqual([
|
||||||
|
"'element' is not a boolean",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multiple errors", () => {
|
||||||
|
expect(validateSchema({ a: 1, b: 2 }, { a: Boolean, b: Boolean })).toEqual([
|
||||||
|
"'a' is not a boolean",
|
||||||
|
"'b' is not a boolean",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing key", () => {
|
||||||
|
expect(validateSchema({}, { a: Boolean })).toEqual(["'a' is missing (should be a boolean)"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("additional key", () => {
|
||||||
|
expect(validateSchema({ b: 1 }, {})).toEqual(["unknown key 'b'"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("undefined key", () => {
|
||||||
|
expect(validateSchema({ a: undefined }, { a: Boolean })).toEqual([
|
||||||
|
"'a' is undefined (should be a boolean)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({}, { a: Boolean })).toEqual(["'a' is missing (should be a boolean)"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use '*' to denote any type", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: "*" })).toEqual([]);
|
||||||
|
expect(validateSchema({}, { a: "*" })).toEqual(["'a' is missing"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an union of type", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: [String, Boolean] })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 1 }, { a: [String, Boolean] })).toEqual([
|
||||||
|
"'a' is not a string or boolean",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: { type: [String, Boolean] } })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("another union of types", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
id: Number,
|
||||||
|
url: [Boolean, { type: Array, element: Number }],
|
||||||
|
};
|
||||||
|
expect(validateSchema({ a: "hey" }, schema)).toEqual([
|
||||||
|
"unknown key 'a'",
|
||||||
|
"'id' is missing (should be a number)",
|
||||||
|
"'url' is missing (should be a boolean or list of numbers)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ id: 1 }, schema)).toEqual([
|
||||||
|
"'url' is missing (should be a boolean or list of numbers)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ id: 1, url: true }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ id: true, url: true }, schema)).toEqual(["'id' is not a number"]);
|
||||||
|
expect(validateSchema({ id: 3, url: 3 }, schema)).toEqual([
|
||||||
|
"'url' is not a boolean or list of numbers",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simplified schema description", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, ["a"])).toEqual([]);
|
||||||
|
expect(validateSchema({ b: 1 }, ["a"])).toEqual(["unknown key 'b'", "'a' is missing"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simplified schema description with optional props and *", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, ["a", "b?", "*"])).toEqual([]);
|
||||||
|
expect(validateSchema({ a: "hey" }, ["a", "*"])).toEqual([]);
|
||||||
|
expect(validateSchema({ a: "hey", b: 1, c: 3 }, ["a", "*"])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simplified schema description with optional props", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, ["a", "b?"])).toEqual([]);
|
||||||
|
expect(validateSchema({ a: "hey", b: 1 }, ["a", "b?"])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("object type description, with no type/optional key", () => {
|
||||||
|
expect(validateSchema({ a: "hey" }, { a: {} })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 1 }, { a: {} })).toEqual([]);
|
||||||
|
expect(validateSchema({}, { a: {} })).toEqual(["'a' is missing"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("optional key", () => {
|
||||||
|
expect(validateSchema({}, { a: { optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({}, { a: { type: Number, optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: undefined }, { a: { type: Number, optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 2 }, { a: { optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: undefined }, { a: { optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 2 }, { a: { type: Number, optional: true } })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 2 }, { a: { type: String, optional: true } })).toEqual([
|
||||||
|
"'a' is not a string",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can validate dates", () => {
|
||||||
|
expect(validateSchema({ a: new Date() }, { a: Date })).toEqual([]);
|
||||||
|
expect(validateSchema({ a: 4 }, { a: Date })).toEqual(["'a' is not a date"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrays with simple element description", () => {
|
||||||
|
const schema: Schema = { p: { type: Array, element: String } };
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: 1 }, schema)).toEqual(["'p' is not a list of strings"]);
|
||||||
|
expect(validateSchema({}, schema)).toEqual(["'p' is missing (should be a list of strings)"]);
|
||||||
|
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||||
|
"'p' is undefined (should be a list of strings)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: ["a"] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: [1] }, schema)).toEqual(["'p[0]' is not a string"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrays with union type as element description", () => {
|
||||||
|
const schema: Schema = { p: { type: Array, element: [String, Boolean] } };
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: 1 }, schema)).toEqual(["'p' is not a list of string or booleans"]);
|
||||||
|
expect(validateSchema({}, schema)).toEqual([
|
||||||
|
"'p' is missing (should be a list of string or booleans)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||||
|
"'p' is undefined (should be a list of string or booleans)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: ["a"] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: [1] }, schema)).toEqual(["'p[0]' is not a string or boolean"]);
|
||||||
|
expect(validateSchema({ p: [true, 1] }, schema)).toEqual(["'p[1]' is not a string or boolean"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("objects with specified shape", () => {
|
||||||
|
const schema: Schema = { p: { type: Object, shape: { id: Number, url: String } } };
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||||
|
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('id' is missing (should be a number), 'url' is missing (should be a string))",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: { id: 1, url: 1 } }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('url' is not a string)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||||
|
"'p' is undefined (should be a object)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("objects with more complex shape", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
p: {
|
||||||
|
type: Object,
|
||||||
|
shape: {
|
||||||
|
id: Number,
|
||||||
|
url: [Boolean, { type: Array, element: Number }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||||
|
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('id' is missing (should be a number), 'url' is missing (should be a boolean or list of numbers))",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('url' is not a boolean or list of numbers)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: { id: 1, url: true } }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||||
|
"'p' is undefined (should be a object)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("objects with shape and *", () => {
|
||||||
|
const schema: Schema = { p: { type: Object, shape: { id: Number, "*": true } } };
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||||
|
expect(validateSchema({ p: {} }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('id' is missing (should be a number))",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: { id: 1 } }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: { id: "asdf" } }, schema)).toEqual([
|
||||||
|
"'p' has not the correct shape ('id' is not a number)",
|
||||||
|
]);
|
||||||
|
expect(validateSchema({ p: { id: 1, url: 1 } }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||||
|
"'p' is undefined (should be a object)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can specify that additional keys are allowed", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
message: String,
|
||||||
|
"*": true,
|
||||||
|
};
|
||||||
|
expect(validateSchema({ message: "hey" }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ message: "hey", otherKey: true }, schema)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("array with element with shape", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
p: {
|
||||||
|
type: Array,
|
||||||
|
element: {
|
||||||
|
type: Object,
|
||||||
|
shape: {
|
||||||
|
num: { type: Number, optional: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateSchema({ p: 1 }, schema)).toEqual(["'p' is not a list of objects"]);
|
||||||
|
expect(validateSchema({ p: {} }, schema)).toEqual(["'p' is not a list of objects"]);
|
||||||
|
expect(validateSchema({ p: [] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: [{}] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: [{ num: 1 }] }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ p: [{ num: true }] }, schema)).toEqual([
|
||||||
|
"'p[0]' has not the correct shape ('num' is not a number)",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schema with custom validate function", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
size: {
|
||||||
|
validate: (e: string) => ["small", "medium", "large"].includes(e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateSchema({ size: "small" }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ size: "sall" }, schema)).toEqual(["'size' is not valid"]);
|
||||||
|
expect(validateSchema({ size: 1 }, schema)).toEqual(["'size' is not valid"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schema with custom validate function and type", () => {
|
||||||
|
const schema: Schema = {
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
validate: (e: string) => ["small", "medium", "large"].includes(e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(validateSchema({ size: "small" }, schema)).toEqual([]);
|
||||||
|
expect(validateSchema({ size: "sall" }, schema)).toEqual(["'size' is not valid"]);
|
||||||
|
expect(validateSchema({ size: 1 }, schema)).toEqual(["'size' is not a string"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user