[IMP] props_validation: have clearer error messages

With this commit, props validation error messages will include a more
developer-friendly error message, avoiding the need to investigate in
the Developer Tools why a complex props structure is invalid.
This commit is contained in:
Paul Morelle
2022-05-12 17:13:54 +02:00
committed by Géry Debongnie
parent c06049076a
commit 940ac64340
3 changed files with 88 additions and 38 deletions
+48 -24
View File
@@ -40,15 +40,16 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
continue;
}
}
let isValid;
let whyInvalid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
whyInvalid = whyInvalidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
if (whyInvalid !== null) {
whyInvalid = whyInvalid.replace(/\${propName}/g, propName);
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}': ${whyInvalid}`);
}
}
for (let propName in props) {
@@ -60,11 +61,11 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
};
/**
* Check if an invidual prop value matches its (static) prop definition
* Check why an invidual prop value doesn't match its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
function whyInvalidProp(prop, propDef): string | null {
if (propDef === true) {
return true;
return null;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
@@ -73,43 +74,66 @@ function isValidProp(prop, propDef): boolean {
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
if (prop instanceof propDef) {
return null;
}
return `\${propName} is not an instance of ${propDef.name}`;
}
return typeof prop === propDef.name.toLowerCase();
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 result = false;
let reasons: string[] = [];
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[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];
}
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
return null;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
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++) {
result = result && isValidProp(prop[i], propDef.element);
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) {
result = result && isValidProp(prop[key], shape[key]);
const why = whyInvalidProp(prop[key], shape[key]);
if (why !== null) {
return why.replace(/\${propName}/g, `\${propName}['${key}']`);
}
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
for (let propName in prop) {
if (!(propName in shape)) {
return `unknown prop \${propName}['${propName}']`;
}
}
}
return result;
return null;
}