Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement undefinedAsNull keyword for enum type #175

Merged
merged 5 commits into from Jun 7, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions declarations/keywords/undefinedAsNull.d.ts
@@ -0,0 +1,15 @@
export default addUndefinedAsNullKeyword;
export type Ajv = import("ajv").default;
export type SchemaValidateFunction = import("ajv").SchemaValidateFunction;
export type AnySchemaObject = import("ajv").AnySchemaObject;
export type ValidateFunction = import("ajv").ValidateFunction;
/** @typedef {import("ajv").default} Ajv */
/** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */
/** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */
/** @typedef {import("ajv").ValidateFunction} ValidateFunction */
/**
*
* @param {Ajv} ajv
* @returns {Ajv}
*/
declare function addUndefinedAsNullKeyword(ajv: Ajv): Ajv;
2 changes: 2 additions & 0 deletions declarations/validate.d.ts
Expand Up @@ -8,6 +8,7 @@ export type Extend = {
formatExclusiveMinimum?: string | undefined;
formatExclusiveMaximum?: string | undefined;
link?: string | undefined;
undefinedAsNull?: boolean | undefined;
};
export type Schema = (JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend;
export type SchemaUtilErrorObject = ErrorObject & {
Expand All @@ -33,6 +34,7 @@ export type ValidationErrorConfiguration = {
* @property {string=} formatExclusiveMinimum
* @property {string=} formatExclusiveMaximum
* @property {string=} link
* @property {boolean=} undefinedAsNull
*/
/** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend} Schema */
/** @typedef {ErrorObject & { children?: Array<ErrorObject>}} SchemaUtilErrorObject */
Expand Down
12 changes: 10 additions & 2 deletions src/ValidationError.js
Expand Up @@ -543,9 +543,17 @@ class ValidationError extends Error {
}

if (schema.enum) {
return /** @type {Array<any>} */ (schema.enum)
.map((item) => JSON.stringify(item))
const enumValues = /** @type {Array<any>} */ (schema.enum)
.map((item) => {
if (item === null && schema.undefinedAsNull) {
return `${JSON.stringify(item)} | undefined`;
}

return JSON.stringify(item);
})
.join(" | ");

return `${enumValues}`;
}

if (typeof schema.const !== "undefined") {
Expand Down
39 changes: 39 additions & 0 deletions src/keywords/undefinedAsNull.js
@@ -0,0 +1,39 @@
/** @typedef {import("ajv").default} Ajv */
/** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */
/** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */
/** @typedef {import("ajv").ValidateFunction} ValidateFunction */

/**
*
* @param {Ajv} ajv
* @returns {Ajv}
*/
function addUndefinedAsNullKeyword(ajv) {
ajv.addKeyword({
keyword: "undefinedAsNull",
before: "enum",
modifying: true,
/** @type {SchemaValidateFunction} */
validate(kwVal, data, metadata, dataCxt) {
if (
kwVal &&
dataCxt &&
metadata &&
typeof metadata.enum !== "undefined"
) {
const idx = dataCxt.parentDataProperty;

if (typeof dataCxt.parentData[idx] === "undefined") {
// eslint-disable-next-line no-param-reassign
dataCxt.parentData[dataCxt.parentDataProperty] = null;
}
}

return true;
},
});

return ajv;
}

export default addUndefinedAsNullKeyword;
4 changes: 4 additions & 0 deletions src/validate.js
@@ -1,4 +1,5 @@
import addAbsolutePathKeyword from "./keywords/absolutePath";
import addUndefinedAsNullKeyword from "./keywords/undefinedAsNull";

import ValidationError from "./ValidationError";

Expand Down Expand Up @@ -48,8 +49,10 @@ const getAjv = memoize(() => {

ajvKeywords(ajv, ["instanceof", "patternRequired"]);
addFormats(ajv, { keywords: true });

// Custom keywords
addAbsolutePathKeyword(ajv);
addUndefinedAsNullKeyword(ajv);

return ajv;
});
Expand All @@ -66,6 +69,7 @@ const getAjv = memoize(() => {
* @property {string=} formatExclusiveMinimum
* @property {string=} formatExclusiveMaximum
* @property {string=} link
* @property {boolean=} undefinedAsNull
*/

/** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend} Schema */
Expand Down
54 changes: 48 additions & 6 deletions test/__snapshots__/index.test.js.snap

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions test/fixtures/schema.json
Expand Up @@ -3791,6 +3791,47 @@
"format": "date",
"formatMinimum": "2016-02-06",
"formatExclusiveMaximum": "2016-12-27"
},
"enumKeywordAndUndefined": {
"undefinedAsNull": true,
"enum": [ 0, false, "", null ]
},
"arrayStringAndEnum": {
"description": "References to other configurations to depend on.",
"type": "array",
"items": {
"anyOf": [
{
"undefinedAsNull": true,
"enum": [ 0, false, "", null ]
},
{
"type": "string",
"minLength": 1
}
]
}
},
"arrayStringAndEnumAndNoUndefined": {
"description": "References to other configurations to depend on.",
"type": "array",
"items": {
"anyOf": [
{
"undefinedAsNull": false,
"enum": [ 0, false, "", null ]
},
{
"type": "string",
"minLength": 1
}
]
}
},
"stringTypeAndUndefinedAsNull": {
"description": "References to other configurations to depend on.",
"type": "string",
"undefinedAsNull": true
}
}
}
69 changes: 69 additions & 0 deletions test/index.test.js
Expand Up @@ -314,6 +314,33 @@ describe("Validation", () => {
},
});

createSuccessTestCase("enum with undefinedAsNull", {
// eslint-disable-next-line no-undefined
enumKeywordAndUndefined: undefined,
});

createSuccessTestCase("enum with undefinedAsNull #2", {
enumKeywordAndUndefined: 0,
});

createSuccessTestCase("array with enum and undefinedAsNull", {
arrayStringAndEnum: ["a", "b", "c"],
});

createSuccessTestCase("array with enum and undefinedAsNull #2", {
// eslint-disable-next-line no-undefined
arrayStringAndEnum: [undefined, false, undefined, 0, "test", undefined],
});

createSuccessTestCase("array with enum and undefinedAsNull #3", {
// eslint-disable-next-line no-undefined
arrayStringAndEnum: [undefined, null, false, 0, ""],
});

createSuccessTestCase("string and undefinedAsNull #3", {
stringTypeAndUndefinedAsNull: "test",
});

// The "name" option
createFailedTestCase(
"webpack name",
Expand Down Expand Up @@ -2987,4 +3014,46 @@ describe("Validation", () => {
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
"enum and undefinedAsNull",
{
enumKeywordAndUndefined: "foo",
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
"array with enum and undefinedAsNull",
{
arrayStringAndEnum: ["foo", "bar", 1],
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
"array with enum and undefinedAsNull #2",
{
// eslint-disable-next-line no-undefined
arrayStringAndEnum: ["foo", "bar", undefined, 1],
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
"array with enum and undefinedAsNull #3",
{
// eslint-disable-next-line no-undefined
arrayStringAndEnumAndNoUndefined: ["foo", "bar", undefined],
},
(msg) => expect(msg).toMatchSnapshot()
);

createFailedTestCase(
"string and undefinedAsNull",
{
stringTypeAndUndefinedAsNull: 1,
},
(msg) => expect(msg).toMatchSnapshot()
);
});