Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/list-property-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"schema-utils": patch
---

output the type of object properties in error messages, i.e. `object { foo?: boolean, bar?: integer }`
67 changes: 57 additions & 10 deletions src/ValidationError.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,31 @@ function getArticle(type) {
return "a";
}

// The constraints a formatted type ends with, they are stated by the error on
// the property itself, so the summary of the shape does not repeat them
const TYPE_CONSTRAINTS_REGEXP = /\s*\([^()]*\)$/;
// A type that is more than a name: a union, an object or an array
const COMPOSITE_TYPE_REGEXP = /[{}[\]()|\n]/;
const MAX_LEAF_TYPE_LENGTH = 24;
// The width the listed types of an object have to fit in
const MAX_TYPED_STRUCTURE_LENGTH = 120;

/**
* Names the type of a property shortly enough to sit next to the property name.
* A composite type is left out, it buries the property names it is listed with.
* @param {string} type formatted type
* @returns {string} the short name of the type, an empty string when it has none
*/
function getLeafTypeName(type) {
const name = type.replace(TYPE_CONSTRAINTS_REGEXP, "");

return name.length > 0 &&
name.length <= MAX_LEAF_TYPE_LENGTH &&
!COMPOSITE_TYPE_REGEXP.test(name)
? name
: "";
}

/**
* @param {Schema=} schema schema
* @returns {string} schema non types
Expand Down Expand Up @@ -830,23 +855,45 @@ class ValidationError extends Error {
...new Set(/** @type {string[]} */ ([...required, ...properties])),
];

const objectStructure = [
...allProperties.map((property) => {
const isRequired = required.includes(property);

// Some properties need quotes, maybe we should add check
// Maybe we should output type of property (`foo: string`), but it is looks very unreadable
return `${property}${isRequired ? "" : "?"}`;
}),
...(typeof schema.additionalProperties === "undefined" ||
const namedProperties = allProperties.map((property) => {
// Some properties need quotes, maybe we should add check
const name = `${property}${required.includes(property) ? "" : "?"}`;
const propertySchema = schema.properties
? schema.properties[property]
: undefined;
const type =
propertySchema && propertySchema !== true
? getLeafTypeName(formatInnerSchema(propertySchema, true))
: "";

return { name, type };
});

const otherProperties =
typeof schema.additionalProperties === "undefined" ||
Boolean(schema.additionalProperties)
? schema.additionalProperties &&
isObject(schema.additionalProperties) &&
schema.additionalProperties !== true
? [`<key>: ${formatInnerSchema(schema.additionalProperties)}`]
: ["…"]
: []),
: [];

const typedStructure = [
...namedProperties.map(
({ name, type }) => `${name}${type ? `: ${type}` : ""}`,
),
...otherProperties,
].join(", ");
// The types are only listed while the shape stays readable, an object with
// that many properties is hard enough to read by the names alone
const objectStructure =
typedStructure.length <= MAX_TYPED_STRUCTURE_LENGTH
? typedStructure
: [
...namedProperties.map(({ name }) => name),
...otherProperties,
].join(", ");

const { dependencies, propertyNames, patternRequired } =
/** @type {Schema & { patternRequired?: string[] }} */ (schema);
Expand Down
51 changes: 42 additions & 9 deletions test/__snapshots__/api.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,73 @@
exports[`api should allow to enable validation using "process.env.SKIP_VALIDATION" #2 1`] = `
"Invalid options object. NAME has been initialized using an options object that does not match the API schema.
- options has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should allow to enable validation using "process.env.SKIP_VALIDATION" 1`] = `
"Invalid options object. NAME has been initialized using an options object that does not match the API schema.
- options has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should allow to enable validation using API 1`] = `
"Invalid options object. NAME has been initialized using an options object that does not match the API schema.
- options has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should get configuration from schema 1`] = `
"Invalid options object. CSS Loader has been initialized using an options object that does not match the API schema.
- options has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should not output the type of a property when it is a composite one 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should be an object:
object { shorthand?: string, union?, nested?, list?, constrained?: number, … }"
`;

exports[`api should not output the types of an object with many properties 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should be an object:
object { property0?, property1?, property2?, property3?, property4?, property5?, property6?, property7?, property8?, property9?, property10?, property11?, property12?, property13?, property14?, property15?, property16?, property17?, property18?, property19?, … }"
`;

exports[`api should output the type of each property 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should be an object:
object { foo?: boolean, bar?: integer, … }"
`;

exports[`api should prefer configuration over "title" #1 1`] = `
"Invalid options object. NAME has been initialized using an options object that does not match the API schema.
- options has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should prefer configuration over "title" #2 1`] = `
"Invalid BaseDataPath object. CSS Loader has been initialized using a BaseDataPath object that does not match the API schema.
- BaseDataPath has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should prefer configuration over "title" 1`] = `
"Invalid BaseDataPath object. NAME has been initialized using a BaseDataPath object that does not match the API schema.
- BaseDataPath has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should use default values when "title" is broken 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'foo'. These properties are valid:
object { name? }"
object { name?: boolean }"
`;

exports[`api should work with anyOf 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration should be one of these:
object { bar, … } | object { baz, … }
object { bar: number, … } | object { baz: number, … }
Details:
* configuration misses the property 'bar' | should be any non-object. Should be:
number
Expand All @@ -74,3 +92,18 @@ exports[`api should work with required properties 1`] = `
- configuration.c misses the property 'e'. Should be:
string"
`;

exports[`api should work with the \`not\` keyword 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should be any non-number | number (should be < 3)."
`;

exports[`api should work with the \`not\` keyword 2`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should be any non-string."
`;

exports[`api should work with the \`not\` keyword 3`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.v should not be 1 | 2."
`;
Loading
Loading