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/humanize-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"schema-utils": patch
---

read the `format` keyword as part of the type, i.e. `should be a date string` instead of `should be a string (should match format "date")`
2 changes: 2 additions & 0 deletions declarations/util/humanize.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare function _exports(str: string): string;
export = _exports;
32 changes: 26 additions & 6 deletions src/util/hints.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
const Range = require("./Range");
const humanize = require("./humanize");

const STRING_TYPE_REGEXP = /string$/;
// A format that is a name, so it can be read as words. A format is any string,
// including a pattern like `[0-9]*`, and such a one is left to a hint
const FORMAT_NAME_REGEXP = /^[A-Za-z][A-Za-z\d]*(?:[-_][A-Za-z\d]+)*$/;

/** @typedef {import("../validate").Schema} Schema */

Expand Down Expand Up @@ -50,6 +56,7 @@ module.exports.numberHints = function numberHints(schema, logic) {
module.exports.stringHints = function stringHints(schema, logic) {
const hints = [];
let type = "string";
let formatName = "";
const currentSchema = { ...schema };

if (!logic) {
Expand Down Expand Up @@ -99,11 +106,17 @@ module.exports.stringHints = function stringHints(schema, logic) {
}

if (currentSchema.format) {
hints.push(
`should${logic ? "" : " not"} match format ${JSON.stringify(
currentSchema.format,
)}`,
);
if (logic && FORMAT_NAME_REGEXP.test(currentSchema.format)) {
// The format names the string, `should be a date string` reads better than
// `should be a string (should match format "date")`
formatName = humanize(currentSchema.format);
} else {
hints.push(
`should${logic ? "" : " not"} match format ${JSON.stringify(
currentSchema.format,
)}`,
);
}
}

if (currentSchema.formatMinimum) {
Expand All @@ -122,5 +135,12 @@ module.exports.stringHints = function stringHints(schema, logic) {
);
}

return [type, ...hints];
// Every type here ends with `string`, the format names the string itself, so it
// goes next to that word - `non-empty email string`, not `email non-empty string`
return [
formatName
? type.replace(STRING_TYPE_REGEXP, `${formatName} string`)
: type,
...hints,
];
};
29 changes: 29 additions & 0 deletions src/util/humanize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const CAMEL_CASE_REGEXP = /([^A-Z])([A-Z])/g;
const LEADING_UNDERSCORE_REGEXP = /^_/;

/**
* Turns the name of a format into words, so `dash-case`, `snake_case`,
* `camelCase` and `PascalCase` all read as `dash case`, `snake case`, and so on.
* @param {string} str provided string
* @returns {string} the string as human readable words
*/
module.exports = function humanize(str) {
if (str.length < 2) {
return str;
}

if (str.includes("-")) {
return str.split("-").join(" ").toLowerCase();
}

// A leading underscore is not a word boundary, `_12Integers` is `12 integers`
const withoutLeadingUnderscore = str.replace(LEADING_UNDERSCORE_REGEXP, "");

if (withoutLeadingUnderscore.includes("_")) {
return withoutLeadingUnderscore.split("_").join(" ").toLowerCase();
}

return withoutLeadingUnderscore
.replace(CAMEL_CASE_REGEXP, "$1 $2")
.toLowerCase();
};
4 changes: 2 additions & 2 deletions test/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ exports[`validation should fail validation for format, formatMaximum and formatE

exports[`validation should fail validation for format, formatMaximum and formatExclusiveMaximum 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.strictFormat should be a string (should match format "date", should be < "2016-02-06")."
- configuration.strictFormat should be a date string (should be < "2016-02-06")."
`;

exports[`validation should fail validation for format, formatMinimum and formatExclusiveMinimum #2 1`] = `
Expand All @@ -743,7 +743,7 @@ exports[`validation should fail validation for format, formatMinimum and formatE

exports[`validation should fail validation for format, formatMinimum and formatExclusiveMinimum 1`] = `
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
- configuration.strictFormat2 should be a string (should match format "date", should be > "2016-02-06")."
- configuration.strictFormat2 should be a date string (should be > "2016-02-06")."
`;

exports[`validation should fail validation for formatExclusiveMaximum #1 1`] = `
Expand Down
12 changes: 11 additions & 1 deletion test/hints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@ const testCases = [
['should match pattern "phone"'],
['should not match pattern "phone"'],
],
[
{ format: "date-time" },
["date time string"],
['should not match format "date-time"'],
],
[
{ format: "email", minLength: 1 },
["non-empty email string"],
['should not match format "email"'],
],
[
{
format: "date",
formatMaximum: "01.01.2022",
formatExclusiveMaximum: "01.01.2022",
},
['should match format "date"', 'should be < "01.01.2022"'],
["date string", 'should be < "01.01.2022"'],
['should not match format "date"', 'should be >= "01.01.2022"'],
],
];
Expand Down
23 changes: 23 additions & 0 deletions test/humanize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const humanize = require("../src/util/humanize");

const words = [
["dash-case", "dash case"],
["_snake_case", "snake case"],
["snake-case", "snake case"],
["PascalCase", "pascal case"],
["_12Integers", "12 integers"],
["awesomeStringFormat13", "awesome string format13"],
["camelCase", "camel case"],
["date-time", "date time"],
["email", "email"],
["a", "a"],
["", ""],
];

describe("humanize", () => {
for (const [provided, expected] of words) {
it(JSON.stringify(provided), () => {
expect(humanize(provided)).toBe(expected);
});
}
});
Loading