[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:
Samuel Degueldre
2023-04-05 08:18:11 +02:00
committed by Géry Debongnie
parent fea8fcaf61
commit 47009912df
5 changed files with 67 additions and 16 deletions
+5
View File
@@ -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 - `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, - `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, - `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 - `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic. the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory - `optional`: if true, the prop is not mandatory
@@ -276,6 +277,10 @@ class ComponentB extends owl.Component {
name: {type: String, optional: true}, name: {type: String, optional: true},
url: String url: String
]}, // object, with keys id (number), name (string, optional) and 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`) someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop otherValue: true, // indicates that it is a prop
+1 -1
View File
@@ -54,7 +54,7 @@ export {
onWillDestroy, onWillDestroy,
onError, onError,
} from "./lifecycle_hooks"; } from "./lifecycle_hooks";
export { validate } from "./validation"; export { validate, validateType } from "./validation";
export { OwlError } from "./error_handling"; export { OwlError } from "./error_handling";
export const __info__ = { export const __info__ = {
+15 -3
View File
@@ -17,6 +17,7 @@ interface TypeInfo {
validate?: Function; validate?: Function;
shape?: Schema; shape?: Schema;
element?: TypeDescription; element?: TypeDescription;
values?: TypeDescription;
} }
type ValueType = { value: any }; type ValueType = { value: any };
@@ -137,7 +138,7 @@ function validateArrayType(key: string, value: any, descr: TypeDescription): str
return null; 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) { if (value === undefined) {
return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`; return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
} else if (isBaseType(descr)) { } else if (isBaseType(descr)) {
@@ -151,13 +152,24 @@ function validateType(key: string, value: any, descr: TypeDescription): string |
let result: string | null = null; let result: string | null = null;
if ("element" in descr) { if ("element" in descr) {
result = validateArrayType(key, value, descr.element!); result = validateArrayType(key, value, descr.element!);
} else if ("shape" in descr && !result) { } else if ("shape" in descr) {
if (typeof value !== "object" || Array.isArray(value)) { if (typeof value !== "object" || Array.isArray(value)) {
result = `'${key}' is not an object`; result = `'${key}' is not an object`;
} else { } else {
const errors = validateSchema(value, descr.shape!); const errors = validateSchema(value, descr.shape!);
if (errors.length) { 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(", ")})`;
} }
} }
} }
+5 -5
View File
@@ -404,7 +404,7 @@ describe("props validation", () => {
await mountProm; await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( 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" } }; props = { p: { id: "1", url: "url" } };
app = new App(Parent, { test: true }); app = new App(Parent, { test: true });
@@ -413,7 +413,7 @@ describe("props validation", () => {
await mountProm; await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( 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; error = undefined;
props = { p: { id: 1 } }; props = { p: { id: 1 } };
@@ -423,7 +423,7 @@ describe("props validation", () => {
await mountProm; await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( 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; await mountProm;
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( 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!).toBeDefined();
expect(error!.message).toBe( 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')"
); );
}); });
+41 -7
View File
@@ -145,17 +145,51 @@ describe("validateSchema", () => {
const schema: Schema = { p: { type: Object, shape: { id: Number, url: String } } }; 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' is not an object"]);
expect(validateSchema({ p: {} }, schema)).toEqual([ 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: "asf" } }, schema)).toEqual([]);
expect(validateSchema({ p: { id: 1, url: 1 } }, 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([ expect(validateSchema({ p: undefined }, schema)).toEqual([
"'p' is undefined (should be a object)", "'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", () => { test("objects with more complex shape", () => {
const schema: Schema = { const schema: Schema = {
p: { p: {
@@ -168,10 +202,10 @@ describe("validateSchema", () => {
}; };
expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]); expect(validateSchema({ p: [] }, schema)).toEqual(["'p' is not an object"]);
expect(validateSchema({ p: {} }, schema)).toEqual([ 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([ 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: { id: 1, url: true } }, schema)).toEqual([]);
expect(validateSchema({ p: undefined }, schema)).toEqual([ expect(validateSchema({ p: undefined }, schema)).toEqual([
@@ -183,11 +217,11 @@ describe("validateSchema", () => {
const schema: Schema = { p: { type: Object, shape: { id: Number, "*": true } } }; 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' is not an object"]);
expect(validateSchema({ p: {} }, schema)).toEqual([ 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: 1 } }, schema)).toEqual([]);
expect(validateSchema({ p: { id: "asdf" } }, 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: { id: 1, url: 1 } }, schema)).toEqual([]);
expect(validateSchema({ p: undefined }, schema)).toEqual([ expect(validateSchema({ p: undefined }, schema)).toEqual([
@@ -222,7 +256,7 @@ describe("validateSchema", () => {
expect(validateSchema({ p: [{}] }, schema)).toEqual([]); expect(validateSchema({ p: [{}] }, schema)).toEqual([]);
expect(validateSchema({ p: [{ num: 1 }] }, schema)).toEqual([]); expect(validateSchema({ p: [{ num: 1 }] }, schema)).toEqual([]);
expect(validateSchema({ p: [{ num: true }] }, 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)",
]); ]);
}); });