mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] runtime: allow validating object values using a type description
In JS, objects are often used as a mapping that gives fast lookup on the keys, in those cases, the keys themselves are not known in advance, but the values may have a shape that they're expected to follow. This commit adds support for a `values` key in schema for objects, which will be checked against all values in the object during validation.
This commit is contained in:
committed by
Géry Debongnie
parent
fea8fcaf61
commit
47009912df
@@ -212,6 +212,7 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `values`: if the type was `Object`, then the `values` key describes the interface of values in the object, this allows validating objects that are used as mappings, where keys are not known in advance but the shape of the values is.
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
@@ -276,6 +277,10 @@ class ComponentB extends owl.Component {
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someObj3: {
|
||||
type: Object,
|
||||
values: { type: Array, element: String },
|
||||
}, // object with arbitary keys where values are arrays of strings
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
|
||||
@@ -54,7 +54,7 @@ export {
|
||||
onWillDestroy,
|
||||
onError,
|
||||
} from "./lifecycle_hooks";
|
||||
export { validate } from "./validation";
|
||||
export { validate, validateType } from "./validation";
|
||||
export { OwlError } from "./error_handling";
|
||||
|
||||
export const __info__ = {
|
||||
|
||||
@@ -17,6 +17,7 @@ interface TypeInfo {
|
||||
validate?: Function;
|
||||
shape?: Schema;
|
||||
element?: TypeDescription;
|
||||
values?: TypeDescription;
|
||||
}
|
||||
|
||||
type ValueType = { value: any };
|
||||
@@ -137,7 +138,7 @@ function validateArrayType(key: string, value: any, descr: TypeDescription): str
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateType(key: string, value: any, descr: TypeDescription): string | null {
|
||||
export 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)) {
|
||||
@@ -151,13 +152,24 @@ function validateType(key: string, value: any, descr: TypeDescription): string |
|
||||
let result: string | null = null;
|
||||
if ("element" in descr) {
|
||||
result = validateArrayType(key, value, descr.element!);
|
||||
} else if ("shape" in descr && !result) {
|
||||
} else if ("shape" in descr) {
|
||||
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(", ")})`;
|
||||
result = `'${key}' doesn't have the correct shape (${errors.join(", ")})`;
|
||||
}
|
||||
}
|
||||
} else if ("values" in descr) {
|
||||
if (typeof value !== "object" || Array.isArray(value)) {
|
||||
result = `'${key}' is not an object`;
|
||||
} else {
|
||||
const errors = Object.entries(value)
|
||||
.map(([key, value]) => validateType(key, value, descr.values!))
|
||||
.filter(Boolean);
|
||||
if (errors.length) {
|
||||
result = `some of the values in '${key}' are invalid (${errors.join(", ")})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape (unknown key 'extra')"
|
||||
);
|
||||
props = { p: { id: "1", url: "url" } };
|
||||
app = new App(Parent, { test: true });
|
||||
@@ -413,7 +413,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('id' is not a number)"
|
||||
);
|
||||
error = undefined;
|
||||
props = { p: { id: 1 } };
|
||||
@@ -423,7 +423,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('url' is missing (should be a string))"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("props validation", () => {
|
||||
await mountProm;
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)"
|
||||
"Invalid props for component 'SubComp': 'p' doesn't have the correct shape ('url' is not a boolean or list of numbers)"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -502,7 +502,7 @@ describe("props validation", () => {
|
||||
}
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe(
|
||||
"Invalid props for component 'TestComponent': 'myprop[0]' has not the correct shape (unknown key 'a')"
|
||||
"Invalid props for component 'TestComponent': 'myprop[0]' doesn't have the correct shape (unknown key 'a')"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -145,17 +145,51 @@ describe("validateSchema", () => {
|
||||
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))",
|
||||
"'p' doesn't have 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)",
|
||||
"'p' doesn't have the correct shape ('url' is not a string)",
|
||||
]);
|
||||
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||
"'p' is undefined (should be a object)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("objects with a values schema", () => {
|
||||
const schema: Schema = {
|
||||
p: { type: Object, values: { type: Object, shape: { id: Number, url: String } } },
|
||||
};
|
||||
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
|
||||
expect(validateSchema({ p: {} }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: { id: 1, url: "asf" } }, schema)).toEqual([
|
||||
"some of the values in 'p' are invalid ('id' is not an object, 'url' is not an object)",
|
||||
]);
|
||||
expect(
|
||||
validateSchema(
|
||||
{
|
||||
p: {
|
||||
a: { id: 1, url: "asf" },
|
||||
},
|
||||
},
|
||||
schema
|
||||
)
|
||||
).toEqual([]);
|
||||
expect(
|
||||
validateSchema(
|
||||
{
|
||||
p: {
|
||||
a: { id: 1, url: "asf" },
|
||||
b: { id: 1, url: 1 },
|
||||
},
|
||||
},
|
||||
schema
|
||||
)
|
||||
).toEqual([
|
||||
"some of the values in 'p' are invalid ('b' doesn't have the correct shape ('url' is not a string))",
|
||||
]);
|
||||
});
|
||||
|
||||
test("objects with more complex shape", () => {
|
||||
const schema: Schema = {
|
||||
p: {
|
||||
@@ -168,10 +202,10 @@ describe("validateSchema", () => {
|
||||
};
|
||||
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))",
|
||||
"'p' doesn't have 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)",
|
||||
"'p' doesn't have 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([
|
||||
@@ -183,11 +217,11 @@ describe("validateSchema", () => {
|
||||
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))",
|
||||
"'p' doesn't have 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)",
|
||||
"'p' doesn't have the correct shape ('id' is not a number)",
|
||||
]);
|
||||
expect(validateSchema({ p: { id: 1, url: 1 } }, schema)).toEqual([]);
|
||||
expect(validateSchema({ p: undefined }, schema)).toEqual([
|
||||
@@ -222,7 +256,7 @@ describe("validateSchema", () => {
|
||||
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)",
|
||||
"'p[0]' doesn't have the correct shape ('num' is not a number)",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user