mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
9c4c3e3b83
This commit add supports for value types in prop validation. To describe
a value V type, one has to simply write {value: V}.
Note that this commit also improves the validation typing.
closes #1198
closes #910
168 lines
5.2 KiB
TypeScript
168 lines
5.2 KiB
TypeScript
type BaseType =
|
|
| typeof String
|
|
| typeof Boolean
|
|
| typeof Number
|
|
| typeof Date
|
|
| typeof Object
|
|
| typeof Array
|
|
| true
|
|
| "*";
|
|
|
|
interface TypeInfo {
|
|
type?: TypeDescription;
|
|
optional?: boolean;
|
|
validate?: Function;
|
|
shape?: Schema;
|
|
element?: TypeDescription;
|
|
}
|
|
|
|
type ValueType = { value: any };
|
|
|
|
type TypeDescription = BaseType | TypeInfo | ValueType | TypeDescription[];
|
|
type SimplifiedSchema = string[];
|
|
type NormalizedSchema = { [key: string]: TypeDescription };
|
|
export type Schema = SimplifiedSchema | NormalizedSchema;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// helpers
|
|
// -----------------------------------------------------------------------------
|
|
const isUnionType = (t: TypeDescription): t is TypeDescription[] => Array.isArray(t);
|
|
const isBaseType = (t: TypeDescription): t is BaseType => typeof t !== "object";
|
|
const isValueType = (t: TypeDescription): t is ValueType =>
|
|
typeof t === "object" && t && "value" in t;
|
|
|
|
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 ");
|
|
} else if (isValueType(info)) {
|
|
return String(info.value);
|
|
}
|
|
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 (isValueType(descr)) {
|
|
return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
|
|
} 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;
|
|
}
|