anyvali 0.3.1 → 0.3.4
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 +67 -44
- package/README.md +370 -370
- package/dist/parse/coerce.d.ts.map +1 -1
- package/dist/parse/coerce.js +14 -0
- package/dist/parse/coerce.js.map +1 -1
- package/dist/schemas/number.d.ts.map +1 -1
- package/dist/schemas/number.js +15 -0
- package/dist/schemas/number.js.map +1 -1
- package/dist/schemas/optional.d.ts.map +1 -1
- package/dist/schemas/optional.js +4 -3
- package/dist/schemas/optional.js.map +1 -1
- package/package.json +40 -40
- package/sdk/js/CHANGELOG.md +13 -13
- package/src/format/validators.ts +71 -71
- package/src/index.ts +285 -285
- package/src/infer.ts +12 -12
- package/src/interchange/importer.ts +285 -285
- package/src/issue-codes.ts +19 -19
- package/src/parse/coerce.ts +15 -0
- package/src/schemas/base.ts +322 -322
- package/src/schemas/intersection.ts +81 -81
- package/src/schemas/number.ts +17 -0
- package/src/schemas/object.ts +203 -203
- package/src/schemas/optional.ts +4 -3
- package/src/schemas/record.ts +55 -55
- package/src/schemas/string.ts +192 -192
- package/src/schemas/union.ts +53 -53
- package/src/types.ts +239 -239
- package/tests/unit/collections.test.ts +99 -99
- package/tests/unit/date-format.test.ts +18 -18
- package/tests/unit/default-mutation.test.ts +32 -32
- package/tests/unit/defaults.test.ts +70 -1
- package/tests/unit/inference.test.ts +306 -306
- package/tests/unit/interchange.test.ts +191 -191
- package/tests/unit/object.test.ts +208 -208
- package/tests/unit/security-recursion.test.ts +105 -105
- package/tests/unit/security.test.ts +1067 -945
- package/tests/unit/shared-ref-falsepos.test.ts +33 -33
- package/tests/unit/string-pattern-redos.test.ts +46 -46
- package/tests/unit/string.test.ts +147 -147
package/src/schemas/record.ts
CHANGED
|
@@ -1,55 +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
|
-
}
|
|
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
|
+
}
|
package/src/schemas/string.ts
CHANGED
|
@@ -1,192 +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
|
-
}
|
|
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
|
+
}
|
package/src/schemas/union.ts
CHANGED
|
@@ -1,53 +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
|
-
}
|
|
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
|
+
}
|