mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
refactor(schema): check bigints with the number keywords, as TypeBox does
This commit is contained in:
parent
4b9d9dc4a8
commit
63aeddc2e9
3 changed files with 56 additions and 83 deletions
|
|
@ -224,39 +224,6 @@ export function maskedAnyCheck(mask: number, checks: readonly Check[]): Check {
|
|||
return (v) => (typeBits(v) & mask) !== 0 || rest(v);
|
||||
}
|
||||
|
||||
/** A bigint within the bounds (bigint `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`). */
|
||||
export function bigintCheck(bounds: BigintBounds): Check {
|
||||
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = bounds;
|
||||
return (v) =>
|
||||
typeof v === "bigint" &&
|
||||
(minimum === undefined || v >= minimum) &&
|
||||
(maximum === undefined || v <= maximum) &&
|
||||
(exclusiveMinimum === undefined || v > exclusiveMinimum) &&
|
||||
(exclusiveMaximum === undefined || v < exclusiveMaximum) &&
|
||||
(multipleOf === undefined || multipleOf === 0n || v % multipleOf === 0n);
|
||||
}
|
||||
|
||||
/** A `bigint` schema's bounds: each keyword when its value is a bigint, as `Type.BigInt` writes them. */
|
||||
export type BigintBounds = {
|
||||
readonly minimum: bigint | undefined;
|
||||
readonly maximum: bigint | undefined;
|
||||
readonly exclusiveMinimum: bigint | undefined;
|
||||
readonly exclusiveMaximum: bigint | undefined;
|
||||
readonly multipleOf: bigint | undefined;
|
||||
};
|
||||
|
||||
/** The bigint bounds of `node`. */
|
||||
export function bigintBounds(node: Readonly<Record<string, unknown>>): BigintBounds {
|
||||
const value = (key: string) => (typeof node[key] === "bigint" ? (node[key] as bigint) : undefined);
|
||||
return {
|
||||
minimum: value("minimum"),
|
||||
maximum: value("maximum"),
|
||||
exclusiveMinimum: value("exclusiveMinimum"),
|
||||
exclusiveMaximum: value("exclusiveMaximum"),
|
||||
multipleOf: value("multipleOf"),
|
||||
};
|
||||
}
|
||||
|
||||
/** A Uint8Array within the length bounds. */
|
||||
export function bytesCheck(min: number, max: number): Check {
|
||||
return (v) => v instanceof Uint8Array && v.length >= min && v.length <= max;
|
||||
|
|
@ -308,26 +275,42 @@ export function stringCheck(
|
|||
};
|
||||
}
|
||||
|
||||
/** The number keywords. `mode`: 0 passes other types, 1 requires a number, 2 an integer. */
|
||||
/** A number bound: a number, or a bigint (TypeBox's `Type.BigInt` writes its bounds as bigints). */
|
||||
export type Bound = number | bigint;
|
||||
|
||||
/**
|
||||
* The number keywords, for numbers and bigints alike (as TypeBox applies them): bounds of either kind compare with
|
||||
* values of either kind. `mode`: 0 passes other types, 1 requires a number, 2 an integer, 3 a bigint.
|
||||
*/
|
||||
export function numberCheck(
|
||||
mode: 0 | 1 | 2,
|
||||
min: number,
|
||||
max: number,
|
||||
xmin: number,
|
||||
xmax: number,
|
||||
multipleOf: number | undefined,
|
||||
mode: 0 | 1 | 2 | 3,
|
||||
min: Bound,
|
||||
max: Bound,
|
||||
xmin: Bound,
|
||||
xmax: Bound,
|
||||
multipleOf: Bound | undefined,
|
||||
): Check {
|
||||
const check: Check = (v) => {
|
||||
if (typeof v !== "number") return mode === 0;
|
||||
if (typeof v !== "number" && typeof v !== "bigint") return mode === 0;
|
||||
if (mode === 3 ? typeof v !== "bigint" : mode !== 0 && typeof v !== "number") return false;
|
||||
if (mode === 2 && !Number.isInteger(v)) return false;
|
||||
return v >= min && v <= max && v > xmin && v < xmax && (multipleOf === undefined || isMultiple(v, multipleOf));
|
||||
};
|
||||
if (mode === 0 || multipleOf !== undefined) return check;
|
||||
const plain =
|
||||
typeof min === "number" && typeof max === "number" && typeof xmin === "number" && typeof xmax === "number";
|
||||
if (mode === 0 || mode === 3 || multipleOf !== undefined || !plain) return check;
|
||||
return leaf(mode === 2 ? 10 : 11, new Bounds(min, max, xmin, xmax), check);
|
||||
}
|
||||
|
||||
/** Whether `v` is a multiple of `d`, exactly for decimals: 0.0075 / 0.0001 is 74.99999999999999 in floating point. */
|
||||
export function isMultiple(v: number, d: number): boolean {
|
||||
/**
|
||||
* Whether `v` is a multiple of `d`, exactly for decimals: 0.0075 / 0.0001 is 74.99999999999999 in floating point. With
|
||||
* a bigint on either side, exact integer arithmetic; a bigint is no multiple of 0 or of a fraction.
|
||||
*/
|
||||
export function isMultiple(v: Bound, d: Bound): boolean {
|
||||
if (typeof v === "bigint" || typeof d === "bigint") {
|
||||
if (!Number.isInteger(Number(v)) || !Number.isInteger(Number(d)) || BigInt(d) === 0n) return false;
|
||||
return BigInt(v) % BigInt(d) === 0n;
|
||||
}
|
||||
const q = v / d;
|
||||
if (Number.isInteger(q)) return true;
|
||||
if (!Number.isFinite(q)) return v % d === 0;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import {
|
|||
ALWAYS,
|
||||
and,
|
||||
anyCheck,
|
||||
bigintBounds,
|
||||
bigintCheck,
|
||||
type Bound,
|
||||
bytesCheck,
|
||||
constCheck,
|
||||
containsCheck,
|
||||
|
|
@ -148,14 +147,12 @@ export class Compiler {
|
|||
if (single === "bytes") {
|
||||
parts.push(bytesCheck(number(node.minLength) ?? 0, number(node.maxLength) ?? Number.POSITIVE_INFINITY));
|
||||
typeInGroup = true;
|
||||
} else if (single === "bigint") {
|
||||
parts.push(bigintCheck(bigintBounds(node)));
|
||||
typeInGroup = true;
|
||||
} else {
|
||||
const string = this.stringGroup(node, single === "string");
|
||||
if (string !== undefined) parts.push(string);
|
||||
const numeric = single === "number" || single === "integer";
|
||||
const number = this.numberGroup(node, single === "number" ? 1 : single === "integer" ? 2 : 0);
|
||||
const numeric = single === "number" || single === "integer" || single === "bigint";
|
||||
const mode = single === "number" ? 1 : single === "integer" ? 2 : single === "bigint" ? 3 : 0;
|
||||
const number = this.numberGroup(node, mode);
|
||||
if (number !== undefined) parts.push(number);
|
||||
const object = this.objectGroup(node, base, single === "object");
|
||||
if (object !== undefined) parts.push(object);
|
||||
|
|
@ -288,12 +285,12 @@ export class Compiler {
|
|||
return stringCheck(strict, min, max, re, format);
|
||||
}
|
||||
|
||||
private numberGroup(node: SchemaObject, mode: 0 | 1 | 2): Check | undefined {
|
||||
const min = number(node.minimum);
|
||||
const max = number(node.maximum);
|
||||
const xmin = number(node.exclusiveMinimum);
|
||||
const xmax = number(node.exclusiveMaximum);
|
||||
const multipleOf = number(node.multipleOf);
|
||||
private numberGroup(node: SchemaObject, mode: 0 | 1 | 2 | 3): Check | undefined {
|
||||
const min = bound(node.minimum);
|
||||
const max = bound(node.maximum);
|
||||
const xmin = bound(node.exclusiveMinimum);
|
||||
const xmax = bound(node.exclusiveMaximum);
|
||||
const multipleOf = bound(node.multipleOf);
|
||||
if ([min, max, xmin, xmax, multipleOf].every((x) => x === undefined)) return undefined;
|
||||
const inf = Number.POSITIVE_INFINITY;
|
||||
return numberCheck(mode, min ?? -inf, max ?? inf, xmin ?? -inf, xmax ?? inf, multipleOf);
|
||||
|
|
@ -617,6 +614,11 @@ export function requiredKeys(node: SchemaObject, properties: SchemaObject): stri
|
|||
});
|
||||
}
|
||||
|
||||
/** A number keyword's value: a number, or a bigint (TypeBox's `Type.BigInt` bounds). */
|
||||
function bound(value: unknown): Bound | undefined {
|
||||
return typeof value === "number" || typeof value === "bigint" ? value : undefined;
|
||||
}
|
||||
|
||||
function number(value: unknown): number | undefined {
|
||||
return typeof value === "number" ? value : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { newAnnotations } from "./annotations.ts";
|
||||
import {
|
||||
bigintBounds,
|
||||
type Bound,
|
||||
codePoints,
|
||||
constCheck,
|
||||
containsCheck,
|
||||
|
|
@ -83,8 +83,7 @@ export class Explainer {
|
|||
push("const", `expected ${JSON.stringify(node.const)}`);
|
||||
if (typeof v === "string" && node.type !== "bytes") this.walkString(node, v, push);
|
||||
if (v instanceof Uint8Array && node.type === "bytes") this.walkBytes(node, v, push);
|
||||
if (typeof v === "number") this.walkNumber(node, v, push);
|
||||
if (typeof v === "bigint" && node.type === "bigint") this.walkBigint(node, v, push);
|
||||
if (typeof v === "number" || typeof v === "bigint") this.walkNumber(node, v, push);
|
||||
if (isObject(v)) this.walkObject(node, b, v, path, out, push);
|
||||
if (Array.isArray(v)) this.walkArray(node, b, v, path, out, push);
|
||||
if (Array.isArray(node.allOf)) for (const s of node.allOf) this.walk(s, b, v, path, out);
|
||||
|
|
@ -159,31 +158,16 @@ export class Explainer {
|
|||
}
|
||||
}
|
||||
|
||||
private walkNumber(node: SchemaObject, v: number, push: Push): void {
|
||||
if (typeof node.minimum === "number" && v < node.minimum) push("minimum", `expected >= ${node.minimum}`);
|
||||
if (typeof node.maximum === "number" && v > node.maximum) push("maximum", `expected <= ${node.maximum}`);
|
||||
if (typeof node.exclusiveMinimum === "number" && v <= node.exclusiveMinimum) {
|
||||
push("exclusiveMinimum", `expected > ${node.exclusiveMinimum}`);
|
||||
}
|
||||
if (typeof node.exclusiveMaximum === "number" && v >= node.exclusiveMaximum) {
|
||||
push("exclusiveMaximum", `expected < ${node.exclusiveMaximum}`);
|
||||
}
|
||||
if (typeof node.multipleOf === "number" && !isMultiple(v, node.multipleOf)) {
|
||||
push("multipleOf", `expected a multiple of ${node.multipleOf}`);
|
||||
}
|
||||
}
|
||||
|
||||
private walkBigint(node: SchemaObject, v: bigint, push: Push): void {
|
||||
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = bigintBounds(node);
|
||||
if (minimum !== undefined && v < minimum) push("minimum", `expected >= ${minimum}`);
|
||||
if (maximum !== undefined && v > maximum) push("maximum", `expected <= ${maximum}`);
|
||||
if (exclusiveMinimum !== undefined && v <= exclusiveMinimum)
|
||||
// Numbers and bigints alike: bounds of either kind compare with values of either kind.
|
||||
private walkNumber(node: SchemaObject, v: Bound, push: Push): void {
|
||||
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = node;
|
||||
if (isBound(minimum) && v < minimum) push("minimum", `expected >= ${minimum}`);
|
||||
if (isBound(maximum) && v > maximum) push("maximum", `expected <= ${maximum}`);
|
||||
if (isBound(exclusiveMinimum) && v <= exclusiveMinimum)
|
||||
push("exclusiveMinimum", `expected > ${exclusiveMinimum}`);
|
||||
if (exclusiveMaximum !== undefined && v >= exclusiveMaximum)
|
||||
if (isBound(exclusiveMaximum) && v >= exclusiveMaximum)
|
||||
push("exclusiveMaximum", `expected < ${exclusiveMaximum}`);
|
||||
if (multipleOf !== undefined && multipleOf !== 0n && v % multipleOf !== 0n) {
|
||||
push("multipleOf", `expected a multiple of ${multipleOf}`);
|
||||
}
|
||||
if (isBound(multipleOf) && !isMultiple(v, multipleOf)) push("multipleOf", `expected a multiple of ${multipleOf}`);
|
||||
}
|
||||
|
||||
private walkObject(
|
||||
|
|
@ -392,6 +376,10 @@ function containsMessage(min: number, max: number): string {
|
|||
return `expected between ${min} and ${max} matching items`;
|
||||
}
|
||||
|
||||
function isBound(value: unknown): value is Bound {
|
||||
return typeof value === "number" || typeof value === "bigint";
|
||||
}
|
||||
|
||||
function pointer(path: string, key: string | number): string {
|
||||
return `${path}/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue