@agentuity/schema 0.0.69
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/AGENTS.md +86 -0
- package/README.md +323 -0
- package/dist/base.d.ts +111 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +93 -0
- package/dist/base.js.map +1 -0
- package/dist/coerce/boolean.d.ts +37 -0
- package/dist/coerce/boolean.d.ts.map +1 -0
- package/dist/coerce/boolean.js +49 -0
- package/dist/coerce/boolean.js.map +1 -0
- package/dist/coerce/date.d.ts +36 -0
- package/dist/coerce/date.d.ts.map +1 -0
- package/dist/coerce/date.js +60 -0
- package/dist/coerce/date.js.map +1 -0
- package/dist/coerce/number.d.ts +36 -0
- package/dist/coerce/number.d.ts.map +1 -0
- package/dist/coerce/number.js +59 -0
- package/dist/coerce/number.js.map +1 -0
- package/dist/coerce/string.d.ts +35 -0
- package/dist/coerce/string.d.ts.map +1 -0
- package/dist/coerce/string.js +47 -0
- package/dist/coerce/string.js.map +1 -0
- package/dist/complex/array.d.ts +56 -0
- package/dist/complex/array.d.ts.map +1 -0
- package/dist/complex/array.js +96 -0
- package/dist/complex/array.js.map +1 -0
- package/dist/complex/object.d.ts +76 -0
- package/dist/complex/object.d.ts.map +1 -0
- package/dist/complex/object.js +104 -0
- package/dist/complex/object.js.map +1 -0
- package/dist/complex/record.d.ts +53 -0
- package/dist/complex/record.d.ts.map +1 -0
- package/dist/complex/record.js +109 -0
- package/dist/complex/record.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +60 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +280 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/primitives/any.d.ts +44 -0
- package/dist/primitives/any.d.ts.map +1 -0
- package/dist/primitives/any.js +57 -0
- package/dist/primitives/any.js.map +1 -0
- package/dist/primitives/boolean.d.ts +39 -0
- package/dist/primitives/boolean.d.ts.map +1 -0
- package/dist/primitives/boolean.js +53 -0
- package/dist/primitives/boolean.js.map +1 -0
- package/dist/primitives/null.d.ts +26 -0
- package/dist/primitives/null.d.ts.map +1 -0
- package/dist/primitives/null.js +40 -0
- package/dist/primitives/null.js.map +1 -0
- package/dist/primitives/number.d.ts +87 -0
- package/dist/primitives/number.d.ts.map +1 -0
- package/dist/primitives/number.js +129 -0
- package/dist/primitives/number.js.map +1 -0
- package/dist/primitives/string.d.ts +64 -0
- package/dist/primitives/string.d.ts.map +1 -0
- package/dist/primitives/string.js +102 -0
- package/dist/primitives/string.js.map +1 -0
- package/dist/primitives/undefined.d.ts +26 -0
- package/dist/primitives/undefined.d.ts.map +1 -0
- package/dist/primitives/undefined.js +40 -0
- package/dist/primitives/undefined.js.map +1 -0
- package/dist/primitives/unknown.d.ts +47 -0
- package/dist/primitives/unknown.d.ts.map +1 -0
- package/dist/primitives/unknown.js +56 -0
- package/dist/primitives/unknown.js.map +1 -0
- package/dist/utils/literal.d.ts +47 -0
- package/dist/utils/literal.d.ts.map +1 -0
- package/dist/utils/literal.js +64 -0
- package/dist/utils/literal.js.map +1 -0
- package/dist/utils/nullable.d.ts +50 -0
- package/dist/utils/nullable.d.ts.map +1 -0
- package/dist/utils/nullable.js +69 -0
- package/dist/utils/nullable.js.map +1 -0
- package/dist/utils/optional.d.ts +50 -0
- package/dist/utils/optional.d.ts.map +1 -0
- package/dist/utils/optional.js +69 -0
- package/dist/utils/optional.js.map +1 -0
- package/dist/utils/union.d.ts +60 -0
- package/dist/utils/union.d.ts.map +1 -0
- package/dist/utils/union.js +87 -0
- package/dist/utils/union.js.map +1 -0
- package/package.json +39 -0
- package/src/__tests__/coerce.test.ts +88 -0
- package/src/__tests__/complex.test.ts +124 -0
- package/src/__tests__/errors.test.ts +129 -0
- package/src/__tests__/json-schema.test.ts +138 -0
- package/src/__tests__/primitives.test.ts +184 -0
- package/src/__tests__/type-inference.test.ts +68 -0
- package/src/__tests__/utils.test.ts +100 -0
- package/src/base.ts +185 -0
- package/src/coerce/boolean.ts +56 -0
- package/src/coerce/date.ts +68 -0
- package/src/coerce/number.ts +67 -0
- package/src/coerce/string.ts +54 -0
- package/src/complex/array.ts +108 -0
- package/src/complex/object.ts +141 -0
- package/src/complex/record.ts +129 -0
- package/src/index.ts +177 -0
- package/src/json-schema.ts +331 -0
- package/src/primitives/any.ts +64 -0
- package/src/primitives/boolean.ts +60 -0
- package/src/primitives/null.ts +47 -0
- package/src/primitives/number.ts +141 -0
- package/src/primitives/string.ts +113 -0
- package/src/primitives/undefined.ts +47 -0
- package/src/primitives/unknown.ts +63 -0
- package/src/utils/literal.ts +71 -0
- package/src/utils/nullable.ts +80 -0
- package/src/utils/optional.ts +80 -0
- package/src/utils/union.ts +103 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Schema, Infer } from '../base';
|
|
2
|
+
import { createIssue, failure, success, createParseMethods } from '../base';
|
|
3
|
+
import { optional } from '../utils/optional';
|
|
4
|
+
import { nullable } from '../utils/nullable';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Schema for validating records (objects with string keys and typed values).
|
|
8
|
+
* Like TypeScript's Record<string, T> type.
|
|
9
|
+
*
|
|
10
|
+
* @template K - The key schema (must be string)
|
|
11
|
+
* @template V - The value schema
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const schema = s.record(s.string(), s.number());
|
|
16
|
+
* schema.parse({ a: 1, b: 2 }); // { a: 1, b: 2 }
|
|
17
|
+
* schema.parse({ a: 'invalid' }); // throws ValidationError
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
export class RecordSchema<K extends Schema<string, string>, V extends Schema<any, any>>
|
|
22
|
+
implements Schema<Record<Infer<K>, Infer<V>>, Record<Infer<K>, Infer<V>>>
|
|
23
|
+
{
|
|
24
|
+
description?: string;
|
|
25
|
+
private recordParseMethods = createParseMethods<Record<Infer<K>, Infer<V>>>();
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private keySchema: K,
|
|
29
|
+
private valueSchema: V
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
readonly '~standard' = {
|
|
33
|
+
version: 1 as const,
|
|
34
|
+
vendor: 'agentuity',
|
|
35
|
+
validate: (value: unknown) => {
|
|
36
|
+
if (value === null) {
|
|
37
|
+
return failure([createIssue('Expected record, got null')]);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return failure([createIssue('Expected record, got array')]);
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== 'object') {
|
|
43
|
+
return failure([createIssue(`Expected record, got ${typeof value}`)]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const result: Record<string, any> = {};
|
|
48
|
+
const issues: ReturnType<typeof createIssue>[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
51
|
+
// Validate key
|
|
52
|
+
const keyValidation = this.keySchema['~standard'].validate(key);
|
|
53
|
+
if (keyValidation instanceof Promise) {
|
|
54
|
+
throw new Error('Async validation not supported');
|
|
55
|
+
}
|
|
56
|
+
if (keyValidation.issues) {
|
|
57
|
+
for (const issue of keyValidation.issues) {
|
|
58
|
+
issues.push(createIssue(`Invalid key "${key}": ${issue.message}`, [key]));
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate value
|
|
64
|
+
const valueValidation = this.valueSchema['~standard'].validate(val);
|
|
65
|
+
if (valueValidation instanceof Promise) {
|
|
66
|
+
throw new Error('Async validation not supported');
|
|
67
|
+
}
|
|
68
|
+
if (valueValidation.issues) {
|
|
69
|
+
for (const issue of valueValidation.issues) {
|
|
70
|
+
issues.push(
|
|
71
|
+
createIssue(issue.message, issue.path ? [key, ...issue.path] : [key])
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
result[key] = valueValidation.value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (issues.length > 0) {
|
|
80
|
+
return failure(issues);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return success(result as Record<Infer<K>, Infer<V>>);
|
|
84
|
+
},
|
|
85
|
+
types: undefined as unknown as {
|
|
86
|
+
input: Record<Infer<K>, Infer<V>>;
|
|
87
|
+
output: Record<Infer<K>, Infer<V>>;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe(description: string): this {
|
|
92
|
+
this.description = description;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
optional() {
|
|
97
|
+
return optional(this);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
nullable() {
|
|
101
|
+
return nullable(this);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
parse = this.recordParseMethods.parse;
|
|
105
|
+
safeParse = this.recordParseMethods.safeParse;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a record schema for objects with string keys and typed values.
|
|
110
|
+
*
|
|
111
|
+
* @param keySchema - Schema for keys (typically s.string())
|
|
112
|
+
* @param valueSchema - Schema for values
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const configSchema = s.record(s.string(), s.number());
|
|
117
|
+
* const config = configSchema.parse({ timeout: 30, retries: 3 });
|
|
118
|
+
*
|
|
119
|
+
* const metadataSchema = s.record(s.string(), s.unknown());
|
|
120
|
+
* const metadata = metadataSchema.parse({ any: 'data', here: 123 });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
export function record<K extends Schema<string, string>, V extends Schema<any, any>>(
|
|
125
|
+
keySchema: K,
|
|
126
|
+
valueSchema: V
|
|
127
|
+
): RecordSchema<K, V> {
|
|
128
|
+
return new RecordSchema(keySchema, valueSchema);
|
|
129
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Schema,
|
|
3
|
+
Infer,
|
|
4
|
+
InferInput,
|
|
5
|
+
InferOutput,
|
|
6
|
+
ValidationIssue,
|
|
7
|
+
ValidationResult,
|
|
8
|
+
SafeParseResult,
|
|
9
|
+
SafeParseSuccess,
|
|
10
|
+
SafeParseError,
|
|
11
|
+
} from './base';
|
|
12
|
+
export { createIssue, success, failure, ValidationError } from './base';
|
|
13
|
+
|
|
14
|
+
export { StringSchema, string } from './primitives/string';
|
|
15
|
+
export { NumberSchema, number } from './primitives/number';
|
|
16
|
+
export { BooleanSchema, boolean } from './primitives/boolean';
|
|
17
|
+
export { NullSchema, null_ } from './primitives/null';
|
|
18
|
+
export { UndefinedSchema, undefined_ } from './primitives/undefined';
|
|
19
|
+
export { UnknownSchema, unknown } from './primitives/unknown';
|
|
20
|
+
export { AnySchema, any } from './primitives/any';
|
|
21
|
+
|
|
22
|
+
export { ObjectSchema, object } from './complex/object';
|
|
23
|
+
export { ArraySchema, array } from './complex/array';
|
|
24
|
+
export { RecordSchema, record } from './complex/record';
|
|
25
|
+
|
|
26
|
+
export { LiteralSchema, literal } from './utils/literal';
|
|
27
|
+
export { OptionalSchema, optional } from './utils/optional';
|
|
28
|
+
export { NullableSchema, nullable } from './utils/nullable';
|
|
29
|
+
export { UnionSchema, union } from './utils/union';
|
|
30
|
+
|
|
31
|
+
export { toJSONSchema, fromJSONSchema, type JSONSchema } from './json-schema';
|
|
32
|
+
|
|
33
|
+
export { CoerceStringSchema, coerceString } from './coerce/string';
|
|
34
|
+
export { CoerceNumberSchema, coerceNumber } from './coerce/number';
|
|
35
|
+
export { CoerceBooleanSchema, coerceBoolean } from './coerce/boolean';
|
|
36
|
+
export { CoerceDateSchema, coerceDate } from './coerce/date';
|
|
37
|
+
|
|
38
|
+
import { string } from './primitives/string';
|
|
39
|
+
import { number } from './primitives/number';
|
|
40
|
+
import { boolean } from './primitives/boolean';
|
|
41
|
+
import { null_ } from './primitives/null';
|
|
42
|
+
import { undefined_ } from './primitives/undefined';
|
|
43
|
+
import { unknown } from './primitives/unknown';
|
|
44
|
+
import { any } from './primitives/any';
|
|
45
|
+
import { object } from './complex/object';
|
|
46
|
+
import { array } from './complex/array';
|
|
47
|
+
import { record } from './complex/record';
|
|
48
|
+
import { literal } from './utils/literal';
|
|
49
|
+
import { optional } from './utils/optional';
|
|
50
|
+
import { nullable } from './utils/nullable';
|
|
51
|
+
import { union } from './utils/union';
|
|
52
|
+
import { toJSONSchema, fromJSONSchema } from './json-schema';
|
|
53
|
+
import { coerceString } from './coerce/string';
|
|
54
|
+
import { coerceNumber } from './coerce/number';
|
|
55
|
+
import { coerceBoolean } from './coerce/boolean';
|
|
56
|
+
import { coerceDate } from './coerce/date';
|
|
57
|
+
|
|
58
|
+
import type { Infer as InferType, Schema } from './base';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create an enum schema (union of literal values).
|
|
62
|
+
* Shorthand for creating a union of literals.
|
|
63
|
+
*
|
|
64
|
+
* @param values - Array of literal values
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const roleSchema = s.enum(['admin', 'user', 'guest']);
|
|
69
|
+
* const role = roleSchema.parse('admin'); // 'admin'
|
|
70
|
+
*
|
|
71
|
+
* // Equivalent to:
|
|
72
|
+
* s.union(s.literal('admin'), s.literal('user'), s.literal('guest'))
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function enumSchema<
|
|
76
|
+
T extends readonly [string | number | boolean, ...(string | number | boolean)[]],
|
|
77
|
+
>(values: T) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
return union(...values.map((v) => literal(v as any)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Main schema builder object.
|
|
84
|
+
* Provides access to all schema types and utilities.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { s } from '@agentuity/schema';
|
|
89
|
+
*
|
|
90
|
+
* // Define a schema
|
|
91
|
+
* const User = s.object({
|
|
92
|
+
* name: s.string(),
|
|
93
|
+
* age: s.number(),
|
|
94
|
+
* role: s.enum(['admin', 'user'])
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* // Extract type
|
|
98
|
+
* type User = s.infer<typeof User>;
|
|
99
|
+
*
|
|
100
|
+
* // Parse data
|
|
101
|
+
* const user = User.parse(data);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const s = {
|
|
105
|
+
/** Create a string schema */
|
|
106
|
+
string,
|
|
107
|
+
/** Create a number schema */
|
|
108
|
+
number,
|
|
109
|
+
/** Create a boolean schema */
|
|
110
|
+
boolean,
|
|
111
|
+
/** Create a null schema */
|
|
112
|
+
null: null_,
|
|
113
|
+
/** Create an undefined schema */
|
|
114
|
+
undefined: undefined_,
|
|
115
|
+
/** Create an unknown schema (accepts any value) */
|
|
116
|
+
unknown,
|
|
117
|
+
/** Create an any schema (accepts any value) */
|
|
118
|
+
any,
|
|
119
|
+
/** Create an object schema with typed properties */
|
|
120
|
+
object,
|
|
121
|
+
/** Create an array schema with typed elements */
|
|
122
|
+
array,
|
|
123
|
+
/** Create a record schema (object with string keys and typed values) */
|
|
124
|
+
record,
|
|
125
|
+
/** Create a literal value schema */
|
|
126
|
+
literal,
|
|
127
|
+
/** Make a schema optional (T | undefined) */
|
|
128
|
+
optional,
|
|
129
|
+
/** Make a schema nullable (T | null) */
|
|
130
|
+
nullable,
|
|
131
|
+
/** Create a union of schemas */
|
|
132
|
+
union,
|
|
133
|
+
/** Create an enum schema (union of literals) */
|
|
134
|
+
enum: enumSchema,
|
|
135
|
+
/** Convert schema to JSON Schema format */
|
|
136
|
+
toJSONSchema,
|
|
137
|
+
/** Convert JSON Schema to schema */
|
|
138
|
+
fromJSONSchema,
|
|
139
|
+
/** Coercion schemas for type conversion */
|
|
140
|
+
coerce: {
|
|
141
|
+
/** Coerce to string using String(value) */
|
|
142
|
+
string: coerceString,
|
|
143
|
+
/** Coerce to number using Number(value) */
|
|
144
|
+
number: coerceNumber,
|
|
145
|
+
/** Coerce to boolean using Boolean(value) */
|
|
146
|
+
boolean: coerceBoolean,
|
|
147
|
+
/** Coerce to Date using new Date(value) */
|
|
148
|
+
date: coerceDate,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Namespace for s.infer type extraction (like zod's z.infer).
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const User = s.object({ name: s.string(), age: s.number() });
|
|
158
|
+
* type User = s.infer<typeof User>;
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
162
|
+
export namespace s {
|
|
163
|
+
/**
|
|
164
|
+
* Extract the TypeScript type from a schema (like zod's z.infer).
|
|
165
|
+
*
|
|
166
|
+
* @template T - The schema type
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const Player = s.object({ username: s.string(), xp: s.number() });
|
|
171
|
+
* type Player = s.infer<typeof Player>;
|
|
172
|
+
* // { username: string; xp: number }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
export type infer<T extends Schema<any, any>> = InferType<T>;
|
|
177
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { Schema } from './base';
|
|
3
|
+
import { StringSchema, string } from './primitives/string';
|
|
4
|
+
import { NumberSchema, number } from './primitives/number';
|
|
5
|
+
import { BooleanSchema, boolean } from './primitives/boolean';
|
|
6
|
+
import { NullSchema, null_ } from './primitives/null';
|
|
7
|
+
import { UndefinedSchema } from './primitives/undefined';
|
|
8
|
+
import { ObjectSchema, object } from './complex/object';
|
|
9
|
+
import { ArraySchema, array } from './complex/array';
|
|
10
|
+
import { LiteralSchema, literal } from './utils/literal';
|
|
11
|
+
import { OptionalSchema, optional } from './utils/optional';
|
|
12
|
+
import { NullableSchema, nullable } from './utils/nullable';
|
|
13
|
+
import { UnionSchema, union } from './utils/union';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* JSON Schema object representation.
|
|
17
|
+
* Subset of JSON Schema Draft 7 specification.
|
|
18
|
+
*/
|
|
19
|
+
export interface JSONSchema {
|
|
20
|
+
type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
|
21
|
+
description?: string;
|
|
22
|
+
const?: string | number | boolean;
|
|
23
|
+
enum?: Array<string | number | boolean>;
|
|
24
|
+
properties?: Record<string, JSONSchema>;
|
|
25
|
+
required?: string[];
|
|
26
|
+
items?: JSONSchema;
|
|
27
|
+
anyOf?: JSONSchema[];
|
|
28
|
+
oneOf?: JSONSchema[];
|
|
29
|
+
allOf?: JSONSchema[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert a schema to JSON Schema format.
|
|
34
|
+
* Supports primitives, objects, arrays, unions, literals, optional, and nullable types.
|
|
35
|
+
*
|
|
36
|
+
* @param schema - The schema to convert
|
|
37
|
+
* @returns JSON Schema object
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const userSchema = s.object({
|
|
42
|
+
* name: s.string().describe('User name'),
|
|
43
|
+
* age: s.number().describe('User age')
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* const jsonSchema = s.toJSONSchema(userSchema);
|
|
47
|
+
* // { type: 'object', properties: {...}, required: [...] }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function toJSONSchema(schema: Schema<any, any>): JSONSchema {
|
|
51
|
+
const result: JSONSchema = {};
|
|
52
|
+
|
|
53
|
+
// Add description if available
|
|
54
|
+
if (schema.description) {
|
|
55
|
+
result.description = schema.description;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Primitive types
|
|
59
|
+
if (schema instanceof StringSchema) {
|
|
60
|
+
result.type = 'string';
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (schema instanceof NumberSchema) {
|
|
65
|
+
result.type = 'number';
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (schema instanceof BooleanSchema) {
|
|
70
|
+
result.type = 'boolean';
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (schema instanceof NullSchema) {
|
|
75
|
+
result.type = 'null';
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (schema instanceof UndefinedSchema) {
|
|
80
|
+
// JSON Schema doesn't have a direct "undefined" type
|
|
81
|
+
// We can represent it as an empty schema or omit the field
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Literal types
|
|
86
|
+
if (schema instanceof LiteralSchema) {
|
|
87
|
+
const value = (schema as any).value;
|
|
88
|
+
result.const = value;
|
|
89
|
+
if (typeof value === 'string') {
|
|
90
|
+
result.type = 'string';
|
|
91
|
+
} else if (typeof value === 'number') {
|
|
92
|
+
result.type = 'number';
|
|
93
|
+
} else if (typeof value === 'boolean') {
|
|
94
|
+
result.type = 'boolean';
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Object types
|
|
100
|
+
if (schema instanceof ObjectSchema) {
|
|
101
|
+
result.type = 'object';
|
|
102
|
+
const shape = (schema as any).shape;
|
|
103
|
+
result.properties = {};
|
|
104
|
+
result.required = [];
|
|
105
|
+
|
|
106
|
+
for (const [key, fieldSchema] of Object.entries(shape) as Array<[string, Schema<any, any>]>) {
|
|
107
|
+
result.properties[key] = toJSONSchema(fieldSchema);
|
|
108
|
+
|
|
109
|
+
// If the field is not optional, add it to required
|
|
110
|
+
if (!(fieldSchema instanceof OptionalSchema)) {
|
|
111
|
+
result.required.push(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove required if empty
|
|
116
|
+
if (result.required.length === 0) {
|
|
117
|
+
delete result.required;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Array types
|
|
124
|
+
if (schema instanceof ArraySchema) {
|
|
125
|
+
result.type = 'array';
|
|
126
|
+
const itemSchema = (schema as any).itemSchema;
|
|
127
|
+
result.items = toJSONSchema(itemSchema);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Optional types
|
|
132
|
+
if (schema instanceof OptionalSchema) {
|
|
133
|
+
const innerSchema = (schema as any).schema;
|
|
134
|
+
const innerJSON = toJSONSchema(innerSchema);
|
|
135
|
+
// Optional is typically handled at the object level via required array
|
|
136
|
+
return innerJSON;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Nullable types
|
|
140
|
+
if (schema instanceof NullableSchema) {
|
|
141
|
+
const innerSchema = (schema as any).schema;
|
|
142
|
+
const innerJSON = toJSONSchema(innerSchema);
|
|
143
|
+
// Nullable can be represented as anyOf with null
|
|
144
|
+
return {
|
|
145
|
+
anyOf: [innerJSON, { type: 'null' }],
|
|
146
|
+
...(schema.description && { description: schema.description }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Union types
|
|
151
|
+
if (schema instanceof UnionSchema) {
|
|
152
|
+
const schemas = (schema as any).schemas as Schema<any, any>[];
|
|
153
|
+
result.anyOf = schemas.map((schema) => toJSONSchema(schema));
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fallback for unknown schema types
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert a JSON Schema object to a schema.
|
|
163
|
+
* Supports round-trip conversion with toJSONSchema.
|
|
164
|
+
*
|
|
165
|
+
* @param jsonSchema - The JSON Schema object to convert
|
|
166
|
+
* @returns Schema instance
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```typescript
|
|
170
|
+
* const jsonSchema = {
|
|
171
|
+
* type: 'object',
|
|
172
|
+
* properties: {
|
|
173
|
+
* name: { type: 'string' },
|
|
174
|
+
* age: { type: 'number' }
|
|
175
|
+
* },
|
|
176
|
+
* required: ['name', 'age']
|
|
177
|
+
* };
|
|
178
|
+
*
|
|
179
|
+
* const schema = s.fromJSONSchema(jsonSchema);
|
|
180
|
+
* const user = schema.parse({ name: 'John', age: 30 });
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function fromJSONSchema(jsonSchema: JSONSchema): Schema<any, any> {
|
|
184
|
+
// Handle const (literal values)
|
|
185
|
+
if (jsonSchema.const !== undefined) {
|
|
186
|
+
const schema = literal(jsonSchema.const);
|
|
187
|
+
if (jsonSchema.description) {
|
|
188
|
+
schema.describe(jsonSchema.description);
|
|
189
|
+
}
|
|
190
|
+
return schema;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle anyOf (union or nullable)
|
|
194
|
+
if (jsonSchema.anyOf && Array.isArray(jsonSchema.anyOf)) {
|
|
195
|
+
// Check if it's a nullable pattern (anyOf with one schema and one null)
|
|
196
|
+
if (jsonSchema.anyOf.length === 2) {
|
|
197
|
+
const nullIndex = jsonSchema.anyOf.findIndex((s) => s.type === 'null');
|
|
198
|
+
if (nullIndex !== -1) {
|
|
199
|
+
const otherIndex = nullIndex === 0 ? 1 : 0;
|
|
200
|
+
const innerSchema = fromJSONSchema(jsonSchema.anyOf[otherIndex]);
|
|
201
|
+
const schema = nullable(innerSchema);
|
|
202
|
+
if (jsonSchema.description) {
|
|
203
|
+
schema.describe(jsonSchema.description);
|
|
204
|
+
}
|
|
205
|
+
return schema;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Otherwise treat as union
|
|
210
|
+
const schemas = jsonSchema.anyOf.map((s) => fromJSONSchema(s));
|
|
211
|
+
const schema = union(...schemas);
|
|
212
|
+
if (jsonSchema.description) {
|
|
213
|
+
schema.describe(jsonSchema.description);
|
|
214
|
+
}
|
|
215
|
+
return schema;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle oneOf (union)
|
|
219
|
+
if (jsonSchema.oneOf && Array.isArray(jsonSchema.oneOf)) {
|
|
220
|
+
const schemas = jsonSchema.oneOf.map((s) => fromJSONSchema(s));
|
|
221
|
+
const schema = union(...schemas);
|
|
222
|
+
if (jsonSchema.description) {
|
|
223
|
+
schema.describe(jsonSchema.description);
|
|
224
|
+
}
|
|
225
|
+
return schema;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle enum (union of literals)
|
|
229
|
+
if (jsonSchema.enum && Array.isArray(jsonSchema.enum)) {
|
|
230
|
+
const schemas = jsonSchema.enum.map((value) => literal(value as string | number | boolean));
|
|
231
|
+
const schema = union(...schemas);
|
|
232
|
+
if (jsonSchema.description) {
|
|
233
|
+
schema.describe(jsonSchema.description);
|
|
234
|
+
}
|
|
235
|
+
return schema;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle primitive types
|
|
239
|
+
switch (jsonSchema.type) {
|
|
240
|
+
case 'string': {
|
|
241
|
+
const schema = string();
|
|
242
|
+
if (jsonSchema.description) {
|
|
243
|
+
schema.describe(jsonSchema.description);
|
|
244
|
+
}
|
|
245
|
+
return schema;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'number':
|
|
249
|
+
case 'integer': {
|
|
250
|
+
const schema = number();
|
|
251
|
+
if (jsonSchema.description) {
|
|
252
|
+
schema.describe(jsonSchema.description);
|
|
253
|
+
}
|
|
254
|
+
return schema;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'boolean': {
|
|
258
|
+
const schema = boolean();
|
|
259
|
+
if (jsonSchema.description) {
|
|
260
|
+
schema.describe(jsonSchema.description);
|
|
261
|
+
}
|
|
262
|
+
return schema;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'null': {
|
|
266
|
+
const schema = null_();
|
|
267
|
+
if (jsonSchema.description) {
|
|
268
|
+
schema.describe(jsonSchema.description);
|
|
269
|
+
}
|
|
270
|
+
return schema;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
case 'array': {
|
|
274
|
+
if (!jsonSchema.items) {
|
|
275
|
+
throw new Error('Array type must have items property');
|
|
276
|
+
}
|
|
277
|
+
const itemSchema = fromJSONSchema(jsonSchema.items);
|
|
278
|
+
const schema = array(itemSchema);
|
|
279
|
+
if (jsonSchema.description) {
|
|
280
|
+
schema.describe(jsonSchema.description);
|
|
281
|
+
}
|
|
282
|
+
return schema;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'object': {
|
|
286
|
+
if (!jsonSchema.properties) {
|
|
287
|
+
// Empty object schema
|
|
288
|
+
const schema = object({});
|
|
289
|
+
if (jsonSchema.description) {
|
|
290
|
+
schema.describe(jsonSchema.description);
|
|
291
|
+
}
|
|
292
|
+
return schema;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const shape: Record<string, Schema<any, any>> = {};
|
|
296
|
+
const requiredFields = new Set(jsonSchema.required || []);
|
|
297
|
+
|
|
298
|
+
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
|
|
299
|
+
let fieldSchema = fromJSONSchema(propSchema);
|
|
300
|
+
|
|
301
|
+
// If field is not in required array, make it optional
|
|
302
|
+
if (!requiredFields.has(key)) {
|
|
303
|
+
fieldSchema = optional(fieldSchema);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
shape[key] = fieldSchema;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const schema = object(shape);
|
|
310
|
+
if (jsonSchema.description) {
|
|
311
|
+
schema.describe(jsonSchema.description);
|
|
312
|
+
}
|
|
313
|
+
return schema;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
default: {
|
|
317
|
+
// If no type is specified, try to infer from other properties
|
|
318
|
+
if (jsonSchema.properties) {
|
|
319
|
+
// Treat as object
|
|
320
|
+
return fromJSONSchema({ ...jsonSchema, type: 'object' });
|
|
321
|
+
}
|
|
322
|
+
if (jsonSchema.items) {
|
|
323
|
+
// Treat as array
|
|
324
|
+
return fromJSONSchema({ ...jsonSchema, type: 'array' });
|
|
325
|
+
}
|
|
326
|
+
// Fallback to string schema for unrecognized/untyped JSON Schema
|
|
327
|
+
// This provides a permissive default but may mask schema issues
|
|
328
|
+
return string();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Schema } from '../base';
|
|
2
|
+
import { success, createParseMethods } from '../base';
|
|
3
|
+
import { optional } from '../utils/optional';
|
|
4
|
+
import { nullable } from '../utils/nullable';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
const parseMethods = createParseMethods<any>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Schema that accepts any value with 'any' type.
|
|
11
|
+
* Returns the value as-is without validation or type safety.
|
|
12
|
+
* Use this sparingly - prefer unknown() for better type safety.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const schema = s.any();
|
|
17
|
+
* const value = schema.parse(123); // any
|
|
18
|
+
* const value2 = schema.parse('hello'); // any
|
|
19
|
+
*
|
|
20
|
+
* // No type checking required
|
|
21
|
+
* value.toUpperCase(); // No error, but may fail at runtime
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
export class AnySchema implements Schema<any, any> {
|
|
26
|
+
description?: string;
|
|
27
|
+
|
|
28
|
+
readonly '~standard' = {
|
|
29
|
+
version: 1 as const,
|
|
30
|
+
vendor: 'agentuity',
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
validate: (value: unknown) => success(value as any),
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
types: undefined as unknown as { input: any; output: any },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe(description: string): this {
|
|
38
|
+
this.description = description;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
optional() {
|
|
43
|
+
return optional(this);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
nullable() {
|
|
47
|
+
return nullable(this);
|
|
48
|
+
}
|
|
49
|
+
parse = parseMethods.parse;
|
|
50
|
+
safeParse = parseMethods.safeParse;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create an any schema that accepts any value.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const schema = s.any();
|
|
59
|
+
* const value = schema.parse(anything); // Type is any
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function any(): AnySchema {
|
|
63
|
+
return new AnySchema();
|
|
64
|
+
}
|