Skip to content

Commit 8a5ec28

Browse files
fix: backport old logic from v3
1 parent 284cd65 commit 8a5ec28

File tree

6 files changed

+241
-15
lines changed

6 files changed

+241
-15
lines changed

declarations/validate.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ export type JSONSchema6 = import("json-schema").JSONSchema6;
33
export type JSONSchema7 = import("json-schema").JSONSchema7;
44
export type ErrorObject = import("ajv").ErrorObject;
55
export type Extend = {
6-
formatMinimum?: string | undefined;
7-
formatMaximum?: string | undefined;
8-
formatExclusiveMinimum?: string | undefined;
9-
formatExclusiveMaximum?: string | undefined;
6+
formatMinimum?: (string | number) | undefined;
7+
formatMaximum?: (string | number) | undefined;
8+
formatExclusiveMinimum?: (string | boolean) | undefined;
9+
formatExclusiveMaximum?: (string | boolean) | undefined;
1010
link?: string | undefined;
1111
undefinedAsNull?: boolean | undefined;
1212
};

src/keywords/limit.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/** @typedef {import("ajv").default} Ajv */
2+
/** @typedef {import("ajv").Code} Code */
3+
/** @typedef {import("ajv").Name} Name */
4+
/** @typedef {import("ajv").KeywordErrorDefinition} KeywordErrorDefinition */
5+
6+
/**
7+
* @param {Ajv} ajv
8+
* @returns {Ajv}
9+
*/
10+
function addLimitKeyword(ajv) {
11+
// eslint-disable-next-line global-require
12+
const { _, str, KeywordCxt, nil, Name } = require("ajv");
13+
14+
/**
15+
* @param {Code | Name} x
16+
* @returns {Code | Name}
17+
*/
18+
function par(x) {
19+
return x instanceof Name ? x : _`(${x})`;
20+
}
21+
22+
/**
23+
* @param {Code} op
24+
* @returns {function(Code, Code): Code}
25+
*/
26+
function mappend(op) {
27+
return (x, y) =>
28+
x === nil ? y : y === nil ? x : _`${par(x)} ${op} ${par(y)}`;
29+
}
30+
31+
const orCode = mappend(_`||`);
32+
33+
// boolean OR (||) expression with the passed arguments
34+
/**
35+
* @param {...Code} args
36+
* @returns {Code}
37+
*/
38+
function or(...args) {
39+
return args.reduce(orCode);
40+
}
41+
42+
/**
43+
* @param {string | number} key
44+
* @returns {Code}
45+
*/
46+
function getProperty(key) {
47+
return _`[${key}]`;
48+
}
49+
50+
const keywords = {
51+
formatMaximum: { okStr: "<=", ok: _`<=`, fail: _`>` },
52+
formatMinimum: { okStr: ">=", ok: _`>=`, fail: _`<` },
53+
formatExclusiveMaximum: { okStr: "<", ok: _`<`, fail: _`>=` },
54+
formatExclusiveMinimum: { okStr: ">", ok: _`>`, fail: _`<=` },
55+
};
56+
57+
/** @type {KeywordErrorDefinition} */
58+
const error = {
59+
message: ({ keyword, schemaCode }) =>
60+
str`should be ${
61+
keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr
62+
} ${schemaCode}`,
63+
params: ({ keyword, schemaCode }) =>
64+
_`{comparison: ${
65+
keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr
66+
}, limit: ${schemaCode}}`,
67+
};
68+
69+
for (const keyword of Object.keys(keywords)) {
70+
ajv.addKeyword({
71+
keyword,
72+
type: "string",
73+
schemaType: keyword.startsWith("formatExclusive")
74+
? ["string", "boolean"]
75+
: ["string", "number"],
76+
$data: true,
77+
error,
78+
code(cxt) {
79+
const { gen, data, schemaCode, keyword, it } = cxt;
80+
const { opts, self } = it;
81+
if (!opts.validateFormats) return;
82+
const fCxt = new KeywordCxt(
83+
it,
84+
/** @type {any} */
85+
(self.RULES.all.format).definition,
86+
"format"
87+
);
88+
89+
/**
90+
* @param {Name} fmt
91+
* @returns {Code}
92+
*/
93+
function compareCode(fmt) {
94+
return _`${fmt}.compare(${data}, ${schemaCode}) ${
95+
keywords[/** @type {keyof typeof keywords} */ (keyword)].fail
96+
} 0`;
97+
}
98+
99+
function validate$DataFormat() {
100+
const fmts = gen.scopeValue("formats", {
101+
ref: self.formats,
102+
code: opts.code.formats,
103+
});
104+
const fmt = gen.const("fmt", _`${fmts}[${fCxt.schemaCode}]`);
105+
106+
cxt.fail$data(
107+
or(
108+
_`typeof ${fmt} != "object"`,
109+
_`${fmt} instanceof RegExp`,
110+
_`typeof ${fmt}.compare != "function"`,
111+
compareCode(fmt)
112+
)
113+
);
114+
}
115+
116+
function validateFormat() {
117+
const format = fCxt.schema;
118+
const fmtDef = self.formats[format];
119+
120+
if (!fmtDef || fmtDef === true) {
121+
return;
122+
}
123+
124+
if (
125+
typeof fmtDef !== "object" ||
126+
fmtDef instanceof RegExp ||
127+
typeof fmtDef.compare !== "function"
128+
) {
129+
throw new Error(
130+
`"${keyword}": format "${format}" does not define "compare" function`
131+
);
132+
}
133+
134+
const fmt = gen.scopeValue("formats", {
135+
key: format,
136+
ref: fmtDef,
137+
code: opts.code.formats
138+
? _`${opts.code.formats}${getProperty(format)}`
139+
: undefined,
140+
});
141+
142+
cxt.fail$data(compareCode(fmt));
143+
}
144+
145+
if (fCxt.$data) {
146+
validate$DataFormat();
147+
} else {
148+
validateFormat();
149+
}
150+
},
151+
dependencies: ["format"],
152+
});
153+
}
154+
155+
return ajv;
156+
}
157+
158+
export default addLimitKeyword;

src/validate.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,20 @@ const getAjv = memoize(() => {
2121
});
2222

2323
ajvKeywords(ajv, ["instanceof", "patternRequired"]);
24-
addFormats(ajv, { keywords: true });
24+
// TODO set `{ keywords: true }` for the next major release and remove `keywords/limit.js`
25+
addFormats(ajv, { keywords: false });
2526

2627
// Custom keywords
2728
// eslint-disable-next-line global-require
2829
const addAbsolutePathKeyword = require("./keywords/absolutePath").default;
2930

3031
addAbsolutePathKeyword(ajv);
3132

33+
// eslint-disable-next-line global-require
34+
const addLimitKeyword = require("./keywords/limit").default;
35+
36+
addLimitKeyword(ajv);
37+
3238
const addUndefinedAsNullKeyword =
3339
// eslint-disable-next-line global-require
3440
require("./keywords/undefinedAsNull").default;
@@ -45,10 +51,10 @@ const getAjv = memoize(() => {
4551

4652
/**
4753
* @typedef {Object} Extend
48-
* @property {string=} formatMinimum
49-
* @property {string=} formatMaximum
50-
* @property {string=} formatExclusiveMinimum
51-
* @property {string=} formatExclusiveMaximum
54+
* @property {(string | number)=} formatMinimum
55+
* @property {(string | number)=} formatMaximum
56+
* @property {(string | boolean)=} formatExclusiveMinimum
57+
* @property {(string | boolean)=} formatExclusiveMaximum
5258
* @property {string=} link
5359
* @property {boolean=} undefinedAsNull
5460
*/

0 commit comments

Comments
 (0)