mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(schema): num no longer takes nan and infinity
This commit is contained in:
parent
ccff9876e7
commit
55a5c69145
5 changed files with 34 additions and 8 deletions
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- A `number` or `integer` no longer accepts `NaN`, `Infinity` or `-Infinity`, as in TypeBox; before, an unbounded `num()` accepted them. JSON has neither, so only values built in code are affected. Errors name the value (`expected number, got NaN`).
|
||||
- The third-party licenses are in `THIRD_PARTY_NOTICES/`, renamed from `LICENSES/`, which Vite's license notices read as the package's license file, failing browser builds that bundle the package.
|
||||
- A required property set to `undefined` is valid when its schema takes `undefined` (a union with `Type.Undefined()`, `Type.Unknown()`, `Type.Any()`), as in TypeBox; before, it was always missing. An optional one set to `undefined` still counts as absent.
|
||||
- Fixed local `$ref`s in schemas whose root `$id` is a relative path with a directory, such as `schemas/tool.json`: the `$id` was applied twice, so `compile` threw and `unresolvedRefs: "reject"` rejected valid tool arguments. Relative `$id`s are now also applied once with `unevaluatedProperties`/`unevaluatedItems`, when explaining errors under nested resources, and on the first coercion of a schema with `$dynamicRef` ([#11697](https://github.com/earendil-works/pi/issues/11697))
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Check } from "./types.ts";
|
|||
export const ALWAYS: Check = () => true;
|
||||
export const NEVER: Check = () => false;
|
||||
const IS_STRING: Check = (v) => typeof v === "string";
|
||||
const IS_NUMBER: Check = (v) => typeof v === "number";
|
||||
const IS_NUMBER: Check = (v) => Number.isFinite(v);
|
||||
const IS_INTEGER: Check = (v) => Number.isInteger(v);
|
||||
const IS_BOOLEAN: Check = (v) => typeof v === "boolean";
|
||||
const IS_NULL: Check = (v) => v === null;
|
||||
|
|
@ -24,7 +24,8 @@ export function isObject(v: unknown): v is Record<string, unknown> {
|
|||
|
||||
/**
|
||||
* The check of a `type` name. `bytes` (a Uint8Array), `bigint` (TypeBox's `Type.BigInt`) and `undefined` (TypeBox's
|
||||
* `Type.Undefined`) extend JSON Schema for values that never pass through JSON.
|
||||
* `Type.Undefined`) extend JSON Schema for values that never pass through JSON. A `number` is finite, as in TypeBox:
|
||||
* JSON has no NaN or Infinity, so only a value built in code can be one.
|
||||
*/
|
||||
export const TYPE_CHECKS: ReadonlyMap<string, Check> = new Map([
|
||||
["string", IS_STRING],
|
||||
|
|
@ -85,7 +86,7 @@ function leafOk(code: number, x: unknown, arg: unknown): boolean {
|
|||
case 1:
|
||||
return typeof x === "string";
|
||||
case 2:
|
||||
return typeof x === "number";
|
||||
return Number.isFinite(x);
|
||||
case 3:
|
||||
return Number.isInteger(x);
|
||||
case 4:
|
||||
|
|
@ -104,7 +105,7 @@ function leafOk(code: number, x: unknown, arg: unknown): boolean {
|
|||
case 10:
|
||||
return Number.isInteger(x) && inBounds(x as number, arg as Bounds);
|
||||
case 11:
|
||||
return typeof x === "number" && inBounds(x, arg as Bounds);
|
||||
return Number.isFinite(x) && inBounds(x as number, arg as Bounds);
|
||||
case 12:
|
||||
return typeof x === "string" && lengthOk(x, (arg as Bounds).min, (arg as Bounds).max);
|
||||
case 13:
|
||||
|
|
@ -193,13 +194,13 @@ export function typeMask(types: readonly unknown[]): number {
|
|||
return mask;
|
||||
}
|
||||
|
||||
/** The type bits of `v`: an integer has the bits of `number` and `integer`. */
|
||||
/** The type bits of `v`: an integer has the bits of `number` and `integer`; NaN and Infinity have neither. */
|
||||
function typeBits(v: unknown): number {
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
return 1;
|
||||
case "number":
|
||||
return Number.isInteger(v) ? 6 : 2;
|
||||
return Number.isInteger(v) ? 6 : Number.isFinite(v) ? 2 : 0;
|
||||
case "boolean":
|
||||
return 8;
|
||||
case "object":
|
||||
|
|
@ -292,7 +293,7 @@ export function numberCheck(
|
|||
): Check {
|
||||
const check: Check = (v) => {
|
||||
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 === 3 ? typeof v !== "bigint" : mode !== 0 && !Number.isFinite(v)) return false;
|
||||
if (mode === 2 && !Number.isInteger(v)) return false;
|
||||
return v >= min && v <= max && v > xmin && v < xmax && (multipleOf === undefined || isMultiple(v, multipleOf));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ function typesOf(schema: SchemaObject): string[] {
|
|||
function matchesType(value: unknown, type: string): boolean {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return typeof value === "number";
|
||||
return Number.isFinite(value);
|
||||
case "integer":
|
||||
return typeof value === "number" && Number.isInteger(value);
|
||||
case "boolean":
|
||||
|
|
|
|||
|
|
@ -394,5 +394,6 @@ function typeName(v: unknown): string {
|
|||
if (v instanceof Uint8Array) return "bytes";
|
||||
if (typeof v === "bigint") return "bigint";
|
||||
if (typeof v === "number" && Number.isInteger(v)) return "integer";
|
||||
if (typeof v === "number" && !Number.isFinite(v)) return String(v);
|
||||
return typeof v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// compile() and Validator: checks, errors, values, options and schemas from other sources.
|
||||
|
||||
import Type from "typebox";
|
||||
import { Check } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formats } from "../src/formats/index.ts";
|
||||
import { compile } from "../src/index.ts";
|
||||
|
|
@ -109,6 +110,28 @@ describe("values", () => {
|
|||
expect(compile({ type: "constructor" }).check(1)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects NaN and Infinity as a number or an integer, with or without bounds, as TypeBox does", () => {
|
||||
const schemas = [
|
||||
{ type: "number" },
|
||||
{ type: "integer" },
|
||||
{ type: "number", minimum: 0 },
|
||||
{ type: ["number", "string"] },
|
||||
{ anyOf: [{ type: "number" }, { type: "null" }] },
|
||||
{ type: "object", properties: { a: { type: "number" } } },
|
||||
];
|
||||
for (const schema of schemas) {
|
||||
const { check } = compile(schema);
|
||||
for (const v of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
|
||||
const value = schema.type === "object" ? { a: v } : v;
|
||||
expect(check(value), `${JSON.stringify(schema)}: ${v}`).toBe(false);
|
||||
expect(check(value)).toBe(Check(schema, value));
|
||||
}
|
||||
}
|
||||
expect(compile({ type: "number" }).errors(Number.NaN)).toEqual([
|
||||
{ path: "", keyword: "type", message: "expected number, got NaN" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("checks a detached check function", () => {
|
||||
const { check } = compile(read);
|
||||
expect([{ path: "a" }, { path: 1 }].filter(check)).toEqual([{ path: "a" }]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue