anyvali 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +370 -0
- package/VERSION +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/format/validators.d.ts +2 -0
- package/dist/format/validators.d.ts.map +1 -0
- package/dist/format/validators.js +57 -0
- package/dist/format/validators.js.map +1 -0
- package/dist/forms/index.d.ts +57 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +586 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +8 -0
- package/dist/infer.d.ts.map +1 -0
- package/dist/infer.js +2 -0
- package/dist/infer.js.map +1 -0
- package/dist/interchange/document.d.ts +5 -0
- package/dist/interchange/document.d.ts.map +1 -0
- package/dist/interchange/document.js +12 -0
- package/dist/interchange/document.js.map +1 -0
- package/dist/interchange/exporter.d.ts +7 -0
- package/dist/interchange/exporter.d.ts.map +1 -0
- package/dist/interchange/exporter.js +7 -0
- package/dist/interchange/exporter.js.map +1 -0
- package/dist/interchange/importer.d.ts +4 -0
- package/dist/interchange/importer.d.ts.map +1 -0
- package/dist/interchange/importer.js +229 -0
- package/dist/interchange/importer.js.map +1 -0
- package/dist/issue-codes.d.ts +19 -0
- package/dist/issue-codes.d.ts.map +1 -0
- package/dist/issue-codes.js +18 -0
- package/dist/issue-codes.js.map +1 -0
- package/dist/parse/coerce.d.ts +16 -0
- package/dist/parse/coerce.d.ts.map +1 -0
- package/dist/parse/coerce.js +115 -0
- package/dist/parse/coerce.js.map +1 -0
- package/dist/parse/defaults.d.ts +7 -0
- package/dist/parse/defaults.d.ts.map +1 -0
- package/dist/parse/defaults.js +12 -0
- package/dist/parse/defaults.js.map +1 -0
- package/dist/parse/parser.d.ts +11 -0
- package/dist/parse/parser.d.ts.map +1 -0
- package/dist/parse/parser.js +13 -0
- package/dist/parse/parser.js.map +1 -0
- package/dist/schemas/any.d.ts +7 -0
- package/dist/schemas/any.d.ts.map +1 -0
- package/dist/schemas/any.js +12 -0
- package/dist/schemas/any.js.map +1 -0
- package/dist/schemas/array.d.ts +13 -0
- package/dist/schemas/array.d.ts.map +1 -0
- package/dist/schemas/array.js +73 -0
- package/dist/schemas/array.js.map +1 -0
- package/dist/schemas/base.d.ts +37 -0
- package/dist/schemas/base.d.ts.map +1 -0
- package/dist/schemas/base.js +285 -0
- package/dist/schemas/base.js.map +1 -0
- package/dist/schemas/bool.d.ts +8 -0
- package/dist/schemas/bool.d.ts.map +1 -0
- package/dist/schemas/bool.js +27 -0
- package/dist/schemas/bool.js.map +1 -0
- package/dist/schemas/enum.d.ts +9 -0
- package/dist/schemas/enum.d.ts.map +1 -0
- package/dist/schemas/enum.js +31 -0
- package/dist/schemas/enum.js.map +1 -0
- package/dist/schemas/index.d.ts +21 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +21 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/int.d.ts +32 -0
- package/dist/schemas/int.d.ts.map +1 -0
- package/dist/schemas/int.js +108 -0
- package/dist/schemas/int.js.map +1 -0
- package/dist/schemas/intersection.d.ts +16 -0
- package/dist/schemas/intersection.d.ts.map +1 -0
- package/dist/schemas/intersection.js +58 -0
- package/dist/schemas/intersection.js.map +1 -0
- package/dist/schemas/literal.d.ts +11 -0
- package/dist/schemas/literal.d.ts.map +1 -0
- package/dist/schemas/literal.js +28 -0
- package/dist/schemas/literal.js.map +1 -0
- package/dist/schemas/never.d.ts +7 -0
- package/dist/schemas/never.d.ts.map +1 -0
- package/dist/schemas/never.js +19 -0
- package/dist/schemas/never.js.map +1 -0
- package/dist/schemas/null.d.ts +7 -0
- package/dist/schemas/null.d.ts.map +1 -0
- package/dist/schemas/null.js +24 -0
- package/dist/schemas/null.js.map +1 -0
- package/dist/schemas/nullable.d.ts +10 -0
- package/dist/schemas/nullable.d.ts.map +1 -0
- package/dist/schemas/nullable.js +29 -0
- package/dist/schemas/nullable.js.map +1 -0
- package/dist/schemas/number.d.ts +27 -0
- package/dist/schemas/number.d.ts.map +1 -0
- package/dist/schemas/number.js +134 -0
- package/dist/schemas/number.js.map +1 -0
- package/dist/schemas/object.d.ts +28 -0
- package/dist/schemas/object.d.ts.map +1 -0
- package/dist/schemas/object.js +153 -0
- package/dist/schemas/object.js.map +1 -0
- package/dist/schemas/optional.d.ts +11 -0
- package/dist/schemas/optional.d.ts.map +1 -0
- package/dist/schemas/optional.js +39 -0
- package/dist/schemas/optional.js.map +1 -0
- package/dist/schemas/record.d.ts +9 -0
- package/dist/schemas/record.d.ts.map +1 -0
- package/dist/schemas/record.js +45 -0
- package/dist/schemas/record.js.map +1 -0
- package/dist/schemas/ref.d.ts +10 -0
- package/dist/schemas/ref.d.ts.map +1 -0
- package/dist/schemas/ref.js +30 -0
- package/dist/schemas/ref.js.map +1 -0
- package/dist/schemas/string.d.ts +29 -0
- package/dist/schemas/string.d.ts.map +1 -0
- package/dist/schemas/string.js +181 -0
- package/dist/schemas/string.js.map +1 -0
- package/dist/schemas/tuple.d.ts +14 -0
- package/dist/schemas/tuple.d.ts.map +1 -0
- package/dist/schemas/tuple.js +59 -0
- package/dist/schemas/tuple.js.map +1 -0
- package/dist/schemas/union.d.ts +9 -0
- package/dist/schemas/union.d.ts.map +1 -0
- package/dist/schemas/union.js +45 -0
- package/dist/schemas/union.js.map +1 -0
- package/dist/schemas/unknown.d.ts +7 -0
- package/dist/schemas/unknown.d.ts.map +1 -0
- package/dist/schemas/unknown.js +12 -0
- package/dist/schemas/unknown.js.map +1 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +12 -0
- package/dist/util.js.map +1 -0
- package/package.json +41 -0
- package/sdk/js/CHANGELOG.md +13 -0
- package/src/errors.ts +17 -0
- package/src/format/validators.ts +71 -0
- package/src/forms/index.ts +789 -0
- package/src/index.ts +285 -0
- package/src/infer.ts +12 -0
- package/src/interchange/document.ts +18 -0
- package/src/interchange/exporter.ts +12 -0
- package/src/interchange/importer.ts +285 -0
- package/src/issue-codes.ts +19 -0
- package/src/parse/coerce.ts +133 -0
- package/src/parse/defaults.ts +15 -0
- package/src/parse/parser.ts +19 -0
- package/src/schemas/any.ts +14 -0
- package/src/schemas/array.ts +83 -0
- package/src/schemas/base.ts +322 -0
- package/src/schemas/bool.ts +30 -0
- package/src/schemas/enum.ts +37 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/int.ts +129 -0
- package/src/schemas/intersection.ts +81 -0
- package/src/schemas/literal.ts +34 -0
- package/src/schemas/never.ts +21 -0
- package/src/schemas/null.ts +26 -0
- package/src/schemas/nullable.ts +36 -0
- package/src/schemas/number.ts +151 -0
- package/src/schemas/object.ts +203 -0
- package/src/schemas/optional.ts +49 -0
- package/src/schemas/record.ts +55 -0
- package/src/schemas/ref.ts +35 -0
- package/src/schemas/string.ts +192 -0
- package/src/schemas/tuple.ts +74 -0
- package/src/schemas/union.ts +53 -0
- package/src/schemas/unknown.ts +14 -0
- package/src/types.ts +239 -0
- package/src/util.ts +9 -0
- package/tests/conformance/runner.test.ts +28 -0
- package/tests/conformance/runner.ts +137 -0
- package/tests/forms.test.ts +146 -0
- package/tests/unit/coerce.test.ts +136 -0
- package/tests/unit/collections.test.ts +99 -0
- package/tests/unit/composition.test.ts +80 -0
- package/tests/unit/date-format.test.ts +18 -0
- package/tests/unit/default-mutation.test.ts +32 -0
- package/tests/unit/defaults.test.ts +49 -0
- package/tests/unit/errors.test.ts +53 -0
- package/tests/unit/export.test.ts +270 -0
- package/tests/unit/inference.test.ts +306 -0
- package/tests/unit/interchange.test.ts +191 -0
- package/tests/unit/number.test.ts +195 -0
- package/tests/unit/object.test.ts +208 -0
- package/tests/unit/parser.test.ts +151 -0
- package/tests/unit/primitives.test.ts +111 -0
- package/tests/unit/security-recursion.test.ts +105 -0
- package/tests/unit/security.test.ts +945 -0
- package/tests/unit/shared-ref-falsepos.test.ts +33 -0
- package/tests/unit/string-pattern-redos.test.ts +46 -0
- package/tests/unit/string.test.ts +147 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode, SchemaKind } from "../types.js";
|
|
2
|
+
import { NumberSchema } from "./number.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
|
|
6
|
+
interface IntRange {
|
|
7
|
+
min: number;
|
|
8
|
+
max: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const INT_RANGES: Record<string, IntRange> = {
|
|
12
|
+
int8: { min: -128, max: 127 },
|
|
13
|
+
int16: { min: -32768, max: 32767 },
|
|
14
|
+
int32: { min: -2147483648, max: 2147483647 },
|
|
15
|
+
int64: { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
|
|
16
|
+
uint8: { min: 0, max: 255 },
|
|
17
|
+
uint16: { min: 0, max: 65535 },
|
|
18
|
+
uint32: { min: 0, max: 4294967295 },
|
|
19
|
+
uint64: { min: 0, max: Number.MAX_SAFE_INTEGER },
|
|
20
|
+
int: { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class IntSchema extends NumberSchema {
|
|
24
|
+
private _intRange: IntRange;
|
|
25
|
+
|
|
26
|
+
constructor(kind: SchemaKind = "int") {
|
|
27
|
+
super(kind);
|
|
28
|
+
this._intRange = INT_RANGES[kind] ?? INT_RANGES.int;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
32
|
+
if (typeof input !== "number" || !Number.isFinite(input)) {
|
|
33
|
+
ctx.issues.push({
|
|
34
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
35
|
+
message: `Expected integer, received ${describeType(input)}`,
|
|
36
|
+
path: [...ctx.path],
|
|
37
|
+
expected: this._kind,
|
|
38
|
+
received: describeType(input),
|
|
39
|
+
});
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!Number.isInteger(input)) {
|
|
44
|
+
ctx.issues.push({
|
|
45
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
46
|
+
message: `Expected integer, received float`,
|
|
47
|
+
path: [...ctx.path],
|
|
48
|
+
expected: this._kind,
|
|
49
|
+
received: "number",
|
|
50
|
+
});
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Range check for the specific int width
|
|
55
|
+
if (input > this._intRange.max) {
|
|
56
|
+
ctx.issues.push({
|
|
57
|
+
code: ISSUE_CODES.TOO_LARGE,
|
|
58
|
+
message: `Value ${input} is above the maximum for ${this._kind}`,
|
|
59
|
+
path: [...ctx.path],
|
|
60
|
+
expected: this._kind,
|
|
61
|
+
received: String(input),
|
|
62
|
+
});
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (input < this._intRange.min) {
|
|
67
|
+
ctx.issues.push({
|
|
68
|
+
code: ISSUE_CODES.TOO_SMALL,
|
|
69
|
+
message: `Value ${input} is below the minimum for ${this._kind}`,
|
|
70
|
+
path: [...ctx.path],
|
|
71
|
+
expected: this._kind,
|
|
72
|
+
received: String(input),
|
|
73
|
+
});
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Additional user constraints
|
|
78
|
+
this._validateConstraints(input, ctx);
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class Int8Schema extends IntSchema {
|
|
84
|
+
constructor() {
|
|
85
|
+
super("int8");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class Int16Schema extends IntSchema {
|
|
90
|
+
constructor() {
|
|
91
|
+
super("int16");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class Int32Schema extends IntSchema {
|
|
96
|
+
constructor() {
|
|
97
|
+
super("int32");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class Int64Schema extends IntSchema {
|
|
102
|
+
constructor() {
|
|
103
|
+
super("int64");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class Uint8Schema extends IntSchema {
|
|
108
|
+
constructor() {
|
|
109
|
+
super("uint8");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class Uint16Schema extends IntSchema {
|
|
114
|
+
constructor() {
|
|
115
|
+
super("uint16");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class Uint32Schema extends IntSchema {
|
|
120
|
+
constructor() {
|
|
121
|
+
super("uint32");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class Uint64Schema extends IntSchema {
|
|
126
|
+
constructor() {
|
|
127
|
+
super("uint64");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
|
|
4
|
+
/** Convert a union of types to an intersection. */
|
|
5
|
+
type UnionToIntersection<U> = (
|
|
6
|
+
U extends any ? (x: U) => void : never
|
|
7
|
+
) extends (x: infer I) => void
|
|
8
|
+
? I
|
|
9
|
+
: never;
|
|
10
|
+
|
|
11
|
+
/** Flatten an intersection into a clean single-level type. */
|
|
12
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
13
|
+
|
|
14
|
+
export class IntersectionSchema<
|
|
15
|
+
T extends BaseSchema<any, any>[] = BaseSchema[],
|
|
16
|
+
> extends BaseSchema<
|
|
17
|
+
unknown,
|
|
18
|
+
Prettify<UnionToIntersection<T[number]["_output"]>>
|
|
19
|
+
> {
|
|
20
|
+
private _schemas: T;
|
|
21
|
+
|
|
22
|
+
constructor(schemas: [...T]) {
|
|
23
|
+
super();
|
|
24
|
+
this._schemas = schemas as T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
28
|
+
let result: unknown = input;
|
|
29
|
+
let anyFailed = false;
|
|
30
|
+
|
|
31
|
+
for (const schema of this._schemas) {
|
|
32
|
+
const innerCtx: ParseContext = {
|
|
33
|
+
path: [...ctx.path],
|
|
34
|
+
issues: [],
|
|
35
|
+
// Propagate recursion depth and circular-reference tracking so the
|
|
36
|
+
// depth guard is not reset (and bypassed) at each intersection member.
|
|
37
|
+
definitions: ctx.definitions,
|
|
38
|
+
seen: ctx.seen,
|
|
39
|
+
depth: ctx.depth,
|
|
40
|
+
};
|
|
41
|
+
const validated = schema._runPipeline(input, innerCtx);
|
|
42
|
+
|
|
43
|
+
if (innerCtx.issues.length > 0) {
|
|
44
|
+
ctx.issues.push(...innerCtx.issues);
|
|
45
|
+
anyFailed = true;
|
|
46
|
+
} else {
|
|
47
|
+
// Merge object results
|
|
48
|
+
if (
|
|
49
|
+
typeof result === "object" &&
|
|
50
|
+
result !== null &&
|
|
51
|
+
typeof validated === "object" &&
|
|
52
|
+
validated !== null &&
|
|
53
|
+
!Array.isArray(result) &&
|
|
54
|
+
!Array.isArray(validated)
|
|
55
|
+
) {
|
|
56
|
+
result = {
|
|
57
|
+
...(result as Record<string, unknown>),
|
|
58
|
+
...(validated as Record<string, unknown>),
|
|
59
|
+
};
|
|
60
|
+
} else {
|
|
61
|
+
result = validated;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (anyFailed) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_toNode(): SchemaNode {
|
|
74
|
+
const node = {
|
|
75
|
+
kind: "intersection" as const,
|
|
76
|
+
allOf: this._schemas.map((s) => s._toNode()),
|
|
77
|
+
};
|
|
78
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
79
|
+
return node as unknown as SchemaNode;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
|
|
5
|
+
type LiteralValue = string | number | boolean | null;
|
|
6
|
+
|
|
7
|
+
export class LiteralSchema<T extends LiteralValue> extends BaseSchema<T, T> {
|
|
8
|
+
private _value: T;
|
|
9
|
+
|
|
10
|
+
constructor(value: T) {
|
|
11
|
+
super();
|
|
12
|
+
this._value = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
16
|
+
if (input !== this._value) {
|
|
17
|
+
ctx.issues.push({
|
|
18
|
+
code: ISSUE_CODES.INVALID_LITERAL,
|
|
19
|
+
message: `Expected literal ${String(this._value)}, received ${String(input)}`,
|
|
20
|
+
path: [...ctx.path],
|
|
21
|
+
expected: String(this._value),
|
|
22
|
+
received: String(input),
|
|
23
|
+
});
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_toNode(): SchemaNode {
|
|
30
|
+
const node = { kind: "literal" as const, value: this._value };
|
|
31
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
32
|
+
return node as unknown as SchemaNode;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
|
|
6
|
+
export class NeverSchema extends BaseSchema<never, never> {
|
|
7
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
8
|
+
ctx.issues.push({
|
|
9
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
10
|
+
message: `Expected never (no value is valid)`,
|
|
11
|
+
path: [...ctx.path],
|
|
12
|
+
expected: "never",
|
|
13
|
+
received: describeType(input),
|
|
14
|
+
});
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_toNode(): SchemaNode {
|
|
19
|
+
return { kind: "never" } as SchemaNode;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
|
|
6
|
+
export class NullSchema extends BaseSchema<null, null> {
|
|
7
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
8
|
+
if (input !== null) {
|
|
9
|
+
ctx.issues.push({
|
|
10
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
11
|
+
message: `Expected null, received ${describeType(input)}`,
|
|
12
|
+
path: [...ctx.path],
|
|
13
|
+
expected: "null",
|
|
14
|
+
received: describeType(input),
|
|
15
|
+
});
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_toNode(): SchemaNode {
|
|
22
|
+
const node: SchemaNode = { kind: "null" } as SchemaNode;
|
|
23
|
+
this._addDefault(node);
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
|
|
4
|
+
export class NullableSchema<
|
|
5
|
+
T extends BaseSchema<any, any> = BaseSchema,
|
|
6
|
+
> extends BaseSchema<unknown, T["_output"] | null> {
|
|
7
|
+
/** @internal */ _inner: T;
|
|
8
|
+
|
|
9
|
+
constructor(inner: T) {
|
|
10
|
+
super();
|
|
11
|
+
this._inner = inner;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
15
|
+
if (input === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return this._inner._validate(input, ctx);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_runPipeline(input: unknown, ctx: ParseContext): unknown {
|
|
22
|
+
if (input === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return this._inner._runPipeline(input, ctx);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_toNode(): SchemaNode {
|
|
29
|
+
const node = {
|
|
30
|
+
kind: "nullable" as const,
|
|
31
|
+
inner: this._inner._toNode(),
|
|
32
|
+
};
|
|
33
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
34
|
+
return node as unknown as SchemaNode;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode, SchemaKind } from "../types.js";
|
|
2
|
+
import { BaseSchema } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
|
|
6
|
+
export class NumberSchema extends BaseSchema<number, number> {
|
|
7
|
+
protected _kind: SchemaKind;
|
|
8
|
+
protected _min?: number;
|
|
9
|
+
protected _max?: number;
|
|
10
|
+
protected _exclusiveMin?: number;
|
|
11
|
+
protected _exclusiveMax?: number;
|
|
12
|
+
protected _multipleOf?: number;
|
|
13
|
+
|
|
14
|
+
constructor(kind: SchemaKind = "number") {
|
|
15
|
+
super();
|
|
16
|
+
this._kind = kind;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_getCoercionTarget(): string {
|
|
20
|
+
return this._kind;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
min(n: number): this {
|
|
24
|
+
const clone = this._clone();
|
|
25
|
+
clone._min = n;
|
|
26
|
+
return clone;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
max(n: number): this {
|
|
30
|
+
const clone = this._clone();
|
|
31
|
+
clone._max = n;
|
|
32
|
+
return clone;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
exclusiveMin(n: number): this {
|
|
36
|
+
const clone = this._clone();
|
|
37
|
+
clone._exclusiveMin = n;
|
|
38
|
+
return clone;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
exclusiveMax(n: number): this {
|
|
42
|
+
const clone = this._clone();
|
|
43
|
+
clone._exclusiveMax = n;
|
|
44
|
+
return clone;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
multipleOf(n: number): this {
|
|
48
|
+
const clone = this._clone();
|
|
49
|
+
clone._multipleOf = n;
|
|
50
|
+
return clone;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
54
|
+
if (typeof input !== "number" || !Number.isFinite(input)) {
|
|
55
|
+
ctx.issues.push({
|
|
56
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
57
|
+
message: `Expected ${this._kind}, received ${describeType(input)}`,
|
|
58
|
+
path: [...ctx.path],
|
|
59
|
+
expected: this._kind,
|
|
60
|
+
received: describeType(input),
|
|
61
|
+
});
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._validateConstraints(input, ctx);
|
|
66
|
+
return input;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected _validateConstraints(val: number, ctx: ParseContext): void {
|
|
70
|
+
if (this._min !== undefined && val < this._min) {
|
|
71
|
+
ctx.issues.push({
|
|
72
|
+
code: ISSUE_CODES.TOO_SMALL,
|
|
73
|
+
message: `Number must be >= ${this._min}`,
|
|
74
|
+
path: [...ctx.path],
|
|
75
|
+
expected: String(this._min),
|
|
76
|
+
received: String(val),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this._max !== undefined && val > this._max) {
|
|
81
|
+
ctx.issues.push({
|
|
82
|
+
code: ISSUE_CODES.TOO_LARGE,
|
|
83
|
+
message: `Number must be <= ${this._max}`,
|
|
84
|
+
path: [...ctx.path],
|
|
85
|
+
expected: String(this._max),
|
|
86
|
+
received: String(val),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this._exclusiveMin !== undefined && val <= this._exclusiveMin) {
|
|
91
|
+
ctx.issues.push({
|
|
92
|
+
code: ISSUE_CODES.TOO_SMALL,
|
|
93
|
+
message: `Number must be > ${this._exclusiveMin}`,
|
|
94
|
+
path: [...ctx.path],
|
|
95
|
+
expected: String(this._exclusiveMin),
|
|
96
|
+
received: String(val),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this._exclusiveMax !== undefined && val >= this._exclusiveMax) {
|
|
101
|
+
ctx.issues.push({
|
|
102
|
+
code: ISSUE_CODES.TOO_LARGE,
|
|
103
|
+
message: `Number must be < ${this._exclusiveMax}`,
|
|
104
|
+
path: [...ctx.path],
|
|
105
|
+
expected: String(this._exclusiveMax),
|
|
106
|
+
received: String(val),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this._multipleOf !== undefined) {
|
|
111
|
+
const remainder = val % this._multipleOf;
|
|
112
|
+
if (
|
|
113
|
+
Math.abs(remainder) > 1e-10 &&
|
|
114
|
+
Math.abs(remainder - this._multipleOf) > 1e-10
|
|
115
|
+
) {
|
|
116
|
+
ctx.issues.push({
|
|
117
|
+
code: ISSUE_CODES.INVALID_NUMBER,
|
|
118
|
+
message: `Number must be a multiple of ${this._multipleOf}`,
|
|
119
|
+
path: [...ctx.path],
|
|
120
|
+
expected: String(this._multipleOf),
|
|
121
|
+
received: String(val),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_toNode(): SchemaNode {
|
|
128
|
+
const node: Record<string, unknown> = { kind: this._kind };
|
|
129
|
+
if (this._min !== undefined) node.min = this._min;
|
|
130
|
+
if (this._max !== undefined) node.max = this._max;
|
|
131
|
+
if (this._exclusiveMin !== undefined)
|
|
132
|
+
node.exclusiveMin = this._exclusiveMin;
|
|
133
|
+
if (this._exclusiveMax !== undefined)
|
|
134
|
+
node.exclusiveMax = this._exclusiveMax;
|
|
135
|
+
if (this._multipleOf !== undefined) node.multipleOf = this._multipleOf;
|
|
136
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
137
|
+
return node as unknown as SchemaNode;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class Float32Schema extends NumberSchema {
|
|
142
|
+
constructor() {
|
|
143
|
+
super("float32");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class Float64Schema extends NumberSchema {
|
|
148
|
+
constructor() {
|
|
149
|
+
super("float64");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ParseContext, SchemaNode, UnknownKeyMode } from "../types.js";
|
|
2
|
+
import { BaseSchema, ABSENT } from "./base.js";
|
|
3
|
+
import { ISSUE_CODES } from "../issue-codes.js";
|
|
4
|
+
import { describeType } from "../util.js";
|
|
5
|
+
import type { OptionalSchema } from "./optional.js";
|
|
6
|
+
|
|
7
|
+
/** Flatten an intersection into a clean single-level type. */
|
|
8
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
9
|
+
|
|
10
|
+
/** Map a shape record to its inferred output type, separating required from optional fields. */
|
|
11
|
+
export type InferShape<
|
|
12
|
+
T extends Record<string, BaseSchema<any, any>>,
|
|
13
|
+
> = Prettify<
|
|
14
|
+
{
|
|
15
|
+
[K in keyof T as T[K] extends OptionalSchema<any> ? never : K]: T[K]["_output"];
|
|
16
|
+
} & {
|
|
17
|
+
[K in keyof T as T[K] extends OptionalSchema<any>
|
|
18
|
+
? K
|
|
19
|
+
: never]?: T[K]["_output"];
|
|
20
|
+
}
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
interface PropertyDef {
|
|
24
|
+
schema: BaseSchema;
|
|
25
|
+
required: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ObjectSchema<
|
|
29
|
+
TShape extends Record<string, BaseSchema<any, any>> = Record<
|
|
30
|
+
string,
|
|
31
|
+
BaseSchema
|
|
32
|
+
>,
|
|
33
|
+
> extends BaseSchema<Record<string, unknown>, InferShape<TShape>> {
|
|
34
|
+
private _properties: Map<string, PropertyDef>;
|
|
35
|
+
private _unknownKeys: UnknownKeyMode;
|
|
36
|
+
private _unknownKeysExplicit: boolean;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
shape: TShape,
|
|
40
|
+
options?: { unknownKeys?: UnknownKeyMode },
|
|
41
|
+
) {
|
|
42
|
+
super();
|
|
43
|
+
this._properties = new Map();
|
|
44
|
+
this._unknownKeys = options?.unknownKeys ?? "strip";
|
|
45
|
+
this._unknownKeysExplicit = options?.unknownKeys !== undefined;
|
|
46
|
+
|
|
47
|
+
for (const [key, schema] of Object.entries(shape)) {
|
|
48
|
+
// Check if the schema is an OptionalSchema wrapper
|
|
49
|
+
const isOptional = (schema as any)._isOptionalWrapper === true;
|
|
50
|
+
this._properties.set(key, {
|
|
51
|
+
schema,
|
|
52
|
+
required: !isOptional,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
unknownKeys(mode: UnknownKeyMode): ObjectSchema<TShape> {
|
|
58
|
+
const clone = this._clone();
|
|
59
|
+
clone._unknownKeys = mode;
|
|
60
|
+
clone._unknownKeysExplicit = true;
|
|
61
|
+
return clone;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private _effectiveUnknownKeys(): UnknownKeyMode {
|
|
65
|
+
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _exportUnknownKeys(): UnknownKeyMode {
|
|
69
|
+
return this._unknownKeysExplicit ? this._unknownKeys : "strip";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_validate(input: unknown, ctx: ParseContext): unknown {
|
|
73
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
74
|
+
ctx.issues.push({
|
|
75
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
76
|
+
message: `Expected object, received ${describeType(input)}`,
|
|
77
|
+
path: [...ctx.path],
|
|
78
|
+
expected: "object",
|
|
79
|
+
received: describeType(input),
|
|
80
|
+
});
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Circular reference detection
|
|
85
|
+
if (!ctx.seen) ctx.seen = new WeakSet();
|
|
86
|
+
if (ctx.seen.has(input as object)) {
|
|
87
|
+
ctx.issues.push({
|
|
88
|
+
code: ISSUE_CODES.INVALID_TYPE,
|
|
89
|
+
message: "Circular reference detected",
|
|
90
|
+
path: [...ctx.path],
|
|
91
|
+
expected: "object",
|
|
92
|
+
received: "circular",
|
|
93
|
+
});
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
ctx.seen.add(input as object);
|
|
97
|
+
|
|
98
|
+
const obj = input as Record<string, unknown>;
|
|
99
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
100
|
+
const inputKeys = new Set(Object.keys(obj));
|
|
101
|
+
|
|
102
|
+
// Detect __proto__ via hasOwnProperty (Object.keys skips it)
|
|
103
|
+
if (Object.prototype.hasOwnProperty.call(obj, "__proto__")) {
|
|
104
|
+
inputKeys.add("__proto__");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate declared properties
|
|
108
|
+
for (const [key, prop] of this._properties) {
|
|
109
|
+
ctx.path.push(key);
|
|
110
|
+
const hasKey = Object.prototype.hasOwnProperty.call(obj, key);
|
|
111
|
+
inputKeys.delete(key);
|
|
112
|
+
|
|
113
|
+
if (!hasKey) {
|
|
114
|
+
// Check if required
|
|
115
|
+
if (prop.required && prop.schema._defaultValue === ABSENT) {
|
|
116
|
+
const expectedKind = prop.schema._toNode().kind;
|
|
117
|
+
ctx.issues.push({
|
|
118
|
+
code: ISSUE_CODES.REQUIRED,
|
|
119
|
+
message: `Required property "${key}" is missing`,
|
|
120
|
+
path: [...ctx.path],
|
|
121
|
+
expected: expectedKind,
|
|
122
|
+
received: "undefined",
|
|
123
|
+
});
|
|
124
|
+
ctx.path.pop();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rawValue = hasKey ? obj[key] : undefined;
|
|
130
|
+
const val = prop.schema._runPipeline(rawValue, ctx);
|
|
131
|
+
|
|
132
|
+
// Only include in result if value is not undefined or it was explicitly present
|
|
133
|
+
if (val !== undefined || hasKey || prop.schema._defaultValue !== ABSENT) {
|
|
134
|
+
Object.defineProperty(result, key, {
|
|
135
|
+
value: val,
|
|
136
|
+
writable: true,
|
|
137
|
+
enumerable: true,
|
|
138
|
+
configurable: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ctx.path.pop();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle unknown keys
|
|
146
|
+
for (const key of inputKeys) {
|
|
147
|
+
switch (this._effectiveUnknownKeys()) {
|
|
148
|
+
case "reject":
|
|
149
|
+
ctx.issues.push({
|
|
150
|
+
code: ISSUE_CODES.UNKNOWN_KEY,
|
|
151
|
+
message: `Unknown key "${key}"`,
|
|
152
|
+
path: [...ctx.path, key],
|
|
153
|
+
expected: "undefined",
|
|
154
|
+
received: key,
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
case "allow":
|
|
158
|
+
Object.defineProperty(result, key, {
|
|
159
|
+
value: obj[key],
|
|
160
|
+
writable: true,
|
|
161
|
+
enumerable: true,
|
|
162
|
+
configurable: true,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
case "strip":
|
|
166
|
+
// Just ignore it
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove from the ancestor set now that this subtree is fully processed.
|
|
172
|
+
// The guard tracks the current ancestor chain (true cycles), not every
|
|
173
|
+
// object ever seen — otherwise a shared/repeated non-circular reference in
|
|
174
|
+
// sibling positions would be falsely rejected as circular.
|
|
175
|
+
ctx.seen.delete(input as object);
|
|
176
|
+
|
|
177
|
+
// Restore normal prototype so result behaves like a standard object
|
|
178
|
+
// while preventing __proto__ pollution via Object.create(null) above
|
|
179
|
+
Object.setPrototypeOf(result, Object.prototype);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_toNode(): SchemaNode {
|
|
184
|
+
const properties: Record<string, SchemaNode> = {};
|
|
185
|
+
const required: string[] = [];
|
|
186
|
+
|
|
187
|
+
for (const [key, prop] of this._properties) {
|
|
188
|
+
properties[key] = prop.schema._toNode();
|
|
189
|
+
if (prop.required) {
|
|
190
|
+
required.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const node = {
|
|
195
|
+
kind: "object" as const,
|
|
196
|
+
properties,
|
|
197
|
+
required,
|
|
198
|
+
unknownKeys: this._exportUnknownKeys(),
|
|
199
|
+
};
|
|
200
|
+
this._addDefault(node as unknown as SchemaNode);
|
|
201
|
+
return node as unknown as SchemaNode;
|
|
202
|
+
}
|
|
203
|
+
}
|