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,49 @@
1
+ import type { ParseContext, SchemaNode } from "../types.js";
2
+ import { BaseSchema, ABSENT } from "./base.js";
3
+
4
+ export class OptionalSchema<
5
+ T extends BaseSchema<any, any> = BaseSchema,
6
+ > extends BaseSchema<unknown, T["_output"] | undefined> {
7
+ /** @internal */ _inner: T;
8
+ /** @internal */ _isOptionalWrapper = true;
9
+
10
+ constructor(inner: T) {
11
+ super();
12
+ this._inner = inner;
13
+ // Inherit defaults/coercion from inner
14
+ this._defaultValue = inner._defaultValue as any;
15
+ this._coercionConfig = inner._coercionConfig;
16
+ }
17
+
18
+ _validate(input: unknown, ctx: ParseContext): unknown {
19
+ if (input === undefined || input === ABSENT) {
20
+ return undefined;
21
+ }
22
+ return this._inner._validate(input, ctx);
23
+ }
24
+
25
+ _runPipeline(input: unknown, ctx: ParseContext): unknown {
26
+ const isAbsent = input === undefined || input === ABSENT;
27
+
28
+ // If absent and we have a default from inner, apply it
29
+ if (isAbsent && this._inner._defaultValue !== ABSENT) {
30
+ return this._inner._runPipeline(input, ctx);
31
+ }
32
+
33
+ if (isAbsent) {
34
+ return undefined;
35
+ }
36
+
37
+ // Delegate to inner's pipeline for coercion etc.
38
+ return this._inner._runPipeline(input, ctx);
39
+ }
40
+
41
+ _toNode(): SchemaNode {
42
+ const node = {
43
+ kind: "optional" as const,
44
+ inner: this._inner._toNode(),
45
+ };
46
+ this._addDefault(node as unknown as SchemaNode);
47
+ return node as unknown as SchemaNode;
48
+ }
49
+ }
@@ -0,0 +1,55 @@
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 RecordSchema<
7
+ T extends BaseSchema<any, any> = BaseSchema,
8
+ > extends BaseSchema<Record<string, unknown>, Record<string, T["_output"]>> {
9
+ private _valueSchema: T;
10
+
11
+ constructor(valueSchema: T) {
12
+ super();
13
+ this._valueSchema = valueSchema;
14
+ }
15
+
16
+ _validate(input: unknown, ctx: ParseContext): unknown {
17
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
18
+ ctx.issues.push({
19
+ code: ISSUE_CODES.INVALID_TYPE,
20
+ message: `Expected record, received ${describeType(input)}`,
21
+ path: [...ctx.path],
22
+ expected: "record",
23
+ received: describeType(input),
24
+ });
25
+ return undefined;
26
+ }
27
+
28
+ const obj = input as Record<string, unknown>;
29
+ const result: Record<string, unknown> = Object.create(null);
30
+
31
+ for (const [key, value] of Object.entries(obj)) {
32
+ ctx.path.push(key);
33
+ Object.defineProperty(result, key, {
34
+ value: this._valueSchema._runPipeline(value, ctx),
35
+ writable: true,
36
+ enumerable: true,
37
+ configurable: true,
38
+ });
39
+ ctx.path.pop();
40
+ }
41
+
42
+ Object.setPrototypeOf(result, Object.prototype);
43
+
44
+ return result;
45
+ }
46
+
47
+ _toNode(): SchemaNode {
48
+ const node = {
49
+ kind: "record" as const,
50
+ valueSchema: this._valueSchema._toNode(),
51
+ };
52
+ this._addDefault(node as unknown as SchemaNode);
53
+ return node as unknown as SchemaNode;
54
+ }
55
+ }
@@ -0,0 +1,35 @@
1
+ import type { ParseContext, SchemaNode } from "../types.js";
2
+ import { BaseSchema } from "./base.js";
3
+ import { ISSUE_CODES } from "../issue-codes.js";
4
+
5
+ export class RefSchema extends BaseSchema<unknown, unknown> {
6
+ private _ref: string;
7
+ private _resolver?: () => BaseSchema;
8
+
9
+ constructor(ref: string, resolver?: () => BaseSchema) {
10
+ super();
11
+ this._ref = ref;
12
+ this._resolver = resolver;
13
+ }
14
+
15
+ _validate(input: unknown, ctx: ParseContext): unknown {
16
+ if (this._resolver) {
17
+ const resolved = this._resolver();
18
+ return resolved._validate(input, ctx);
19
+ }
20
+
21
+ ctx.issues.push({
22
+ code: ISSUE_CODES.UNSUPPORTED_SCHEMA_KIND,
23
+ message: `Unresolved ref: ${this._ref}`,
24
+ path: [...ctx.path],
25
+ });
26
+ return undefined;
27
+ }
28
+
29
+ _toNode(): SchemaNode {
30
+ return {
31
+ kind: "ref" as const,
32
+ ref: this._ref,
33
+ } as unknown as SchemaNode;
34
+ }
35
+ }
@@ -0,0 +1,192 @@
1
+ import type { ParseContext, SchemaNode, StringFormat } from "../types.js";
2
+ import { BaseSchema } from "./base.js";
3
+ import { ISSUE_CODES } from "../issue-codes.js";
4
+ import { validateFormat } from "../format/validators.js";
5
+ import { describeType } from "../util.js";
6
+
7
+ export class StringSchema extends BaseSchema<string, string> {
8
+ private _minLength?: number;
9
+ private _maxLength?: number;
10
+ private _pattern?: string;
11
+ private _startsWith?: string;
12
+ private _endsWith?: string;
13
+ private _includes?: string;
14
+ private _format?: StringFormat;
15
+ /**
16
+ * Cached compiled pattern. `undefined` = not yet compiled, `null` =
17
+ * compilation failed (invalid pattern). Avoids recompiling on every value.
18
+ */
19
+ private _patternRe?: RegExp | null;
20
+
21
+ _getCoercionTarget(): string {
22
+ return "string";
23
+ }
24
+
25
+ minLength(n: number): this {
26
+ const clone = this._clone();
27
+ clone._minLength = n;
28
+ return clone;
29
+ }
30
+
31
+ maxLength(n: number): this {
32
+ const clone = this._clone();
33
+ clone._maxLength = n;
34
+ return clone;
35
+ }
36
+
37
+ pattern(p: string): this {
38
+ const clone = this._clone();
39
+ clone._pattern = p;
40
+ // Reset cached compilation inherited from the source via _clone().
41
+ clone._patternRe = undefined;
42
+ return clone;
43
+ }
44
+
45
+ /** Lazily compile and cache the pattern. Returns null if invalid. */
46
+ private _getPatternRe(): RegExp | null {
47
+ if (this._patternRe !== undefined) return this._patternRe;
48
+ try {
49
+ this._patternRe = new RegExp(this._pattern as string);
50
+ } catch {
51
+ this._patternRe = null;
52
+ }
53
+ return this._patternRe;
54
+ }
55
+
56
+ startsWith(s: string): this {
57
+ const clone = this._clone();
58
+ clone._startsWith = s;
59
+ return clone;
60
+ }
61
+
62
+ endsWith(s: string): this {
63
+ const clone = this._clone();
64
+ clone._endsWith = s;
65
+ return clone;
66
+ }
67
+
68
+ includes(s: string): this {
69
+ const clone = this._clone();
70
+ clone._includes = s;
71
+ return clone;
72
+ }
73
+
74
+ format(f: StringFormat): this {
75
+ const clone = this._clone();
76
+ clone._format = f;
77
+ return clone;
78
+ }
79
+
80
+ _validate(input: unknown, ctx: ParseContext): unknown {
81
+ if (typeof input !== "string") {
82
+ ctx.issues.push({
83
+ code: ISSUE_CODES.INVALID_TYPE,
84
+ message: `Expected string, received ${describeType(input)}`,
85
+ path: [...ctx.path],
86
+ expected: "string",
87
+ received: describeType(input),
88
+ });
89
+ return undefined;
90
+ }
91
+
92
+ const val = input;
93
+ const length = Array.from(val).length;
94
+
95
+ if (this._minLength !== undefined && length < this._minLength) {
96
+ ctx.issues.push({
97
+ code: ISSUE_CODES.TOO_SMALL,
98
+ message: `String must have at least ${this._minLength} character(s)`,
99
+ path: [...ctx.path],
100
+ expected: String(this._minLength),
101
+ received: String(length),
102
+ });
103
+ }
104
+
105
+ if (this._maxLength !== undefined && length > this._maxLength) {
106
+ ctx.issues.push({
107
+ code: ISSUE_CODES.TOO_LARGE,
108
+ message: `String must have at most ${this._maxLength} character(s)`,
109
+ path: [...ctx.path],
110
+ expected: String(this._maxLength),
111
+ received: String(length),
112
+ });
113
+ }
114
+
115
+ if (this._pattern !== undefined) {
116
+ const re = this._getPatternRe();
117
+ if (re === null) {
118
+ // Invalid regex pattern - treat as validation failure
119
+ ctx.issues.push({
120
+ code: ISSUE_CODES.INVALID_STRING,
121
+ message: `Invalid regex pattern: ${this._pattern}`,
122
+ path: [...ctx.path],
123
+ expected: this._pattern,
124
+ received: val,
125
+ });
126
+ } else if (!re.test(val)) {
127
+ ctx.issues.push({
128
+ code: ISSUE_CODES.INVALID_STRING,
129
+ message: `String does not match pattern: ${this._pattern}`,
130
+ path: [...ctx.path],
131
+ expected: this._pattern,
132
+ received: val,
133
+ });
134
+ }
135
+ }
136
+
137
+ if (this._startsWith !== undefined && !val.startsWith(this._startsWith)) {
138
+ ctx.issues.push({
139
+ code: ISSUE_CODES.INVALID_STRING,
140
+ message: `String must start with "${this._startsWith}"`,
141
+ path: [...ctx.path],
142
+ expected: this._startsWith,
143
+ received: val,
144
+ });
145
+ }
146
+
147
+ if (this._endsWith !== undefined && !val.endsWith(this._endsWith)) {
148
+ ctx.issues.push({
149
+ code: ISSUE_CODES.INVALID_STRING,
150
+ message: `String must end with "${this._endsWith}"`,
151
+ path: [...ctx.path],
152
+ expected: this._endsWith,
153
+ received: val,
154
+ });
155
+ }
156
+
157
+ if (this._includes !== undefined && !val.includes(this._includes)) {
158
+ ctx.issues.push({
159
+ code: ISSUE_CODES.INVALID_STRING,
160
+ message: `String must include "${this._includes}"`,
161
+ path: [...ctx.path],
162
+ expected: this._includes,
163
+ received: val,
164
+ });
165
+ }
166
+
167
+ if (this._format !== undefined && !validateFormat(val, this._format)) {
168
+ ctx.issues.push({
169
+ code: ISSUE_CODES.INVALID_STRING,
170
+ message: `Invalid ${this._format} format`,
171
+ path: [...ctx.path],
172
+ expected: this._format,
173
+ received: val,
174
+ });
175
+ }
176
+
177
+ return val;
178
+ }
179
+
180
+ _toNode(): SchemaNode {
181
+ const node: Record<string, unknown> = { kind: "string" };
182
+ if (this._minLength !== undefined) node.minLength = this._minLength;
183
+ if (this._maxLength !== undefined) node.maxLength = this._maxLength;
184
+ if (this._pattern !== undefined) node.pattern = this._pattern;
185
+ if (this._startsWith !== undefined) node.startsWith = this._startsWith;
186
+ if (this._endsWith !== undefined) node.endsWith = this._endsWith;
187
+ if (this._includes !== undefined) node.includes = this._includes;
188
+ if (this._format !== undefined) node.format = this._format;
189
+ this._addDefault(node as unknown as SchemaNode);
190
+ return node as unknown as SchemaNode;
191
+ }
192
+ }
@@ -0,0 +1,74 @@
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
+ /** Map a tuple of schemas to a tuple of their output types. */
7
+ type InferTuple<T extends BaseSchema<any, any>[]> = {
8
+ [K in keyof T]: T[K]["_output"];
9
+ };
10
+
11
+ export class TupleSchema<
12
+ T extends BaseSchema<any, any>[] = BaseSchema[],
13
+ > extends BaseSchema<unknown[], InferTuple<T>> {
14
+ private _items: T;
15
+
16
+ constructor(items: [...T]) {
17
+ super();
18
+ this._items = items as T;
19
+ }
20
+
21
+ _validate(input: unknown, ctx: ParseContext): unknown {
22
+ if (!Array.isArray(input)) {
23
+ ctx.issues.push({
24
+ code: ISSUE_CODES.INVALID_TYPE,
25
+ message: `Expected tuple, received ${describeType(input)}`,
26
+ path: [...ctx.path],
27
+ expected: "tuple",
28
+ received: describeType(input),
29
+ });
30
+ return undefined;
31
+ }
32
+
33
+ if (input.length < this._items.length) {
34
+ ctx.issues.push({
35
+ code: ISSUE_CODES.TOO_SMALL,
36
+ message: `Tuple must have exactly ${this._items.length} element(s), received ${input.length}`,
37
+ path: [...ctx.path],
38
+ expected: String(this._items.length),
39
+ received: String(input.length),
40
+ });
41
+ return undefined;
42
+ }
43
+
44
+ if (input.length > this._items.length) {
45
+ ctx.issues.push({
46
+ code: ISSUE_CODES.TOO_LARGE,
47
+ message: `Tuple must have exactly ${this._items.length} element(s), received ${input.length}`,
48
+ path: [...ctx.path],
49
+ expected: String(this._items.length),
50
+ received: String(input.length),
51
+ });
52
+ return undefined;
53
+ }
54
+
55
+ const result: unknown[] = [];
56
+ for (let i = 0; i < this._items.length; i++) {
57
+ ctx.path.push(i);
58
+ const val = this._items[i]._runPipeline(input[i], ctx);
59
+ result.push(val);
60
+ ctx.path.pop();
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ _toNode(): SchemaNode {
67
+ const node = {
68
+ kind: "tuple" as const,
69
+ elements: this._items.map((s) => s._toNode()),
70
+ };
71
+ this._addDefault(node as unknown as SchemaNode);
72
+ return node as unknown as SchemaNode;
73
+ }
74
+ }
@@ -0,0 +1,53 @@
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 UnionSchema<
7
+ T extends BaseSchema<any, any>[] = BaseSchema[],
8
+ > extends BaseSchema<unknown, T[number]["_output"]> {
9
+ private _variants: T;
10
+
11
+ constructor(variants: [...T]) {
12
+ super();
13
+ this._variants = variants as T;
14
+ }
15
+
16
+ _validate(input: unknown, ctx: ParseContext): unknown {
17
+ for (const variant of this._variants) {
18
+ const innerCtx: ParseContext = {
19
+ path: [...ctx.path],
20
+ issues: [],
21
+ // Propagate recursion depth and circular-reference tracking so the
22
+ // depth guard is not reset (and bypassed) at each union boundary.
23
+ definitions: ctx.definitions,
24
+ seen: ctx.seen,
25
+ depth: ctx.depth,
26
+ };
27
+ const result = variant._runPipeline(input, innerCtx);
28
+ if (innerCtx.issues.length === 0) {
29
+ return result;
30
+ }
31
+ }
32
+
33
+ const variantKinds = this._variants.map((v) => v._toNode().kind);
34
+ ctx.issues.push({
35
+ code: ISSUE_CODES.INVALID_UNION,
36
+ message: `Input did not match any variant of the union`,
37
+ path: [...ctx.path],
38
+ expected: variantKinds.join(" | "),
39
+ received: describeType(input),
40
+ });
41
+
42
+ return undefined;
43
+ }
44
+
45
+ _toNode(): SchemaNode {
46
+ const node = {
47
+ kind: "union" as const,
48
+ variants: this._variants.map((v) => v._toNode()),
49
+ };
50
+ this._addDefault(node as unknown as SchemaNode);
51
+ return node as unknown as SchemaNode;
52
+ }
53
+ }
@@ -0,0 +1,14 @@
1
+ import type { ParseContext, SchemaNode } from "../types.js";
2
+ import { BaseSchema } from "./base.js";
3
+
4
+ export class UnknownSchema extends BaseSchema<unknown, unknown> {
5
+ _validate(input: unknown, _ctx: ParseContext): unknown {
6
+ return input;
7
+ }
8
+
9
+ _toNode(): SchemaNode {
10
+ const node: SchemaNode = { kind: "unknown" } as SchemaNode;
11
+ this._addDefault(node);
12
+ return node;
13
+ }
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,239 @@
1
+ // ---- Schema Node (interchange JSON representation) ----
2
+
3
+ export type SchemaKind =
4
+ | "any"
5
+ | "unknown"
6
+ | "never"
7
+ | "null"
8
+ | "bool"
9
+ | "string"
10
+ | "number"
11
+ | "int"
12
+ | "float32"
13
+ | "float64"
14
+ | "int8"
15
+ | "int16"
16
+ | "int32"
17
+ | "int64"
18
+ | "uint8"
19
+ | "uint16"
20
+ | "uint32"
21
+ | "uint64"
22
+ | "literal"
23
+ | "enum"
24
+ | "array"
25
+ | "tuple"
26
+ | "object"
27
+ | "record"
28
+ | "union"
29
+ | "intersection"
30
+ | "optional"
31
+ | "nullable"
32
+ | "ref";
33
+
34
+ export interface MetadataOptions {
35
+ replace?: boolean;
36
+ }
37
+
38
+ export interface DescribeOptions {
39
+ title?: string;
40
+ deprecated?: boolean;
41
+ deprecatedMessage?: string;
42
+ notStable?: boolean;
43
+ since?: string;
44
+ sensitive?: boolean;
45
+ readonly?: boolean;
46
+ writeonly?: boolean;
47
+ examples?: unknown[];
48
+ }
49
+
50
+ export interface SchemaNodeBase {
51
+ kind: SchemaKind;
52
+ default?: unknown;
53
+ coerce?: CoercionConfig | string | string[];
54
+ metadata?: Record<string, unknown>;
55
+ }
56
+
57
+ export interface CoercionConfig {
58
+ from?: string;
59
+ trim?: boolean;
60
+ lower?: boolean;
61
+ upper?: boolean;
62
+ }
63
+
64
+ // String node
65
+ export interface StringSchemaNode extends SchemaNodeBase {
66
+ kind: "string";
67
+ minLength?: number;
68
+ maxLength?: number;
69
+ pattern?: string;
70
+ startsWith?: string;
71
+ endsWith?: string;
72
+ includes?: string;
73
+ format?: StringFormat;
74
+ }
75
+
76
+ export type StringFormat =
77
+ | "email"
78
+ | "url"
79
+ | "uuid"
80
+ | "ipv4"
81
+ | "ipv6"
82
+ | "date"
83
+ | "date-time";
84
+
85
+ // Numeric nodes
86
+ export interface NumericSchemaNode extends SchemaNodeBase {
87
+ kind:
88
+ | "number"
89
+ | "int"
90
+ | "float32"
91
+ | "float64"
92
+ | "int8"
93
+ | "int16"
94
+ | "int32"
95
+ | "int64"
96
+ | "uint8"
97
+ | "uint16"
98
+ | "uint32"
99
+ | "uint64";
100
+ min?: number;
101
+ max?: number;
102
+ exclusiveMin?: number;
103
+ exclusiveMax?: number;
104
+ multipleOf?: number;
105
+ }
106
+
107
+ // Simple nodes
108
+ export interface SimpleSchemaNode extends SchemaNodeBase {
109
+ kind: "any" | "unknown" | "never" | "null" | "bool";
110
+ }
111
+
112
+ // Literal node
113
+ export interface LiteralSchemaNode extends SchemaNodeBase {
114
+ kind: "literal";
115
+ value: string | number | boolean | null;
116
+ }
117
+
118
+ // Enum node
119
+ export interface EnumSchemaNode extends SchemaNodeBase {
120
+ kind: "enum";
121
+ values: (string | number)[];
122
+ }
123
+
124
+ // Array node
125
+ export interface ArraySchemaNode extends SchemaNodeBase {
126
+ kind: "array";
127
+ items: SchemaNode;
128
+ minItems?: number;
129
+ maxItems?: number;
130
+ }
131
+
132
+ // Tuple node
133
+ export interface TupleSchemaNode extends SchemaNodeBase {
134
+ kind: "tuple";
135
+ items: SchemaNode[];
136
+ }
137
+
138
+ // Object node
139
+ export interface ObjectSchemaNode extends SchemaNodeBase {
140
+ kind: "object";
141
+ properties: Record<string, SchemaNode>;
142
+ required: string[];
143
+ unknownKeys?: UnknownKeyMode;
144
+ }
145
+
146
+ // Record node
147
+ export interface RecordSchemaNode extends SchemaNodeBase {
148
+ kind: "record";
149
+ valueSchema: SchemaNode;
150
+ }
151
+
152
+ // Union node
153
+ export interface UnionSchemaNode extends SchemaNodeBase {
154
+ kind: "union";
155
+ variants: SchemaNode[];
156
+ }
157
+
158
+ // Intersection node
159
+ export interface IntersectionSchemaNode extends SchemaNodeBase {
160
+ kind: "intersection";
161
+ allOf: SchemaNode[];
162
+ }
163
+
164
+ // Optional node
165
+ export interface OptionalSchemaNode extends SchemaNodeBase {
166
+ kind: "optional";
167
+ inner: SchemaNode;
168
+ }
169
+
170
+ // Nullable node
171
+ export interface NullableSchemaNode extends SchemaNodeBase {
172
+ kind: "nullable";
173
+ inner: SchemaNode;
174
+ }
175
+
176
+ // Ref node
177
+ export interface RefSchemaNode extends SchemaNodeBase {
178
+ kind: "ref";
179
+ ref: string;
180
+ }
181
+
182
+ export type SchemaNode =
183
+ | StringSchemaNode
184
+ | NumericSchemaNode
185
+ | SimpleSchemaNode
186
+ | LiteralSchemaNode
187
+ | EnumSchemaNode
188
+ | ArraySchemaNode
189
+ | TupleSchemaNode
190
+ | ObjectSchemaNode
191
+ | RecordSchemaNode
192
+ | UnionSchemaNode
193
+ | IntersectionSchemaNode
194
+ | OptionalSchemaNode
195
+ | NullableSchemaNode
196
+ | RefSchemaNode;
197
+
198
+ // ---- Parse Result ----
199
+
200
+ export type ParseResult<T> =
201
+ | { success: true; data: T }
202
+ | { success: false; issues: ValidationIssue[] };
203
+
204
+ export interface ValidationIssue {
205
+ code: string;
206
+ message: string;
207
+ path: (string | number)[];
208
+ expected?: string;
209
+ received?: string;
210
+ meta?: Record<string, unknown>;
211
+ }
212
+
213
+ // ---- Interchange Document ----
214
+
215
+ export interface AnyValiDocument {
216
+ anyvaliVersion: string;
217
+ schemaVersion: string;
218
+ root: SchemaNode;
219
+ definitions: Record<string, SchemaNode>;
220
+ extensions: Record<string, Record<string, unknown>>;
221
+ }
222
+
223
+ // ---- Modes ----
224
+
225
+ export type ExportMode = "portable" | "extended";
226
+
227
+ export type UnknownKeyMode = "reject" | "strip" | "allow";
228
+
229
+ // ---- Parse Context (internal) ----
230
+
231
+ export interface ParseContext {
232
+ path: (string | number)[];
233
+ issues: ValidationIssue[];
234
+ definitions?: Record<string, SchemaNode>;
235
+ /** Tracks objects already being validated to detect circular references. */
236
+ seen?: WeakSet<object>;
237
+ /** Current recursion depth, used to bound unbounded recursion (DoS guard). */
238
+ depth?: number;
239
+ }