@agen-ai/validation 0.1.0
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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +226 -0
- package/dist/zod.d.ts +6 -0
- package/dist/zod.js +146 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trevor Nichols
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# `@agen-ai/validation`
|
|
2
|
+
|
|
3
|
+
`@agen-ai/validation` defines a small, validator-neutral issue format for reusable packages. It
|
|
4
|
+
normalizes validation failures without making an ordinary public API expose a particular schema
|
|
5
|
+
library. The optional Zod entrypoint adapts Zod 4 issues to the same stable shape.
|
|
6
|
+
|
|
7
|
+
## Entrypoints
|
|
8
|
+
|
|
9
|
+
- `@agen-ai/validation` exports plain issue types plus normalization functions. Its declarations do
|
|
10
|
+
not expose Zod.
|
|
11
|
+
- `@agen-ai/validation/zod` exports the deliberate Zod 4 adapter and may expose Zod types.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { normalizeValidationIssues } from "@agen-ai/validation";
|
|
15
|
+
|
|
16
|
+
const issues = normalizeValidationIssues(
|
|
17
|
+
[{ code: "invalid_type", path: ["sessionId"], message: "Invalid input" }],
|
|
18
|
+
{ sessionId: 42 },
|
|
19
|
+
);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Consumers should treat `ValidationIssue.code`, `path`, and `message` as the portable error
|
|
23
|
+
contract. Validator-native errors remain implementation details unless the consumer explicitly
|
|
24
|
+
imports `/zod`.
|
|
25
|
+
|
|
26
|
+
## Versioning and release
|
|
27
|
+
|
|
28
|
+
The package follows semantic versioning. Breaking changes to ordinary types, normalized issue
|
|
29
|
+
semantics, or exported entrypoints require a major release. Additive issue helpers and compatible
|
|
30
|
+
normalization improvements may be minor releases; fixes that preserve the public contract are
|
|
31
|
+
patch releases.
|
|
32
|
+
|
|
33
|
+
The repository check builds and packs this package, inspects the tarball, and installs it into a
|
|
34
|
+
temporary project outside the workspace. Run it from the public repository root:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm check
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That command never publishes. Registry publication, tags, and release credentials are separate
|
|
41
|
+
release operations.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type ValidationPathSegment = string | number;
|
|
2
|
+
export interface ValidationIssue {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly path: readonly ValidationPathSegment[];
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationIssueInput {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly path?: readonly PropertyKey[] | undefined;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly expected?: string | undefined;
|
|
12
|
+
readonly values?: readonly unknown[] | undefined;
|
|
13
|
+
readonly minimum?: number | bigint | undefined;
|
|
14
|
+
readonly maximum?: number | bigint | undefined;
|
|
15
|
+
readonly origin?: string | undefined;
|
|
16
|
+
readonly keys?: readonly unknown[] | undefined;
|
|
17
|
+
readonly format?: string | undefined;
|
|
18
|
+
readonly inclusive?: boolean | undefined;
|
|
19
|
+
readonly exact?: boolean | undefined;
|
|
20
|
+
readonly includes?: string | undefined;
|
|
21
|
+
readonly prefix?: string | undefined;
|
|
22
|
+
readonly suffix?: string | undefined;
|
|
23
|
+
readonly position?: number | undefined;
|
|
24
|
+
readonly divisor?: number | bigint | undefined;
|
|
25
|
+
readonly note?: string | undefined;
|
|
26
|
+
readonly discriminator?: string | undefined;
|
|
27
|
+
readonly options?: readonly unknown[] | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface ValidationErrorInput {
|
|
30
|
+
readonly issues: readonly ValidationIssueInput[];
|
|
31
|
+
}
|
|
32
|
+
export type ValidationInvalidValueKind = 'enum' | 'literal';
|
|
33
|
+
export interface ValidationNormalizationOptions {
|
|
34
|
+
readonly resolveInvalidValueKind?: (issue: ValidationIssueInput) => ValidationInvalidValueKind | undefined;
|
|
35
|
+
}
|
|
36
|
+
export declare function normalizeValidationIssues(issues: readonly ValidationIssueInput[], input: unknown, options?: ValidationNormalizationOptions): readonly ValidationIssue[];
|
|
37
|
+
export declare function normalizeValidationError(error: ValidationErrorInput, input: unknown, options?: ValidationNormalizationOptions): readonly ValidationIssue[];
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const DEFAULT_FORMAT_MESSAGES = {
|
|
2
|
+
base64: ["Invalid base64-encoded string", "Invalid base64"],
|
|
3
|
+
cidrv4: ["Invalid IPv4 range", "Invalid cidr"],
|
|
4
|
+
cidrv6: ["Invalid IPv6 range", "Invalid cidr"],
|
|
5
|
+
cuid: ["Invalid cuid", "Invalid cuid"],
|
|
6
|
+
date: ["Invalid ISO date", "Invalid date"],
|
|
7
|
+
datetime: ["Invalid ISO datetime", "Invalid datetime"],
|
|
8
|
+
duration: ["Invalid ISO duration", "Invalid duration"],
|
|
9
|
+
email: ["Invalid email address", "Invalid email"],
|
|
10
|
+
emoji: ["Invalid emoji", "Invalid emoji"],
|
|
11
|
+
ipv4: ["Invalid IPv4 address", "Invalid ip"],
|
|
12
|
+
ipv6: ["Invalid IPv6 address", "Invalid ip"],
|
|
13
|
+
jwt: ["Invalid JWT", "Invalid jwt"],
|
|
14
|
+
nanoid: ["Invalid nanoid", "Invalid nanoid"],
|
|
15
|
+
time: ["Invalid ISO time", "Invalid time"],
|
|
16
|
+
ulid: ["Invalid ULID", "Invalid ulid"],
|
|
17
|
+
url: ["Invalid URL", "Invalid url"],
|
|
18
|
+
uuid: ["Invalid UUID", "Invalid uuid"]
|
|
19
|
+
};
|
|
20
|
+
function pathSegments(path) {
|
|
21
|
+
return (path ?? []).map(
|
|
22
|
+
(segment) => typeof segment === "number" ? segment : String(segment)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
function valueAtPath(input, path) {
|
|
26
|
+
let value = input;
|
|
27
|
+
for (const segment of path) {
|
|
28
|
+
if (typeof value === "string" && typeof segment === "number") {
|
|
29
|
+
value = value.split(",")[segment];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
33
|
+
value = value[segment];
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function quotedValue(value) {
|
|
38
|
+
if (typeof value === "string") return `'${value}'`;
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
function serializedLiteral(value) {
|
|
42
|
+
return JSON.stringify(
|
|
43
|
+
value,
|
|
44
|
+
(_key, item) => typeof item === "bigint" ? item.toString() : item
|
|
45
|
+
) ?? String(value);
|
|
46
|
+
}
|
|
47
|
+
function receivedType(value) {
|
|
48
|
+
if (value === void 0) return "undefined";
|
|
49
|
+
if (value === null) return "null";
|
|
50
|
+
if (typeof value === "number" && Number.isNaN(value)) return "nan";
|
|
51
|
+
if (Array.isArray(value)) return "array";
|
|
52
|
+
if (value instanceof Date) return "date";
|
|
53
|
+
if (value instanceof Map) return "map";
|
|
54
|
+
if (value instanceof Set) return "set";
|
|
55
|
+
if (value instanceof Promise) return "promise";
|
|
56
|
+
return typeof value;
|
|
57
|
+
}
|
|
58
|
+
function invalidTypeIssue(issue, path, input) {
|
|
59
|
+
if (issue.code !== "invalid_type" || typeof issue.expected !== "string" || !issue.message.startsWith("Invalid input: expected ")) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const receivedValue = valueAtPath(input, path);
|
|
63
|
+
if (receivedValue === void 0) {
|
|
64
|
+
return { code: issue.code, path, message: "Required" };
|
|
65
|
+
}
|
|
66
|
+
const expected = issue.expected === "int" ? "integer" : issue.expected;
|
|
67
|
+
const received = issue.expected === "int" && typeof receivedValue === "number" && !Number.isInteger(receivedValue) ? "float" : receivedType(receivedValue);
|
|
68
|
+
return {
|
|
69
|
+
code: issue.code,
|
|
70
|
+
path,
|
|
71
|
+
message: `Expected ${expected}, received ${received}`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function nonFiniteNumberIssue(issue, path, input) {
|
|
75
|
+
const receivedValue = valueAtPath(input, path);
|
|
76
|
+
if (issue.code !== "invalid_type" || issue.expected !== "number" || issue.message !== "Invalid input: expected number, received number" || receivedValue !== Number.POSITIVE_INFINITY && receivedValue !== Number.NEGATIVE_INFINITY) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
code: "not_finite",
|
|
81
|
+
path,
|
|
82
|
+
message: "Number must be finite"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function limitDescription(issue, boundary, direction) {
|
|
86
|
+
const exact = issue.exact === true;
|
|
87
|
+
const inclusive = issue.inclusive !== false;
|
|
88
|
+
if (issue.origin === "array") {
|
|
89
|
+
const comparison = exact ? "exactly" : direction === "minimum" ? inclusive ? "at least" : "more than" : inclusive ? "at most" : "less than";
|
|
90
|
+
return `Array must contain ${comparison} ${boundary} element(s)`;
|
|
91
|
+
}
|
|
92
|
+
if (issue.origin === "string") {
|
|
93
|
+
const comparison = exact ? "exactly" : direction === "minimum" ? inclusive ? "at least" : "over" : inclusive ? "at most" : "under";
|
|
94
|
+
return `String must contain ${comparison} ${boundary} character(s)`;
|
|
95
|
+
}
|
|
96
|
+
if (issue.origin === "number" || issue.origin === "int") {
|
|
97
|
+
const comparison = exact ? "exactly equal to" : direction === "minimum" ? inclusive ? "greater than or equal to" : "greater than" : inclusive ? "less than or equal to" : "less than";
|
|
98
|
+
return `Number must be ${comparison} ${boundary}`;
|
|
99
|
+
}
|
|
100
|
+
if (issue.origin === "bigint") {
|
|
101
|
+
const comparison = exact ? "exactly" : direction === "minimum" ? inclusive ? "greater than or equal to" : "greater than" : inclusive ? "less than or equal to" : "less than";
|
|
102
|
+
const label = direction === "minimum" ? "Number" : "BigInt";
|
|
103
|
+
return `${label} must be ${comparison} ${boundary}`;
|
|
104
|
+
}
|
|
105
|
+
if (issue.origin === "date") {
|
|
106
|
+
const comparison = exact ? "exactly" : direction === "minimum" ? inclusive ? "greater than or equal to" : "greater than" : inclusive ? "smaller than or equal to" : "smaller than";
|
|
107
|
+
return `Date must be ${comparison} ${new Date(Number(boundary))}`;
|
|
108
|
+
}
|
|
109
|
+
return "Invalid input";
|
|
110
|
+
}
|
|
111
|
+
function limitIssue(issue, path) {
|
|
112
|
+
const direction = issue.code === "too_small" ? "minimum" : issue.code === "too_big" ? "maximum" : null;
|
|
113
|
+
if (direction === null || !issue.message.startsWith("Too ")) return null;
|
|
114
|
+
const boundary = issue[direction];
|
|
115
|
+
if (typeof boundary !== "number" && typeof boundary !== "bigint") return null;
|
|
116
|
+
return {
|
|
117
|
+
code: issue.code,
|
|
118
|
+
path,
|
|
119
|
+
message: limitDescription(issue, boundary, direction)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function notMultipleOfIssue(issue, path) {
|
|
123
|
+
if (issue.code !== "not_multiple_of" || typeof issue.divisor !== "number" && typeof issue.divisor !== "bigint" || issue.message !== `Invalid number: must be a multiple of ${issue.divisor}`) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
code: issue.code,
|
|
128
|
+
path,
|
|
129
|
+
message: `Number must be a multiple of ${issue.divisor}`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function invalidUnionDiscriminatorIssue(issue, path) {
|
|
133
|
+
if (issue.code !== "invalid_union" || issue.note !== "No matching discriminator" || typeof issue.discriminator !== "string" || !Array.isArray(issue.options)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
code: "invalid_union_discriminator",
|
|
138
|
+
path,
|
|
139
|
+
message: issue.message
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function invalidFormatMessage(issue) {
|
|
143
|
+
if (issue.format === "regex" && issue.message.startsWith("Invalid string: must match pattern ")) {
|
|
144
|
+
return "Invalid";
|
|
145
|
+
}
|
|
146
|
+
if (issue.format === "includes" && issue.includes !== void 0 && issue.message === `Invalid string: must include "${issue.includes}"`) {
|
|
147
|
+
const position = typeof issue.position === "number" ? ` at one or more positions greater than or equal to ${issue.position}` : "";
|
|
148
|
+
return `Invalid input: must include "${issue.includes}"${position}`;
|
|
149
|
+
}
|
|
150
|
+
if (issue.format === "starts_with" && issue.prefix !== void 0 && issue.message === `Invalid string: must start with "${issue.prefix}"`) {
|
|
151
|
+
return `Invalid input: must start with "${issue.prefix}"`;
|
|
152
|
+
}
|
|
153
|
+
if (issue.format === "ends_with" && issue.suffix !== void 0 && issue.message === `Invalid string: must end with "${issue.suffix}"`) {
|
|
154
|
+
return `Invalid input: must end with "${issue.suffix}"`;
|
|
155
|
+
}
|
|
156
|
+
const defaultMessage = issue.format === void 0 ? void 0 : DEFAULT_FORMAT_MESSAGES[issue.format];
|
|
157
|
+
if (defaultMessage !== void 0 && issue.message === defaultMessage[0]) {
|
|
158
|
+
return defaultMessage[1];
|
|
159
|
+
}
|
|
160
|
+
return issue.message;
|
|
161
|
+
}
|
|
162
|
+
function normalizeIssue(issue, input, options) {
|
|
163
|
+
const path = pathSegments(issue.path);
|
|
164
|
+
const nonFiniteNumber = nonFiniteNumberIssue(issue, path, input);
|
|
165
|
+
if (nonFiniteNumber !== null) return nonFiniteNumber;
|
|
166
|
+
const invalidType = invalidTypeIssue(issue, path, input);
|
|
167
|
+
if (invalidType !== null) return invalidType;
|
|
168
|
+
if (issue.code === "invalid_value" && Array.isArray(issue.values)) {
|
|
169
|
+
const invalidValueKind = issue.values.length === 1 ? options.resolveInvalidValueKind?.(issue) ?? "literal" : "enum";
|
|
170
|
+
if (invalidValueKind === "literal") {
|
|
171
|
+
return {
|
|
172
|
+
code: "invalid_literal",
|
|
173
|
+
path,
|
|
174
|
+
message: `Invalid literal value, expected ${serializedLiteral(issue.values[0])}`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const receivedValue = valueAtPath(input, path);
|
|
178
|
+
if (receivedValue === void 0) {
|
|
179
|
+
return {
|
|
180
|
+
code: "invalid_type",
|
|
181
|
+
path,
|
|
182
|
+
message: "Required"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
code: "invalid_enum_value",
|
|
187
|
+
path,
|
|
188
|
+
message: `Invalid enum value. Expected ${issue.values.map(quotedValue).join(" | ")}, received ${quotedValue(receivedValue)}`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const limit = limitIssue(issue, path);
|
|
192
|
+
if (limit !== null) return limit;
|
|
193
|
+
const notMultipleOf = notMultipleOfIssue(issue, path);
|
|
194
|
+
if (notMultipleOf !== null) return notMultipleOf;
|
|
195
|
+
const invalidUnionDiscriminator = invalidUnionDiscriminatorIssue(issue, path);
|
|
196
|
+
if (invalidUnionDiscriminator !== null) return invalidUnionDiscriminator;
|
|
197
|
+
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) {
|
|
198
|
+
return {
|
|
199
|
+
code: issue.code,
|
|
200
|
+
path,
|
|
201
|
+
message: `Unrecognized key(s) in object: ${issue.keys.map(quotedValue).join(", ")}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (issue.code === "invalid_format") {
|
|
205
|
+
return {
|
|
206
|
+
code: "invalid_string",
|
|
207
|
+
path,
|
|
208
|
+
message: invalidFormatMessage(issue)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
code: issue.code,
|
|
213
|
+
path,
|
|
214
|
+
message: issue.message
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function normalizeValidationIssues(issues, input, options = {}) {
|
|
218
|
+
return issues.map((issue) => normalizeIssue(issue, input, options));
|
|
219
|
+
}
|
|
220
|
+
function normalizeValidationError(error, input, options = {}) {
|
|
221
|
+
return normalizeValidationIssues(error.issues, input, options);
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
normalizeValidationError,
|
|
225
|
+
normalizeValidationIssues
|
|
226
|
+
};
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ValidationErrorInput, type ValidationInvalidValueKind, type ValidationIssue, type ValidationIssueInput } from './index.js';
|
|
2
|
+
import type { ZodType } from 'zod/v4';
|
|
3
|
+
export declare function resolveZodInvalidValueKind(schema: ZodType, path: readonly PropertyKey[]): ValidationInvalidValueKind | undefined;
|
|
4
|
+
export declare function normalizeZodValidationIssues(schema: ZodType, issues: readonly ValidationIssueInput[], input: unknown): readonly ValidationIssue[];
|
|
5
|
+
export declare function normalizeZodValidationError(schema: ZodType, error: ValidationErrorInput, input: unknown): readonly ValidationIssue[];
|
|
6
|
+
//# sourceMappingURL=zod.d.ts.map
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeValidationIssues
|
|
3
|
+
} from "./index.js";
|
|
4
|
+
function schemaNode(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && "_zod" in value ? value : null;
|
|
6
|
+
}
|
|
7
|
+
function schemaDefinition(schema) {
|
|
8
|
+
return schemaNode(schema)?._zod?.def ?? null;
|
|
9
|
+
}
|
|
10
|
+
function nestedSchema(definition, key) {
|
|
11
|
+
return schemaNode(definition[key]);
|
|
12
|
+
}
|
|
13
|
+
function collectInvalidValueKinds(schema, path, pathIndex, depth) {
|
|
14
|
+
if (depth > path.length * 4 + 32) return [];
|
|
15
|
+
const definition = schemaDefinition(schema);
|
|
16
|
+
const schemaType = definition?.type;
|
|
17
|
+
if (definition === null || typeof schemaType !== "string") return [];
|
|
18
|
+
if (schemaType === "optional" || schemaType === "nullable" || schemaType === "default" || schemaType === "prefault" || schemaType === "catch" || schemaType === "readonly" || schemaType === "nonoptional" || schemaType === "promise") {
|
|
19
|
+
const innerType = nestedSchema(definition, "innerType");
|
|
20
|
+
return innerType === null ? [] : collectInvalidValueKinds(
|
|
21
|
+
innerType,
|
|
22
|
+
path,
|
|
23
|
+
pathIndex,
|
|
24
|
+
depth + 1
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (schemaType === "pipe") {
|
|
28
|
+
return [
|
|
29
|
+
...collectInvalidValueKinds(
|
|
30
|
+
nestedSchema(definition, "in"),
|
|
31
|
+
path,
|
|
32
|
+
pathIndex,
|
|
33
|
+
depth + 1
|
|
34
|
+
),
|
|
35
|
+
...collectInvalidValueKinds(
|
|
36
|
+
nestedSchema(definition, "out"),
|
|
37
|
+
path,
|
|
38
|
+
pathIndex,
|
|
39
|
+
depth + 1
|
|
40
|
+
)
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
if (schemaType === "union") {
|
|
44
|
+
const options = Array.isArray(definition.options) ? definition.options : [];
|
|
45
|
+
return options.flatMap(
|
|
46
|
+
(option) => collectInvalidValueKinds(option, path, pathIndex, depth + 1)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (schemaType === "intersection") {
|
|
50
|
+
return [
|
|
51
|
+
...collectInvalidValueKinds(
|
|
52
|
+
nestedSchema(definition, "left"),
|
|
53
|
+
path,
|
|
54
|
+
pathIndex,
|
|
55
|
+
depth + 1
|
|
56
|
+
),
|
|
57
|
+
...collectInvalidValueKinds(
|
|
58
|
+
nestedSchema(definition, "right"),
|
|
59
|
+
path,
|
|
60
|
+
pathIndex,
|
|
61
|
+
depth + 1
|
|
62
|
+
)
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
if (schemaType === "lazy" && typeof definition.getter === "function") {
|
|
66
|
+
return collectInvalidValueKinds(
|
|
67
|
+
definition.getter(),
|
|
68
|
+
path,
|
|
69
|
+
pathIndex,
|
|
70
|
+
depth + 1
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (pathIndex === path.length) {
|
|
74
|
+
if (schemaType === "enum") return ["enum"];
|
|
75
|
+
if (schemaType === "literal") return ["literal"];
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const segment = path[pathIndex];
|
|
79
|
+
if (schemaType === "object") {
|
|
80
|
+
const rawShape = typeof definition.shape === "function" ? definition.shape() : definition.shape;
|
|
81
|
+
const shape = rawShape !== null && typeof rawShape === "object" ? rawShape : {};
|
|
82
|
+
const child = schemaNode(shape[segment]);
|
|
83
|
+
const fallback = child ?? nestedSchema(definition, "catchall");
|
|
84
|
+
return fallback === null ? [] : collectInvalidValueKinds(
|
|
85
|
+
fallback,
|
|
86
|
+
path,
|
|
87
|
+
pathIndex + 1,
|
|
88
|
+
depth + 1
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (schemaType === "array") {
|
|
92
|
+
const element = nestedSchema(definition, "element");
|
|
93
|
+
return element === null ? [] : collectInvalidValueKinds(
|
|
94
|
+
element,
|
|
95
|
+
path,
|
|
96
|
+
pathIndex + 1,
|
|
97
|
+
depth + 1
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (schemaType === "tuple" && typeof segment === "number") {
|
|
101
|
+
const items = Array.isArray(definition.items) ? definition.items : [];
|
|
102
|
+
const item = schemaNode(items[segment]) ?? nestedSchema(definition, "rest");
|
|
103
|
+
return item === null ? [] : collectInvalidValueKinds(
|
|
104
|
+
item,
|
|
105
|
+
path,
|
|
106
|
+
pathIndex + 1,
|
|
107
|
+
depth + 1
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (schemaType === "record" || schemaType === "map" || schemaType === "set") {
|
|
111
|
+
const valueType = nestedSchema(definition, "valueType") ?? nestedSchema(definition, "element");
|
|
112
|
+
return valueType === null ? [] : collectInvalidValueKinds(
|
|
113
|
+
valueType,
|
|
114
|
+
path,
|
|
115
|
+
pathIndex + 1,
|
|
116
|
+
depth + 1
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
function resolveZodInvalidValueKind(schema, path) {
|
|
122
|
+
const kinds = new Set(
|
|
123
|
+
collectInvalidValueKinds(schema, path, 0, 0)
|
|
124
|
+
);
|
|
125
|
+
return kinds.size === 1 ? kinds.values().next().value : void 0;
|
|
126
|
+
}
|
|
127
|
+
function zodNormalizationOptions(schema) {
|
|
128
|
+
return {
|
|
129
|
+
resolveInvalidValueKind: (issue) => resolveZodInvalidValueKind(schema, issue.path ?? [])
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function normalizeZodValidationIssues(schema, issues, input) {
|
|
133
|
+
return normalizeValidationIssues(
|
|
134
|
+
issues,
|
|
135
|
+
input,
|
|
136
|
+
zodNormalizationOptions(schema)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function normalizeZodValidationError(schema, error, input) {
|
|
140
|
+
return normalizeZodValidationIssues(schema, error.issues, input);
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
normalizeZodValidationError,
|
|
144
|
+
normalizeZodValidationIssues,
|
|
145
|
+
resolveZodInvalidValueKind
|
|
146
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agen-ai/validation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Validator-neutral issue contracts and optional Zod 4 normalization for reusable AgenAI packages.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/**/*.js",
|
|
14
|
+
"dist/**/*.d.ts",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./zod": {
|
|
24
|
+
"types": "./dist/zod.d.ts",
|
|
25
|
+
"import": "./dist/zod.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"zod": "4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.5.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.0.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/trevor-nichols/agenai-agent-sdk.git",
|
|
41
|
+
"directory": "packages/validation"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/trevor-nichols/agenai-agent-sdk#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/trevor-nichols/agenai-agent-sdk/issues"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "pnpm run clean && pnpm run build:runtime && pnpm run build:types",
|
|
53
|
+
"build:runtime": "tsup",
|
|
54
|
+
"build:types": "tsc --project tsconfig.json --emitDeclarationOnly --outDir dist",
|
|
55
|
+
"dev": "pnpm run build:runtime -- --watch",
|
|
56
|
+
"dev:types": "tsc --project tsconfig.json --emitDeclarationOnly --outDir dist --watch",
|
|
57
|
+
"test": "node --import tsx --test src/*.test.ts",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"clean": "rimraf dist *.tsbuildinfo"
|
|
60
|
+
}
|
|
61
|
+
}
|