Skip to content

Commit

Permalink
fix: backport old logic from v3
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Dec 11, 2024
1 parent 284cd65 commit 8a5ec28
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 15 deletions.
8 changes: 4 additions & 4 deletions declarations/validate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ export type JSONSchema6 = import("json-schema").JSONSchema6;
export type JSONSchema7 = import("json-schema").JSONSchema7;
export type ErrorObject = import("ajv").ErrorObject;
export type Extend = {
formatMinimum?: string | undefined;
formatMaximum?: string | undefined;
formatExclusiveMinimum?: string | undefined;
formatExclusiveMaximum?: string | undefined;
formatMinimum?: (string | number) | undefined;
formatMaximum?: (string | number) | undefined;
formatExclusiveMinimum?: (string | boolean) | undefined;
formatExclusiveMaximum?: (string | boolean) | undefined;
link?: string | undefined;
undefinedAsNull?: boolean | undefined;
};
Expand Down
158 changes: 158 additions & 0 deletions src/keywords/limit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/** @typedef {import("ajv").default} Ajv */
/** @typedef {import("ajv").Code} Code */
/** @typedef {import("ajv").Name} Name */
/** @typedef {import("ajv").KeywordErrorDefinition} KeywordErrorDefinition */

/**
* @param {Ajv} ajv
* @returns {Ajv}
*/
function addLimitKeyword(ajv) {
// eslint-disable-next-line global-require
const { _, str, KeywordCxt, nil, Name } = require("ajv");

/**
* @param {Code | Name} x
* @returns {Code | Name}
*/
function par(x) {
return x instanceof Name ? x : _`(${x})`;
}

/**
* @param {Code} op
* @returns {function(Code, Code): Code}
*/
function mappend(op) {
return (x, y) =>
x === nil ? y : y === nil ? x : _`${par(x)} ${op} ${par(y)}`;
}

const orCode = mappend(_`||`);

// boolean OR (||) expression with the passed arguments
/**
* @param {...Code} args
* @returns {Code}
*/
function or(...args) {
return args.reduce(orCode);
}

/**
* @param {string | number} key
* @returns {Code}
*/
function getProperty(key) {
return _`[${key}]`;
}

const keywords = {
formatMaximum: { okStr: "<=", ok: _`<=`, fail: _`>` },
formatMinimum: { okStr: ">=", ok: _`>=`, fail: _`<` },
formatExclusiveMaximum: { okStr: "<", ok: _`<`, fail: _`>=` },
formatExclusiveMinimum: { okStr: ">", ok: _`>`, fail: _`<=` },
};

/** @type {KeywordErrorDefinition} */
const error = {
message: ({ keyword, schemaCode }) =>
str`should be ${
keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr
} ${schemaCode}`,
params: ({ keyword, schemaCode }) =>
_`{comparison: ${
keywords[/** @type {keyof typeof keywords} */ (keyword)].okStr
}, limit: ${schemaCode}}`,
};

for (const keyword of Object.keys(keywords)) {
ajv.addKeyword({
keyword,
type: "string",
schemaType: keyword.startsWith("formatExclusive")
? ["string", "boolean"]
: ["string", "number"],
$data: true,
error,
code(cxt) {
const { gen, data, schemaCode, keyword, it } = cxt;
const { opts, self } = it;
if (!opts.validateFormats) return;
const fCxt = new KeywordCxt(
it,
/** @type {any} */
(self.RULES.all.format).definition,
"format"
);

/**
* @param {Name} fmt
* @returns {Code}
*/
function compareCode(fmt) {
return _`${fmt}.compare(${data}, ${schemaCode}) ${
keywords[/** @type {keyof typeof keywords} */ (keyword)].fail
} 0`;
}

function validate$DataFormat() {
const fmts = gen.scopeValue("formats", {
ref: self.formats,
code: opts.code.formats,
});
const fmt = gen.const("fmt", _`${fmts}[${fCxt.schemaCode}]`);

cxt.fail$data(
or(
_`typeof ${fmt} != "object"`,
_`${fmt} instanceof RegExp`,
_`typeof ${fmt}.compare != "function"`,
compareCode(fmt)
)
);
}

function validateFormat() {
const format = fCxt.schema;
const fmtDef = self.formats[format];

if (!fmtDef || fmtDef === true) {
return;
}

if (
typeof fmtDef !== "object" ||
fmtDef instanceof RegExp ||
typeof fmtDef.compare !== "function"
) {
throw new Error(
`"${keyword}": format "${format}" does not define "compare" function`
);
}

const fmt = gen.scopeValue("formats", {
key: format,
ref: fmtDef,
code: opts.code.formats
? _`${opts.code.formats}${getProperty(format)}`
: undefined,
});

cxt.fail$data(compareCode(fmt));
}

if (fCxt.$data) {
validate$DataFormat();
} else {
validateFormat();
}
},
dependencies: ["format"],
});
}

return ajv;
}

export default addLimitKeyword;
16 changes: 11 additions & 5 deletions src/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ const getAjv = memoize(() => {
});

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

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

addAbsolutePathKeyword(ajv);

// eslint-disable-next-line global-require
const addLimitKeyword = require("./keywords/limit").default;

addLimitKeyword(ajv);

const addUndefinedAsNullKeyword =
// eslint-disable-next-line global-require
require("./keywords/undefinedAsNull").default;
Expand All @@ -45,10 +51,10 @@ const getAjv = memoize(() => {

/**
* @typedef {Object} Extend
* @property {string=} formatMinimum
* @property {string=} formatMaximum
* @property {string=} formatExclusiveMinimum
* @property {string=} formatExclusiveMaximum
* @property {(string | number)=} formatMinimum
* @property {(string | number)=} formatMaximum
* @property {(string | boolean)=} formatExclusiveMinimum
* @property {(string | boolean)=} formatExclusiveMaximum
* @property {string=} link
* @property {boolean=} undefinedAsNull
*/
Expand Down
Loading

0 comments on commit 8a5ec28

Please sign in to comment.