Skip to content

Commit

Permalink
feat: implement undefinedAsNull keyword for enum type (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 7, 2023
1 parent 6d5a65f commit 1265eac
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 8 deletions.
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()
);
});

0 comments on commit 1265eac

Please sign in to comment.