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.
Files changed (204) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +370 -0
  3. package/VERSION +1 -0
  4. package/dist/errors.d.ts +6 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +12 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/format/validators.d.ts +2 -0
  9. package/dist/format/validators.d.ts.map +1 -0
  10. package/dist/format/validators.js +57 -0
  11. package/dist/format/validators.js.map +1 -0
  12. package/dist/forms/index.d.ts +57 -0
  13. package/dist/forms/index.d.ts.map +1 -0
  14. package/dist/forms/index.js +586 -0
  15. package/dist/forms/index.js.map +1 -0
  16. package/dist/index.d.ts +93 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +156 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/infer.d.ts +8 -0
  21. package/dist/infer.d.ts.map +1 -0
  22. package/dist/infer.js +2 -0
  23. package/dist/infer.js.map +1 -0
  24. package/dist/interchange/document.d.ts +5 -0
  25. package/dist/interchange/document.d.ts.map +1 -0
  26. package/dist/interchange/document.js +12 -0
  27. package/dist/interchange/document.js.map +1 -0
  28. package/dist/interchange/exporter.d.ts +7 -0
  29. package/dist/interchange/exporter.d.ts.map +1 -0
  30. package/dist/interchange/exporter.js +7 -0
  31. package/dist/interchange/exporter.js.map +1 -0
  32. package/dist/interchange/importer.d.ts +4 -0
  33. package/dist/interchange/importer.d.ts.map +1 -0
  34. package/dist/interchange/importer.js +229 -0
  35. package/dist/interchange/importer.js.map +1 -0
  36. package/dist/issue-codes.d.ts +19 -0
  37. package/dist/issue-codes.d.ts.map +1 -0
  38. package/dist/issue-codes.js +18 -0
  39. package/dist/issue-codes.js.map +1 -0
  40. package/dist/parse/coerce.d.ts +16 -0
  41. package/dist/parse/coerce.d.ts.map +1 -0
  42. package/dist/parse/coerce.js +115 -0
  43. package/dist/parse/coerce.js.map +1 -0
  44. package/dist/parse/defaults.d.ts +7 -0
  45. package/dist/parse/defaults.d.ts.map +1 -0
  46. package/dist/parse/defaults.js +12 -0
  47. package/dist/parse/defaults.js.map +1 -0
  48. package/dist/parse/parser.d.ts +11 -0
  49. package/dist/parse/parser.d.ts.map +1 -0
  50. package/dist/parse/parser.js +13 -0
  51. package/dist/parse/parser.js.map +1 -0
  52. package/dist/schemas/any.d.ts +7 -0
  53. package/dist/schemas/any.d.ts.map +1 -0
  54. package/dist/schemas/any.js +12 -0
  55. package/dist/schemas/any.js.map +1 -0
  56. package/dist/schemas/array.d.ts +13 -0
  57. package/dist/schemas/array.d.ts.map +1 -0
  58. package/dist/schemas/array.js +73 -0
  59. package/dist/schemas/array.js.map +1 -0
  60. package/dist/schemas/base.d.ts +37 -0
  61. package/dist/schemas/base.d.ts.map +1 -0
  62. package/dist/schemas/base.js +285 -0
  63. package/dist/schemas/base.js.map +1 -0
  64. package/dist/schemas/bool.d.ts +8 -0
  65. package/dist/schemas/bool.d.ts.map +1 -0
  66. package/dist/schemas/bool.js +27 -0
  67. package/dist/schemas/bool.js.map +1 -0
  68. package/dist/schemas/enum.d.ts +9 -0
  69. package/dist/schemas/enum.d.ts.map +1 -0
  70. package/dist/schemas/enum.js +31 -0
  71. package/dist/schemas/enum.js.map +1 -0
  72. package/dist/schemas/index.d.ts +21 -0
  73. package/dist/schemas/index.d.ts.map +1 -0
  74. package/dist/schemas/index.js +21 -0
  75. package/dist/schemas/index.js.map +1 -0
  76. package/dist/schemas/int.d.ts +32 -0
  77. package/dist/schemas/int.d.ts.map +1 -0
  78. package/dist/schemas/int.js +108 -0
  79. package/dist/schemas/int.js.map +1 -0
  80. package/dist/schemas/intersection.d.ts +16 -0
  81. package/dist/schemas/intersection.d.ts.map +1 -0
  82. package/dist/schemas/intersection.js +58 -0
  83. package/dist/schemas/intersection.js.map +1 -0
  84. package/dist/schemas/literal.d.ts +11 -0
  85. package/dist/schemas/literal.d.ts.map +1 -0
  86. package/dist/schemas/literal.js +28 -0
  87. package/dist/schemas/literal.js.map +1 -0
  88. package/dist/schemas/never.d.ts +7 -0
  89. package/dist/schemas/never.d.ts.map +1 -0
  90. package/dist/schemas/never.js +19 -0
  91. package/dist/schemas/never.js.map +1 -0
  92. package/dist/schemas/null.d.ts +7 -0
  93. package/dist/schemas/null.d.ts.map +1 -0
  94. package/dist/schemas/null.js +24 -0
  95. package/dist/schemas/null.js.map +1 -0
  96. package/dist/schemas/nullable.d.ts +10 -0
  97. package/dist/schemas/nullable.d.ts.map +1 -0
  98. package/dist/schemas/nullable.js +29 -0
  99. package/dist/schemas/nullable.js.map +1 -0
  100. package/dist/schemas/number.d.ts +27 -0
  101. package/dist/schemas/number.d.ts.map +1 -0
  102. package/dist/schemas/number.js +134 -0
  103. package/dist/schemas/number.js.map +1 -0
  104. package/dist/schemas/object.d.ts +28 -0
  105. package/dist/schemas/object.d.ts.map +1 -0
  106. package/dist/schemas/object.js +153 -0
  107. package/dist/schemas/object.js.map +1 -0
  108. package/dist/schemas/optional.d.ts +11 -0
  109. package/dist/schemas/optional.d.ts.map +1 -0
  110. package/dist/schemas/optional.js +39 -0
  111. package/dist/schemas/optional.js.map +1 -0
  112. package/dist/schemas/record.d.ts +9 -0
  113. package/dist/schemas/record.d.ts.map +1 -0
  114. package/dist/schemas/record.js +45 -0
  115. package/dist/schemas/record.js.map +1 -0
  116. package/dist/schemas/ref.d.ts +10 -0
  117. package/dist/schemas/ref.d.ts.map +1 -0
  118. package/dist/schemas/ref.js +30 -0
  119. package/dist/schemas/ref.js.map +1 -0
  120. package/dist/schemas/string.d.ts +29 -0
  121. package/dist/schemas/string.d.ts.map +1 -0
  122. package/dist/schemas/string.js +181 -0
  123. package/dist/schemas/string.js.map +1 -0
  124. package/dist/schemas/tuple.d.ts +14 -0
  125. package/dist/schemas/tuple.d.ts.map +1 -0
  126. package/dist/schemas/tuple.js +59 -0
  127. package/dist/schemas/tuple.js.map +1 -0
  128. package/dist/schemas/union.d.ts +9 -0
  129. package/dist/schemas/union.d.ts.map +1 -0
  130. package/dist/schemas/union.js +45 -0
  131. package/dist/schemas/union.js.map +1 -0
  132. package/dist/schemas/unknown.d.ts +7 -0
  133. package/dist/schemas/unknown.d.ts.map +1 -0
  134. package/dist/schemas/unknown.js +12 -0
  135. package/dist/schemas/unknown.js.map +1 -0
  136. package/dist/types.d.ts +132 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/util.d.ts +6 -0
  141. package/dist/util.d.ts.map +1 -0
  142. package/dist/util.js +12 -0
  143. package/dist/util.js.map +1 -0
  144. package/package.json +41 -0
  145. package/sdk/js/CHANGELOG.md +13 -0
  146. package/src/errors.ts +17 -0
  147. package/src/format/validators.ts +71 -0
  148. package/src/forms/index.ts +789 -0
  149. package/src/index.ts +285 -0
  150. package/src/infer.ts +12 -0
  151. package/src/interchange/document.ts +18 -0
  152. package/src/interchange/exporter.ts +12 -0
  153. package/src/interchange/importer.ts +285 -0
  154. package/src/issue-codes.ts +19 -0
  155. package/src/parse/coerce.ts +133 -0
  156. package/src/parse/defaults.ts +15 -0
  157. package/src/parse/parser.ts +19 -0
  158. package/src/schemas/any.ts +14 -0
  159. package/src/schemas/array.ts +83 -0
  160. package/src/schemas/base.ts +322 -0
  161. package/src/schemas/bool.ts +30 -0
  162. package/src/schemas/enum.ts +37 -0
  163. package/src/schemas/index.ts +30 -0
  164. package/src/schemas/int.ts +129 -0
  165. package/src/schemas/intersection.ts +81 -0
  166. package/src/schemas/literal.ts +34 -0
  167. package/src/schemas/never.ts +21 -0
  168. package/src/schemas/null.ts +26 -0
  169. package/src/schemas/nullable.ts +36 -0
  170. package/src/schemas/number.ts +151 -0
  171. package/src/schemas/object.ts +203 -0
  172. package/src/schemas/optional.ts +49 -0
  173. package/src/schemas/record.ts +55 -0
  174. package/src/schemas/ref.ts +35 -0
  175. package/src/schemas/string.ts +192 -0
  176. package/src/schemas/tuple.ts +74 -0
  177. package/src/schemas/union.ts +53 -0
  178. package/src/schemas/unknown.ts +14 -0
  179. package/src/types.ts +239 -0
  180. package/src/util.ts +9 -0
  181. package/tests/conformance/runner.test.ts +28 -0
  182. package/tests/conformance/runner.ts +137 -0
  183. package/tests/forms.test.ts +146 -0
  184. package/tests/unit/coerce.test.ts +136 -0
  185. package/tests/unit/collections.test.ts +99 -0
  186. package/tests/unit/composition.test.ts +80 -0
  187. package/tests/unit/date-format.test.ts +18 -0
  188. package/tests/unit/default-mutation.test.ts +32 -0
  189. package/tests/unit/defaults.test.ts +49 -0
  190. package/tests/unit/errors.test.ts +53 -0
  191. package/tests/unit/export.test.ts +270 -0
  192. package/tests/unit/inference.test.ts +306 -0
  193. package/tests/unit/interchange.test.ts +191 -0
  194. package/tests/unit/number.test.ts +195 -0
  195. package/tests/unit/object.test.ts +208 -0
  196. package/tests/unit/parser.test.ts +151 -0
  197. package/tests/unit/primitives.test.ts +111 -0
  198. package/tests/unit/security-recursion.test.ts +105 -0
  199. package/tests/unit/security.test.ts +945 -0
  200. package/tests/unit/shared-ref-falsepos.test.ts +33 -0
  201. package/tests/unit/string-pattern-redos.test.ts +46 -0
  202. package/tests/unit/string.test.ts +147 -0
  203. package/tsconfig.json +21 -0
  204. 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
+ }