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,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
|
+
}
|