Skip to content

Commit 1265eac

Browse files
feat: implement undefinedAsNull keyword for enum type (#175)
1 parent 6d5a65f commit 1265eac

File tree

8 files changed

+228
-8
lines changed

8 files changed

+228
-8
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default addUndefinedAsNullKeyword;
2+
export type Ajv = import("ajv").default;
3+
export type SchemaValidateFunction = import("ajv").SchemaValidateFunction;
4+
export type AnySchemaObject = import("ajv").AnySchemaObject;
5+
export type ValidateFunction = import("ajv").ValidateFunction;
6+
/** @typedef {import("ajv").default} Ajv */
7+
/** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */
8+
/** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */
9+
/** @typedef {import("ajv").ValidateFunction} ValidateFunction */
10+
/**
11+
*
12+
* @param {Ajv} ajv
13+
* @returns {Ajv}
14+
*/
15+
declare function addUndefinedAsNullKeyword(ajv: Ajv): Ajv;

declarations/validate.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type Extend = {
88
formatExclusiveMinimum?: string | undefined;
99
formatExclusiveMaximum?: string | undefined;
1010
link?: string | undefined;
11+
undefinedAsNull?: boolean | undefined;
1112
};
1213
export type Schema = (JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend;
1314
export type SchemaUtilErrorObject = ErrorObject & {
@@ -33,6 +34,7 @@ export type ValidationErrorConfiguration = {
3334
* @property {string=} formatExclusiveMinimum
3435
* @property {string=} formatExclusiveMaximum
3536
* @property {string=} link
37+
* @property {boolean=} undefinedAsNull
3638
*/
3739
/** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend} Schema */
3840
/** @typedef {ErrorObject & { children?: Array<ErrorObject>}} SchemaUtilErrorObject */

src/ValidationError.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,17 @@ class ValidationError extends Error {
543543
}
544544

545545
if (schema.enum) {
546-
return /** @type {Array<any>} */ (schema.enum)
547-
.map((item) => JSON.stringify(item))
546+
const enumValues = /** @type {Array<any>} */ (schema.enum)
547+
.map((item) => {
548+
if (item === null && schema.undefinedAsNull) {
549+
return `${JSON.stringify(item)} | undefined`;
550+
}
551+
552+
return JSON.stringify(item);
553+
})
548554
.join(" | ");
555+
556+
return `${enumValues}`;
549557
}
550558

551559
if (typeof schema.const !== "undefined") {

src/keywords/undefinedAsNull.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/** @typedef {import("ajv").default} Ajv */
2+
/** @typedef {import("ajv").SchemaValidateFunction} SchemaValidateFunction */
3+
/** @typedef {import("ajv").AnySchemaObject} AnySchemaObject */
4+
/** @typedef {import("ajv").ValidateFunction} ValidateFunction */
5+
6+
/**
7+
*
8+
* @param {Ajv} ajv
9+
* @returns {Ajv}
10+
*/
11+
function addUndefinedAsNullKeyword(ajv) {
12+
ajv.addKeyword({
13+
keyword: "undefinedAsNull",
14+
before: "enum",
15+
modifying: true,
16+
/** @type {SchemaValidateFunction} */
17+
validate(kwVal, data, metadata, dataCxt) {
18+
if (
19+
kwVal &&
20+
dataCxt &&
21+
metadata &&
22+
typeof metadata.enum !== "undefined"
23+
) {
24+
const idx = dataCxt.parentDataProperty;
25+
26+
if (typeof dataCxt.parentData[idx] === "undefined") {
27+
// eslint-disable-next-line no-param-reassign
28+
dataCxt.parentData[dataCxt.parentDataProperty] = null;
29+
}
30+
}
31+
32+
return true;
33+
},
34+
});
35+
36+
return ajv;
37+
}
38+
39+
export default addUndefinedAsNullKeyword;

src/validate.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import addAbsolutePathKeyword from "./keywords/absolutePath";
2+
import addUndefinedAsNullKeyword from "./keywords/undefinedAsNull";
23

34
import ValidationError from "./ValidationError";
45

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

4950
ajvKeywords(ajv, ["instanceof", "patternRequired"]);
5051
addFormats(ajv, { keywords: true });
52+
5153
// Custom keywords
5254
addAbsolutePathKeyword(ajv);
55+
addUndefinedAsNullKeyword(ajv);
5356

5457
return ajv;
5558
});
@@ -66,6 +69,7 @@ const getAjv = memoize(() => {
6669
* @property {string=} formatExclusiveMinimum
6770
* @property {string=} formatExclusiveMaximum
6871
* @property {string=} link
72+
* @property {boolean=} undefinedAsNull
6973
*/
7074

7175
/** @typedef {(JSONSchema4 | JSONSchema6 | JSONSchema7) & Extend} Schema */

test/__snapshots__/index.test.js.snap

Lines changed: 48 additions & 6 deletions
Large diffs are not rendered by default.

test/fixtures/schema.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3791,6 +3791,47 @@
37913791
"format": "date",
37923792
"formatMinimum": "2016-02-06",
37933793
"formatExclusiveMaximum": "2016-12-27"
3794+
},
3795+
"enumKeywordAndUndefined": {
3796+
"undefinedAsNull": true,
3797+
"enum": [ 0, false, "", null ]
3798+
},
3799+
"arrayStringAndEnum": {
3800+
"description": "References to other configurations to depend on.",
3801+
"type": "array",
3802+
"items": {
3803+
"anyOf": [
3804+
{
3805+
"undefinedAsNull": true,
3806+
"enum": [ 0, false, "", null ]
3807+
},
3808+
{
3809+
"type": "string",
3810+
"minLength": 1
3811+
}
3812+
]
3813+
}
3814+
},
3815+
"arrayStringAndEnumAndNoUndefined": {
3816+
"description": "References to other configurations to depend on.",
3817+
"type": "array",
3818+
"items": {
3819+
"anyOf": [
3820+
{
3821+
"undefinedAsNull": false,
3822+
"enum": [ 0, false, "", null ]
3823+
},
3824+
{
3825+
"type": "string",
3826+
"minLength": 1
3827+
}
3828+
]
3829+
}
3830+
},
3831+
"stringTypeAndUndefinedAsNull": {
3832+
"description": "References to other configurations to depend on.",
3833+
"type": "string",
3834+
"undefinedAsNull": true
37943835
}
37953836
}
37963837
}

test/index.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,33 @@ describe("Validation", () => {
314314
},
315315
});
316316

317+
createSuccessTestCase("enum with undefinedAsNull", {
318+
// eslint-disable-next-line no-undefined
319+
enumKeywordAndUndefined: undefined,
320+
});
321+
322+
createSuccessTestCase("enum with undefinedAsNull #2", {
323+
enumKeywordAndUndefined: 0,
324+
});
325+
326+
createSuccessTestCase("array with enum and undefinedAsNull", {
327+
arrayStringAndEnum: ["a", "b", "c"],
328+
});
329+
330+
createSuccessTestCase("array with enum and undefinedAsNull #2", {
331+
// eslint-disable-next-line no-undefined
332+
arrayStringAndEnum: [undefined, false, undefined, 0, "test", undefined],
333+
});
334+
335+
createSuccessTestCase("array with enum and undefinedAsNull #3", {
336+
// eslint-disable-next-line no-undefined
337+
arrayStringAndEnum: [undefined, null, false, 0, ""],
338+
});
339+
340+
createSuccessTestCase("string and undefinedAsNull #3", {
341+
stringTypeAndUndefinedAsNull: "test",
342+
});
343+
317344
// The "name" option
318345
createFailedTestCase(
319346
"webpack name",
@@ -2987,4 +3014,46 @@ describe("Validation", () => {
29873014
},
29883015
(msg) => expect(msg).toMatchSnapshot()
29893016
);
3017+
3018+
createFailedTestCase(
3019+
"enum and undefinedAsNull",
3020+
{
3021+
enumKeywordAndUndefined: "foo",
3022+
},
3023+
(msg) => expect(msg).toMatchSnapshot()
3024+
);
3025+
3026+
createFailedTestCase(
3027+
"array with enum and undefinedAsNull",
3028+
{
3029+
arrayStringAndEnum: ["foo", "bar", 1],
3030+
},
3031+
(msg) => expect(msg).toMatchSnapshot()
3032+
);
3033+
3034+
createFailedTestCase(
3035+
"array with enum and undefinedAsNull #2",
3036+
{
3037+
// eslint-disable-next-line no-undefined
3038+
arrayStringAndEnum: ["foo", "bar", undefined, 1],
3039+
},
3040+
(msg) => expect(msg).toMatchSnapshot()
3041+
);
3042+
3043+
createFailedTestCase(
3044+
"array with enum and undefinedAsNull #3",
3045+
{
3046+
// eslint-disable-next-line no-undefined
3047+
arrayStringAndEnumAndNoUndefined: ["foo", "bar", undefined],
3048+
},
3049+
(msg) => expect(msg).toMatchSnapshot()
3050+
);
3051+
3052+
createFailedTestCase(
3053+
"string and undefinedAsNull",
3054+
{
3055+
stringTypeAndUndefinedAsNull: 1,
3056+
},
3057+
(msg) => expect(msg).toMatchSnapshot()
3058+
);
29903059
});

0 commit comments

Comments
 (0)