schema-to-library 0.0.2 → 0.0.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/README.md +3 -3
- package/dist/cli/index.d.ts +68 -0
- package/dist/cli/index.js +320 -0
- package/dist/helper/dep/resolve-schema-dependencies-from-shema.d.ts +2 -0
- package/dist/helper/dep/resolve-schema-dependencies-from-shema.js +89 -0
- package/dist/helper/dep/type.d.ts +2 -0
- package/dist/helper/dep/type.js +156 -0
- package/dist/helper/index.d.ts +3 -0
- package/dist/helper/index.js +3 -0
- package/dist/helper/utils/index.d.ts +15 -0
- package/dist/helper/utils/index.js +16 -0
- package/dist/zod/index.d.ts +2 -0
- package/dist/zod/index.js +103 -0
- package/dist/zod/zod/index.d.ts +6 -0
- package/dist/zod/zod/index.js +577 -0
- package/dist/zod.js +2 -2
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ npm install -D schema-to-library
|
|
|
6
6
|
|
|
7
7
|
## What is schema-to-library?
|
|
8
8
|
|
|
9
|
-
**schema-to-library** is a CLI tool that converts JSON Schema into code for validation libraries like Zod,
|
|
9
|
+
**[schema-to-library](https://www.npmjs.com/package/schema-to-library)** is a CLI tool that converts JSON Schema into code for validation libraries like Zod,
|
|
10
10
|
It helps you automatically generate type-safe validation schemas and TypeScript types from your existing schema definitions.
|
|
11
11
|
|
|
12
12
|
## Upcoming Support
|
|
@@ -24,7 +24,7 @@ Support for additional libraries is planned:
|
|
|
24
24
|
```bash
|
|
25
25
|
npx schema-to-zod path/to/input.{json,yaml} -o path/to/output.ts
|
|
26
26
|
```
|
|
27
|
-
|
|
27
|
+
-
|
|
28
28
|
### Example
|
|
29
29
|
|
|
30
30
|
input:
|
|
@@ -75,4 +75,4 @@ export type Schema = z.infer<typeof Schema>
|
|
|
75
75
|
|
|
76
76
|
## License
|
|
77
77
|
|
|
78
|
-
Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/schema-to-
|
|
78
|
+
Distributed under the MIT License. See [LICENSE](https://github.com/nakita628/schema-to-libray?tab=MIT-1-ov-file) for more information.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
export declare function cli(fn: (schema: Schema, rootName?: string) => string, helpText: string): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
value: string;
|
|
5
|
+
error?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
error: string | undefined;
|
|
9
|
+
value?: undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function parseYaml(i: string): {
|
|
12
|
+
ok: true;
|
|
13
|
+
value: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
declare const TypeSchema: v.UnionSchema<[v.LiteralSchema<"string", undefined>, v.LiteralSchema<"number", undefined>, v.LiteralSchema<"integer", undefined>, v.LiteralSchema<"date", undefined>, v.LiteralSchema<"boolean", undefined>, v.LiteralSchema<"array", undefined>, v.LiteralSchema<"object", undefined>, v.LiteralSchema<"null", undefined>], undefined>;
|
|
19
|
+
type Type = v.InferInput<typeof TypeSchema>;
|
|
20
|
+
export declare const FormatSchema: v.UnionSchema<[v.LiteralSchema<"email", undefined>, v.LiteralSchema<"uuid", undefined>, v.LiteralSchema<"uuidv4", undefined>, v.LiteralSchema<"uuidv6", undefined>, v.LiteralSchema<"uuidv7", undefined>, v.LiteralSchema<"uri", undefined>, v.LiteralSchema<"emoji", undefined>, v.LiteralSchema<"base64", undefined>, v.LiteralSchema<"base64url", undefined>, v.LiteralSchema<"nanoid", undefined>, v.LiteralSchema<"cuid", undefined>, v.LiteralSchema<"cuid2", undefined>, v.LiteralSchema<"ulid", undefined>, v.LiteralSchema<"ip", undefined>, v.LiteralSchema<"ipv4", undefined>, v.LiteralSchema<"ipv6", undefined>, v.LiteralSchema<"cidrv4", undefined>, v.LiteralSchema<"cidrv6", undefined>, v.LiteralSchema<"date", undefined>, v.LiteralSchema<"time", undefined>, v.LiteralSchema<"date-time", undefined>, v.LiteralSchema<"duration", undefined>, v.LiteralSchema<"binary", undefined>, v.LiteralSchema<"toLowerCase", undefined>, v.LiteralSchema<"toUpperCase", undefined>, v.LiteralSchema<"trim", undefined>, v.LiteralSchema<"jwt", undefined>, v.LiteralSchema<"int32", undefined>, v.LiteralSchema<"int64", undefined>, v.LiteralSchema<"bigint", undefined>, v.LiteralSchema<"float", undefined>, v.LiteralSchema<"float32", undefined>, v.LiteralSchema<"float64", undefined>, v.LiteralSchema<"double", undefined>, v.LiteralSchema<"decimal", undefined>], undefined>;
|
|
21
|
+
type Format = v.InferInput<typeof FormatSchema>;
|
|
22
|
+
type SchemaType = {
|
|
23
|
+
title?: string;
|
|
24
|
+
definitions?: Record<string, SchemaType>;
|
|
25
|
+
$defs?: Record<string, SchemaType>;
|
|
26
|
+
name?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
type?: Type | [Type, ...Type[]];
|
|
29
|
+
format?: Format;
|
|
30
|
+
pattern?: string;
|
|
31
|
+
minLength?: number;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
minimum?: number;
|
|
34
|
+
maximum?: number;
|
|
35
|
+
exclusiveMinimum?: number | boolean;
|
|
36
|
+
exclusiveMaximum?: number | boolean;
|
|
37
|
+
multipleOf?: number;
|
|
38
|
+
minItems?: number;
|
|
39
|
+
maxItems?: number;
|
|
40
|
+
default?: unknown;
|
|
41
|
+
example?: unknown;
|
|
42
|
+
examples?: unknown[];
|
|
43
|
+
properties?: Record<string, SchemaType>;
|
|
44
|
+
required?: string[] | boolean;
|
|
45
|
+
items?: SchemaType;
|
|
46
|
+
enum?: (string | number | boolean | null | (string | number | boolean | null)[])[];
|
|
47
|
+
nullable?: boolean;
|
|
48
|
+
additionalProperties?: SchemaType | boolean;
|
|
49
|
+
$ref?: string;
|
|
50
|
+
xml?: {
|
|
51
|
+
name?: string;
|
|
52
|
+
wrapped?: boolean;
|
|
53
|
+
};
|
|
54
|
+
oneOf?: SchemaType[];
|
|
55
|
+
allOf?: SchemaType[];
|
|
56
|
+
anyOf?: SchemaType[];
|
|
57
|
+
not?: SchemaType;
|
|
58
|
+
discriminator?: {
|
|
59
|
+
propertyName?: string;
|
|
60
|
+
};
|
|
61
|
+
externalDocs?: {
|
|
62
|
+
url?: string;
|
|
63
|
+
};
|
|
64
|
+
const?: unknown;
|
|
65
|
+
};
|
|
66
|
+
declare const Schema: v.GenericSchema<SchemaType>;
|
|
67
|
+
export type Schema = v.InferInput<typeof Schema>;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { format } from 'prettier';
|
|
4
|
+
import * as v from 'valibot';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
const IOSchema = v.object({
|
|
7
|
+
input: v.custom((value) => typeof value === 'string' && (value.endsWith('.yaml') || value.endsWith('.json')), 'Input must be a .json, or .yaml file'),
|
|
8
|
+
output: v.custom((value) => typeof value === 'string' && value.endsWith('.ts'), 'Output must be a .ts file'),
|
|
9
|
+
});
|
|
10
|
+
const IsYAMLSchema = v.custom((value) => typeof value === 'string' && value.endsWith('.yaml'), 'Must end with .yaml');
|
|
11
|
+
const IsJSONSchema = v.custom((value) => typeof value === 'string' && value.endsWith('.json'), 'Must end with .json');
|
|
12
|
+
export async function cli(fn, helpText) {
|
|
13
|
+
// Slice the arguments to remove the first two (node and script path)
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const isHelpRequested = (args) => {
|
|
16
|
+
return args.length === 1 && (args[0] === '--help' || args[0] === '-h');
|
|
17
|
+
};
|
|
18
|
+
if (isHelpRequested(args)) {
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
value: helpText,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const i = args[0];
|
|
25
|
+
const oIdx = args.indexOf('-o');
|
|
26
|
+
const o = oIdx !== -1 ? args[oIdx + 1] : undefined;
|
|
27
|
+
const valid = v.safeParse(IOSchema, {
|
|
28
|
+
input: i,
|
|
29
|
+
output: o,
|
|
30
|
+
});
|
|
31
|
+
if (!valid.success) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: valid.issues.map((issue) => issue.message)[0],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const { input, output } = valid.output;
|
|
38
|
+
const schemaResult = await parseSchema(input);
|
|
39
|
+
if (!schemaResult.ok) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: schemaResult.error,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const schemaValid = v.safeParse(Schema, schemaResult.value);
|
|
46
|
+
if (!schemaValid.success) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: schemaValid.issues.map((issue) => issue.message)[0],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const schema = schemaValid.output;
|
|
53
|
+
const result = fn(schema);
|
|
54
|
+
const fmtResult = await fmt(result);
|
|
55
|
+
if (!fmtResult.ok) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: fmtResult.error,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const mkdirResult = await mkdir(path.dirname(output));
|
|
62
|
+
if (!mkdirResult.ok) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: mkdirResult.error,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const writeFileResult = await writeFile(output, fmtResult.value);
|
|
69
|
+
if (!writeFileResult.ok) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: writeFileResult.error,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
value: `${output} created`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function parseSchema(i) {
|
|
81
|
+
if (i.endsWith('.yaml')) {
|
|
82
|
+
const valid = v.safeParse(IsYAMLSchema, i);
|
|
83
|
+
if (!valid.success) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: valid.issues.map((issue) => issue.message)[0],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const input = valid.output;
|
|
90
|
+
const file = await readFile(input);
|
|
91
|
+
if (!file.ok) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: file.error,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const yaml = parseYaml(file.value);
|
|
98
|
+
if (!yaml.ok) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: yaml.error,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
value: yaml.value,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (i.endsWith('.json')) {
|
|
110
|
+
const valid = v.safeParse(IsJSONSchema, i);
|
|
111
|
+
if (!valid.success) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: valid.issues.map((issue) => issue.message)[0],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const input = valid.output;
|
|
118
|
+
const file = await readFile(input);
|
|
119
|
+
if (!file.ok) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: file.error,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
value: JSON.parse(file.value),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: 'Invalid input file type',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// parseYaml
|
|
136
|
+
export function parseYaml(i) {
|
|
137
|
+
try {
|
|
138
|
+
const yaml = parse(i);
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
value: yaml,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: String(e),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// readFile
|
|
152
|
+
async function readFile(path) {
|
|
153
|
+
try {
|
|
154
|
+
const res = await fsp.readFile(path, 'utf-8');
|
|
155
|
+
return { ok: true, value: res };
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: e instanceof Error ? e.message : String(e),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Formats TypeScript source with Prettier.
|
|
166
|
+
*
|
|
167
|
+
* @param code - Source code to format.
|
|
168
|
+
* @returns A `Result` containing the formatted code or an error message.
|
|
169
|
+
*/
|
|
170
|
+
async function fmt(code) {
|
|
171
|
+
try {
|
|
172
|
+
const formatted = await format(code, {
|
|
173
|
+
parser: 'typescript',
|
|
174
|
+
printWidth: 100,
|
|
175
|
+
singleQuote: true,
|
|
176
|
+
semi: false,
|
|
177
|
+
});
|
|
178
|
+
return { ok: true, value: formatted };
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: e instanceof Error ? e.message : String(e),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Creates a directory if it does not already exist.
|
|
189
|
+
*
|
|
190
|
+
* @param dir - Directory path to create.
|
|
191
|
+
* @returns A `Result` that is `ok` on success, otherwise an error message.
|
|
192
|
+
*/
|
|
193
|
+
async function mkdir(dir) {
|
|
194
|
+
try {
|
|
195
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
value: undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: e instanceof Error ? e.message : String(e),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Writes UTF-8 text to a file, creating it if necessary.
|
|
210
|
+
*
|
|
211
|
+
* @param path - File path to write.
|
|
212
|
+
* @param data - Text data to write.
|
|
213
|
+
* @returns A `Result` that is `ok` on success, otherwise an error message.
|
|
214
|
+
*/
|
|
215
|
+
async function writeFile(path, data) {
|
|
216
|
+
try {
|
|
217
|
+
await fsp.writeFile(path, data, 'utf-8');
|
|
218
|
+
return { ok: true, value: undefined };
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const TypeSchema = v.union([
|
|
225
|
+
v.literal('string'),
|
|
226
|
+
v.literal('number'),
|
|
227
|
+
v.literal('integer'),
|
|
228
|
+
v.literal('date'),
|
|
229
|
+
v.literal('boolean'),
|
|
230
|
+
v.literal('array'),
|
|
231
|
+
v.literal('object'),
|
|
232
|
+
v.literal('null'),
|
|
233
|
+
]);
|
|
234
|
+
export const FormatSchema = v.union([
|
|
235
|
+
v.literal('email'),
|
|
236
|
+
v.literal('uuid'),
|
|
237
|
+
v.literal('uuidv4'),
|
|
238
|
+
v.literal('uuidv6'),
|
|
239
|
+
v.literal('uuidv7'),
|
|
240
|
+
v.literal('uri'),
|
|
241
|
+
v.literal('emoji'),
|
|
242
|
+
v.literal('base64'),
|
|
243
|
+
v.literal('base64url'),
|
|
244
|
+
v.literal('nanoid'),
|
|
245
|
+
v.literal('cuid'),
|
|
246
|
+
v.literal('cuid2'),
|
|
247
|
+
v.literal('ulid'),
|
|
248
|
+
v.literal('ip'),
|
|
249
|
+
v.literal('ipv4'),
|
|
250
|
+
v.literal('ipv6'),
|
|
251
|
+
v.literal('cidrv4'),
|
|
252
|
+
v.literal('cidrv6'),
|
|
253
|
+
v.literal('date'),
|
|
254
|
+
v.literal('time'),
|
|
255
|
+
v.literal('date-time'),
|
|
256
|
+
v.literal('duration'),
|
|
257
|
+
v.literal('binary'),
|
|
258
|
+
v.literal('toLowerCase'),
|
|
259
|
+
v.literal('toUpperCase'),
|
|
260
|
+
v.literal('trim'),
|
|
261
|
+
v.literal('jwt'),
|
|
262
|
+
v.literal('int32'),
|
|
263
|
+
v.literal('int64'),
|
|
264
|
+
v.literal('bigint'),
|
|
265
|
+
v.literal('float'),
|
|
266
|
+
v.literal('float32'),
|
|
267
|
+
v.literal('float64'),
|
|
268
|
+
v.literal('double'),
|
|
269
|
+
v.literal('decimal'),
|
|
270
|
+
]);
|
|
271
|
+
const Schema = v.looseObject({
|
|
272
|
+
title: v.optional(v.string()),
|
|
273
|
+
definitions: v.optional(v.record(v.string(), v.lazy(() => Schema))),
|
|
274
|
+
$defs: v.optional(v.record(v.string(), v.lazy(() => Schema))),
|
|
275
|
+
name: v.optional(v.string()),
|
|
276
|
+
description: v.optional(v.string()),
|
|
277
|
+
type: v.optional(v.union([TypeSchema, v.tuple([TypeSchema])])),
|
|
278
|
+
format: v.optional(FormatSchema),
|
|
279
|
+
pattern: v.optional(v.string()),
|
|
280
|
+
minLength: v.optional(v.number()),
|
|
281
|
+
maxLength: v.optional(v.number()),
|
|
282
|
+
minimum: v.optional(v.number()),
|
|
283
|
+
maximum: v.optional(v.number()),
|
|
284
|
+
exclusiveMinimum: v.optional(v.union([v.number(), v.boolean()])),
|
|
285
|
+
exclusiveMaximum: v.optional(v.union([v.number(), v.boolean()])),
|
|
286
|
+
multipleOf: v.optional(v.number()),
|
|
287
|
+
minItems: v.optional(v.number()),
|
|
288
|
+
maxItems: v.optional(v.number()),
|
|
289
|
+
default: v.optional(v.unknown()),
|
|
290
|
+
example: v.optional(v.unknown()),
|
|
291
|
+
examples: v.optional(v.array(v.unknown())),
|
|
292
|
+
properties: v.optional(v.record(v.string(), v.lazy(() => Schema))),
|
|
293
|
+
required: v.optional(v.union([v.array(v.string()), v.boolean()])),
|
|
294
|
+
items: v.optional(v.lazy(() => Schema)),
|
|
295
|
+
enum: v.optional(v.array(v.union([
|
|
296
|
+
v.string(),
|
|
297
|
+
v.number(),
|
|
298
|
+
v.boolean(),
|
|
299
|
+
v.null(),
|
|
300
|
+
v.array(v.union([v.string(), v.number(), v.boolean(), v.null()])),
|
|
301
|
+
]))),
|
|
302
|
+
nullable: v.optional(v.boolean()),
|
|
303
|
+
additionalProperties: v.optional(v.union([v.lazy(() => Schema), v.boolean()])),
|
|
304
|
+
$ref: v.optional(v.string()),
|
|
305
|
+
xml: v.optional(v.object({
|
|
306
|
+
name: v.optional(v.string()),
|
|
307
|
+
wrapped: v.optional(v.boolean()),
|
|
308
|
+
})),
|
|
309
|
+
oneOf: v.optional(v.array(v.lazy(() => Schema))),
|
|
310
|
+
allOf: v.optional(v.array(v.lazy(() => Schema))),
|
|
311
|
+
anyOf: v.optional(v.array(v.lazy(() => Schema))),
|
|
312
|
+
not: v.optional(v.lazy(() => Schema)),
|
|
313
|
+
discriminator: v.optional(v.object({
|
|
314
|
+
propertyName: v.optional(v.string()),
|
|
315
|
+
})),
|
|
316
|
+
externalDocs: v.optional(v.object({
|
|
317
|
+
url: v.optional(v.string()),
|
|
318
|
+
})),
|
|
319
|
+
const: v.optional(v.unknown()),
|
|
320
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export function resolveSchemaDependenciesFromSchema(schema) {
|
|
2
|
+
// Merge both definitions and $defs
|
|
3
|
+
const definitions = {
|
|
4
|
+
...(schema.definitions ?? {}),
|
|
5
|
+
...(schema.$defs ?? {}),
|
|
6
|
+
};
|
|
7
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
8
|
+
const collectRefs = (schema) => {
|
|
9
|
+
const refs = new Set();
|
|
10
|
+
const stack = [schema];
|
|
11
|
+
while (stack.length > 0) {
|
|
12
|
+
const node = stack.pop();
|
|
13
|
+
if (!isRecord(node))
|
|
14
|
+
continue;
|
|
15
|
+
if ('$ref' in node && typeof node.$ref === 'string') {
|
|
16
|
+
const ref = node.$ref;
|
|
17
|
+
if (ref === '#')
|
|
18
|
+
continue;
|
|
19
|
+
// Check for both definitions and $defs refs
|
|
20
|
+
const match = ref.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/);
|
|
21
|
+
if (match) {
|
|
22
|
+
refs.add(match[1]);
|
|
23
|
+
}
|
|
24
|
+
// Check for relative references like #node
|
|
25
|
+
const relativeMatch = ref.match(/^#([^/]+)$/);
|
|
26
|
+
if (relativeMatch) {
|
|
27
|
+
refs.add(relativeMatch[1]);
|
|
28
|
+
}
|
|
29
|
+
// Check for external file references with fragments
|
|
30
|
+
if (ref.includes('#')) {
|
|
31
|
+
const [filePath, fragment] = ref.split('#');
|
|
32
|
+
if (fragment) {
|
|
33
|
+
// Extract the schema name from the fragment
|
|
34
|
+
const fragmentMatch = fragment.match(/^\/(?:definitions|\$defs)\/([^/]+)$/);
|
|
35
|
+
if (fragmentMatch) {
|
|
36
|
+
refs.add(fragmentMatch[1]);
|
|
37
|
+
}
|
|
38
|
+
// Handle simple fragment like "#node"
|
|
39
|
+
const simpleMatch = fragment.match(/^\/([^/]+)$/);
|
|
40
|
+
if (simpleMatch) {
|
|
41
|
+
refs.add(simpleMatch[1]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Skip external references that we can't resolve
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const value of Object.values(node)) {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
if (isRecord(item))
|
|
52
|
+
stack.push(item);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (isRecord(value)) {
|
|
56
|
+
stack.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Array.from(refs).sort();
|
|
61
|
+
};
|
|
62
|
+
const sorted = [];
|
|
63
|
+
const perm = new Set();
|
|
64
|
+
const temp = new Set();
|
|
65
|
+
const visit = (name) => {
|
|
66
|
+
if (perm.has(name))
|
|
67
|
+
return;
|
|
68
|
+
if (temp.has(name)) {
|
|
69
|
+
// Circular dependency detected - skip this dependency but continue processing
|
|
70
|
+
// console.warn(`Warning: Circular dependency detected for type "${name}", skipping...`)
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const schema = definitions[name];
|
|
74
|
+
if (!schema)
|
|
75
|
+
return;
|
|
76
|
+
temp.add(name);
|
|
77
|
+
for (const ref of collectRefs(schema)) {
|
|
78
|
+
if (ref in definitions)
|
|
79
|
+
visit(ref);
|
|
80
|
+
}
|
|
81
|
+
temp.delete(name);
|
|
82
|
+
perm.add(name);
|
|
83
|
+
sorted.push(name);
|
|
84
|
+
};
|
|
85
|
+
for (const name of Object.keys(definitions).sort()) {
|
|
86
|
+
visit(name);
|
|
87
|
+
}
|
|
88
|
+
return sorted;
|
|
89
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { normalizeTypes, toPascalCase } from '../utils/index.js';
|
|
2
|
+
export function type(schema, rootName = 'Schema') {
|
|
3
|
+
if (schema === undefined)
|
|
4
|
+
return '';
|
|
5
|
+
// $ref case
|
|
6
|
+
if (schema.$ref) {
|
|
7
|
+
if (schema.$ref === '#' || schema.$ref === '') {
|
|
8
|
+
return `z.infer<typeof ${rootName}>`;
|
|
9
|
+
}
|
|
10
|
+
const TABLE = [
|
|
11
|
+
['#/components/schemas/', 'Schema'],
|
|
12
|
+
['#/definitions/', 'Schema'],
|
|
13
|
+
['#/$defs/', 'Schema'],
|
|
14
|
+
];
|
|
15
|
+
for (const [prefix] of TABLE) {
|
|
16
|
+
if (schema.$ref?.startsWith(prefix)) {
|
|
17
|
+
const name = schema.$ref.slice(prefix.length);
|
|
18
|
+
const pascalCaseName = toPascalCase(name);
|
|
19
|
+
// For self-references, use the type name directly
|
|
20
|
+
if (pascalCaseName === rootName) {
|
|
21
|
+
return `${pascalCaseName}Type`;
|
|
22
|
+
}
|
|
23
|
+
// For other references, use the type name directly to avoid circular references
|
|
24
|
+
return `${pascalCaseName}Type`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Handle relative references like #animal
|
|
28
|
+
if (schema.$ref?.startsWith('#')) {
|
|
29
|
+
const refName = schema.$ref.slice(1); // Remove the leading #
|
|
30
|
+
if (refName === '') {
|
|
31
|
+
return `z.infer<typeof ${rootName}>`;
|
|
32
|
+
}
|
|
33
|
+
// Only handle simple references like #animal, not complex paths
|
|
34
|
+
if (!refName.includes('/')) {
|
|
35
|
+
const pascalCaseName = toPascalCase(refName);
|
|
36
|
+
return `${pascalCaseName}Type`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Handle external file references with fragments
|
|
40
|
+
if (schema.$ref?.includes('#')) {
|
|
41
|
+
const [filePath, fragment] = schema.$ref.split('#');
|
|
42
|
+
// All external references are unknown
|
|
43
|
+
return 'unknown';
|
|
44
|
+
}
|
|
45
|
+
// Handle HTTP references without fragments
|
|
46
|
+
if (schema.$ref?.startsWith('http')) {
|
|
47
|
+
return 'unknown';
|
|
48
|
+
}
|
|
49
|
+
return 'unknown';
|
|
50
|
+
}
|
|
51
|
+
// combinators
|
|
52
|
+
if (schema.oneOf)
|
|
53
|
+
return union(schema.oneOf, rootName);
|
|
54
|
+
if (schema.anyOf)
|
|
55
|
+
return union(schema.anyOf, rootName);
|
|
56
|
+
if (schema.allOf)
|
|
57
|
+
return intersection(schema.allOf, rootName);
|
|
58
|
+
if (schema.not)
|
|
59
|
+
return 'unknown';
|
|
60
|
+
// const
|
|
61
|
+
if (schema.const !== undefined) {
|
|
62
|
+
return typeof schema.const === 'string' ? `"${schema.const}"` : String(schema.const);
|
|
63
|
+
}
|
|
64
|
+
// enum
|
|
65
|
+
if (schema.enum) {
|
|
66
|
+
if (schema.enum.length === 1) {
|
|
67
|
+
const v = schema.enum[0];
|
|
68
|
+
return typeof v === 'string' ? `"${v}"` : String(v);
|
|
69
|
+
}
|
|
70
|
+
const allStrings = schema.enum.every((v) => typeof v === 'string');
|
|
71
|
+
if (allStrings) {
|
|
72
|
+
return `(${schema.enum.map((v) => `"${v}"`).join(' | ')})`;
|
|
73
|
+
}
|
|
74
|
+
return `(${schema.enum.map((v) => (typeof v === 'string' ? `"${v}"` : String(v))).join(' | ')})`;
|
|
75
|
+
}
|
|
76
|
+
// properties
|
|
77
|
+
if (schema.properties)
|
|
78
|
+
return object(schema, rootName);
|
|
79
|
+
const t = normalizeTypes(schema.type);
|
|
80
|
+
// primitive types
|
|
81
|
+
if (t.includes('string'))
|
|
82
|
+
return 'string';
|
|
83
|
+
if (t.includes('number'))
|
|
84
|
+
return 'number';
|
|
85
|
+
if (t.includes('integer'))
|
|
86
|
+
return 'number';
|
|
87
|
+
if (t.includes('boolean'))
|
|
88
|
+
return 'boolean';
|
|
89
|
+
if (t.includes('array'))
|
|
90
|
+
return array(schema, rootName);
|
|
91
|
+
if (t.includes('object'))
|
|
92
|
+
return object(schema, rootName);
|
|
93
|
+
if (t.includes('date'))
|
|
94
|
+
return 'Date';
|
|
95
|
+
if (t.length === 1 && t[0] === 'null')
|
|
96
|
+
return 'null';
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
function union(schemas, rootName) {
|
|
100
|
+
const types = schemas.map((s) => type(s, rootName));
|
|
101
|
+
return `(${types.join(' | ')})`;
|
|
102
|
+
}
|
|
103
|
+
function intersection(schemas, rootName) {
|
|
104
|
+
const types = schemas
|
|
105
|
+
.filter((s) => {
|
|
106
|
+
// null type is excluded
|
|
107
|
+
if (s.type === 'null')
|
|
108
|
+
return false;
|
|
109
|
+
// nullable: true only is excluded
|
|
110
|
+
if (s.nullable === true && Object.keys(s).length === 1)
|
|
111
|
+
return false;
|
|
112
|
+
// simple properties (default, const, etc.) are excluded
|
|
113
|
+
if (Object.keys(s).length === 1 && (s.default !== undefined || s.const !== undefined))
|
|
114
|
+
return false;
|
|
115
|
+
return true;
|
|
116
|
+
})
|
|
117
|
+
.map((s) => type(s, rootName));
|
|
118
|
+
if (types.length === 0)
|
|
119
|
+
return 'unknown';
|
|
120
|
+
if (types.length === 1)
|
|
121
|
+
return types[0];
|
|
122
|
+
return `(${types.join(' & ')})`;
|
|
123
|
+
}
|
|
124
|
+
function array(schema, rootName) {
|
|
125
|
+
if (schema.items) {
|
|
126
|
+
const itemType = type(schema.items, rootName);
|
|
127
|
+
return `${itemType}[]`;
|
|
128
|
+
}
|
|
129
|
+
return 'unknown[]';
|
|
130
|
+
}
|
|
131
|
+
function object(schema, rootName) {
|
|
132
|
+
if (!schema.properties) {
|
|
133
|
+
if (schema.additionalProperties) {
|
|
134
|
+
if (typeof schema.additionalProperties === 'boolean') {
|
|
135
|
+
return schema.additionalProperties ? 'Record<string, unknown>' : 'Record<string, never>';
|
|
136
|
+
}
|
|
137
|
+
const valueType = type(schema.additionalProperties, rootName);
|
|
138
|
+
return `Record<string, ${valueType}>`;
|
|
139
|
+
}
|
|
140
|
+
return 'Record<string, unknown>';
|
|
141
|
+
}
|
|
142
|
+
const properties = [];
|
|
143
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
144
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
145
|
+
const propType = type(propSchema, rootName);
|
|
146
|
+
const isRequired = required.includes(key);
|
|
147
|
+
const safeKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `"${key}"`;
|
|
148
|
+
if (isRequired) {
|
|
149
|
+
properties.push(`${safeKey}: ${propType}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
properties.push(`${safeKey}?: ${propType}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return `{${properties.join('; ')}}`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param name
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export declare function toPascalCase(name: string): string;
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param t
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeTypes(t?: 'string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null' | [
|
|
13
|
+
'string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null',
|
|
14
|
+
...('string' | 'number' | 'integer' | 'date' | 'boolean' | 'array' | 'object' | 'null')[]
|
|
15
|
+
]): ("string" | "number" | "boolean" | "object" | "integer" | "date" | "array" | "null")[];
|