@huanglangjian/specs 0.4.0 → 0.5.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/dist/index.mjs CHANGED
@@ -1,20 +1,11 @@
1
- import { resolve } from "pathe";
2
- import { format } from "prettier";
3
- import PrettierJavaPlugin from "prettier-plugin-java";
4
- import PrettierRustPlugin from "prettier-plugin-rust";
5
- //#region src/type-system/basic.ts
1
+ import { camelCase, pascalCase } from "text-case";
2
+ //#region src/types.ts
6
3
  function int32(options) {
7
4
  return {
8
5
  kind: "int32",
9
6
  ...options
10
7
  };
11
8
  }
12
- function int64(options) {
13
- return {
14
- kind: "int64",
15
- ...options
16
- };
17
- }
18
9
  function float32(options) {
19
10
  return {
20
11
  kind: "float32",
@@ -39,56 +30,54 @@ function string(options) {
39
30
  ...options
40
31
  };
41
32
  }
42
- function literal(options) {
33
+ function array(options) {
43
34
  return {
44
- kind: "literal",
35
+ kind: "array",
45
36
  ...options
46
37
  };
47
38
  }
48
- /**
49
- * 创建一个可选类型模型。
50
- *
51
- * @param base 基础模型类型
52
- * @param options 可选配置选项
53
- * @returns 可选类型模型
54
- */
55
- function optional(base, options) {
39
+ function set(options) {
56
40
  return {
57
- kind: "optional",
58
- base,
41
+ kind: "set",
59
42
  ...options
60
43
  };
61
44
  }
62
- function array(base, options) {
45
+ function map(options) {
63
46
  return {
64
- kind: "array",
65
- base,
47
+ kind: "map",
66
48
  ...options
67
49
  };
68
50
  }
69
- function set(base, options) {
51
+ function record(options) {
70
52
  return {
71
- kind: "set",
72
- base,
53
+ kind: "record",
73
54
  ...options
74
55
  };
75
56
  }
76
- function map(base, options) {
57
+ function union(options) {
77
58
  return {
78
- kind: "map",
79
- base,
59
+ kind: "union",
80
60
  ...options
81
61
  };
82
62
  }
83
- function record(options) {
63
+ function taggedUnion(options) {
84
64
  return {
85
- kind: "record",
65
+ kind: "taggedUnion",
86
66
  ...options
87
67
  };
88
68
  }
89
- function union(options) {
69
+ function literal(value) {
90
70
  return {
91
- kind: "union",
71
+ kind: "literal",
72
+ value
73
+ };
74
+ }
75
+ function nullLike() {
76
+ return { kind: "null" };
77
+ }
78
+ function enums(options) {
79
+ return {
80
+ kind: "enums",
92
81
  ...options
93
82
  };
94
83
  }
@@ -110,904 +99,1456 @@ function duration(options) {
110
99
  ...options
111
100
  };
112
101
  }
113
- function error(options) {
102
+ //#endregion
103
+ //#region src/api.ts
104
+ function route(options) {
114
105
  return {
115
- kind: "error",
106
+ kind: "route",
116
107
  ...options
117
108
  };
118
109
  }
119
- function enums(options) {
110
+ function json(options) {
120
111
  return {
121
- kind: "enums",
112
+ kind: "json-response",
122
113
  ...options
123
114
  };
124
115
  }
125
- function taggedUnion(options) {
116
+ function jsonStream(options) {
117
+ return {
118
+ kind: "stream-response",
119
+ ...options
120
+ };
121
+ }
122
+ function sseStream(options) {
126
123
  return {
127
- kind: "tagged-union",
124
+ kind: "sse-response",
128
125
  ...options
129
126
  };
130
127
  }
128
+ function binary(options) {
129
+ return {
130
+ kind: "binary",
131
+ ...options
132
+ };
133
+ }
134
+ //#endregion
135
+ //#region src/security.ts
136
+ function apikey(options) {
137
+ const component = {
138
+ kind: "apikey",
139
+ ...options
140
+ };
141
+ return {
142
+ ...component,
143
+ apply: () => ({
144
+ component,
145
+ scopes: []
146
+ })
147
+ };
148
+ }
149
+ function openIdConnect(options) {
150
+ const component = {
151
+ kind: "openIdConnect",
152
+ ...options
153
+ };
154
+ return {
155
+ ...component,
156
+ apply: (...scopes) => ({
157
+ component,
158
+ scopes
159
+ })
160
+ };
161
+ }
162
+ //#endregion
163
+ //#region src/deployment.ts
164
+ function deployOpenIdConnect(options) {
165
+ return {
166
+ kind: "openIdConnectDeployment",
167
+ component: options.component,
168
+ issuer: options.issuer
169
+ };
170
+ }
171
+ //#endregion
172
+ //#region src/generate-jsonschema.ts
131
173
  function generateJsonSchema(options) {
132
- const { model, target = "draft-2020-12", reference = "json-schema", depth = 0 } = options ?? {};
133
- const $schema = getSchemaRef(target);
134
- const convert = (partial) => ({
135
- $schema: depth === 0 ? $schema : void 0,
136
- title: model.title,
137
- description: model.description,
138
- examples: model.examples,
139
- deprecated: model.deprecated,
140
- default: model.default,
141
- ...partial,
142
- ..."schema" in model ? model.schema : {}
143
- });
144
- const getReferenceSchema = (schema) => {
145
- switch (reference) {
146
- case "inline": return generateJsonSchema({
147
- model: schema,
148
- target,
149
- reference,
150
- depth: depth + 1
151
- });
152
- case "json-schema": return schema.id != null ? { $ref: "#/$defs/" + schema.id } : generateJsonSchema({
153
- model: schema,
154
- target,
155
- reference,
156
- depth: depth + 1
174
+ const { model, toJsonSchema = () => ({}) } = options;
175
+ const registry = options.registry ?? createJsonSchemaRegistry();
176
+ const schema = toJsonSchema(model.schema);
177
+ switch (model.kind) {
178
+ case "int32": return {
179
+ jsonSchema: {
180
+ ...schema,
181
+ type: "integer",
182
+ format: "int32"
183
+ },
184
+ registry
185
+ };
186
+ case "float32": return {
187
+ jsonSchema: {
188
+ ...schema,
189
+ type: "number",
190
+ format: "float"
191
+ },
192
+ registry
193
+ };
194
+ case "float64": return {
195
+ jsonSchema: {
196
+ ...schema,
197
+ type: "number",
198
+ format: "float"
199
+ },
200
+ registry
201
+ };
202
+ case "boolean": return {
203
+ jsonSchema: {
204
+ ...schema,
205
+ type: "boolean"
206
+ },
207
+ registry
208
+ };
209
+ case "null": return {
210
+ jsonSchema: {
211
+ ...schema,
212
+ type: "null"
213
+ },
214
+ registry
215
+ };
216
+ case "enums": return {
217
+ jsonSchema: {
218
+ ...schema,
219
+ type: "string",
220
+ enum: Object.values(model.variants)
221
+ },
222
+ registry
223
+ };
224
+ case "datetime": return {
225
+ jsonSchema: {
226
+ ...schema,
227
+ type: "string",
228
+ format: "date-time"
229
+ },
230
+ registry
231
+ };
232
+ case "date": return {
233
+ jsonSchema: {
234
+ ...schema,
235
+ type: "string",
236
+ format: "date"
237
+ },
238
+ registry
239
+ };
240
+ case "duration": return {
241
+ jsonSchema: {
242
+ ...schema,
243
+ type: "string",
244
+ format: "duration"
245
+ },
246
+ registry
247
+ };
248
+ case "literal": return {
249
+ jsonSchema: {
250
+ ...schema,
251
+ const: model.value
252
+ },
253
+ registry
254
+ };
255
+ case "string": return {
256
+ jsonSchema: {
257
+ ...schema,
258
+ type: "string"
259
+ },
260
+ registry
261
+ };
262
+ case "array": {
263
+ const { jsonSchema, registry: newRegistry } = generateJsonSchema({
264
+ model: model.base,
265
+ registry,
266
+ toJsonSchema
157
267
  });
158
- case "openapi": return schema.id != null ? { $ref: "#/components/schemas/" + schema.id } : generateJsonSchema({
159
- model: schema,
160
- target,
161
- reference,
162
- depth: depth + 1
268
+ return {
269
+ jsonSchema: {
270
+ ...schema,
271
+ type: "array",
272
+ items: jsonSchema
273
+ },
274
+ registry: newRegistry
275
+ };
276
+ }
277
+ case "map": {
278
+ const { jsonSchema, registry: newRegistry } = generateJsonSchema({
279
+ model: model.base,
280
+ registry,
281
+ toJsonSchema
163
282
  });
283
+ return {
284
+ jsonSchema: {
285
+ ...schema,
286
+ type: "object",
287
+ additionalProperties: jsonSchema
288
+ },
289
+ registry: newRegistry
290
+ };
164
291
  }
165
- };
166
- switch (model.kind) {
167
- case "int32": return convert({
168
- type: "integer",
169
- format: "int32"
170
- });
171
- case "float32": return convert({
172
- type: "number",
173
- format: "float"
174
- });
175
- case "float64": return convert({
176
- type: "number",
177
- format: "float"
178
- });
179
- case "int64": return convert({
180
- type: "string",
181
- format: "int64"
182
- });
183
- case "boolean": return convert({ type: "boolean" });
184
- case "string": return convert({ type: "string" });
185
- case "literal": return convert({ const: model.value });
186
- case "optional": {
187
- const baseSchema = getReferenceSchema(model.base);
188
- if ("$ref" in baseSchema) return convert({ oneOf: [baseSchema, { type: "null" }] });
189
- const { type = [], ...others } = baseSchema;
190
- return convert({
191
- type: Array.isArray(type) ? [...new Set([...type, "null"])] : [type, "null"],
192
- ...others
292
+ case "set": {
293
+ const { jsonSchema, registry: newRegistry } = generateJsonSchema({
294
+ model: model.base,
295
+ registry,
296
+ toJsonSchema
193
297
  });
298
+ return {
299
+ jsonSchema: {
300
+ ...schema,
301
+ type: "array",
302
+ items: jsonSchema,
303
+ uniqueItems: true
304
+ },
305
+ registry: newRegistry
306
+ };
194
307
  }
195
- case "array": return convert({
196
- type: "array",
197
- items: getReferenceSchema(model.base)
198
- });
199
- case "set": return {
200
- $schema,
201
- type: "array",
202
- items: getReferenceSchema(model.base),
203
- uniqueItems: true
204
- };
205
- case "map": return convert({
206
- type: "object",
207
- additionalProperties: getReferenceSchema(model.base)
208
- });
209
- case "record": return convert({
210
- type: "object",
211
- properties: Object.fromEntries(Object.entries(model.properties).map(([k, v]) => [k, getReferenceSchema(v)])),
212
- required: Object.entries(model.properties).filter(([_, v]) => v.kind !== "optional").map(([k]) => k),
213
- additionalProperties: false
214
- });
215
- case "union": return convert({ oneOf: Object.entries(model.variants).map(([k, v]) => ({
216
- type: "object",
217
- title: v.title ?? k,
218
- properties: { [k]: getReferenceSchema(v) },
219
- required: [k],
220
- additionalProperties: false
221
- })) });
222
- case "tagged-union": return convert({ oneOf: Object.values(model.variants).map((o) => getReferenceSchema(o)) });
223
- case "datetime": return convert({
224
- type: "string",
225
- format: "date-time"
226
- });
227
- case "date": return convert({
228
- type: "string",
229
- format: "date"
230
- });
231
- case "duration": return convert({
232
- type: "string",
233
- format: "duration"
234
- });
235
- case "error": return convert({
236
- type: "object",
237
- properties: {
238
- code: { const: model.code },
239
- context: model.context == null ? void 0 : {
308
+ case "record": {
309
+ const result = Object.entries(model.properties).reduce((acc, [key, propModel]) => {
310
+ const ref = acc.registry.getRef(propModel);
311
+ if (ref) return {
312
+ registry: acc.registry,
313
+ properties: {
314
+ ...acc.properties,
315
+ [key]: { $ref: ref }
316
+ }
317
+ };
318
+ const generated = generateJsonSchema({
319
+ model: propModel,
320
+ registry: acc.registry,
321
+ toJsonSchema
322
+ });
323
+ return {
324
+ registry: generated.registry,
325
+ properties: {
326
+ ...acc.properties,
327
+ [key]: generated.jsonSchema
328
+ }
329
+ };
330
+ }, {
331
+ registry,
332
+ properties: {}
333
+ });
334
+ return {
335
+ jsonSchema: {
336
+ ...schema,
240
337
  type: "object",
241
- properties: Object.fromEntries(Object.entries(model.context).map(([k, v]) => [k, getReferenceSchema(v)])),
242
- additionalProperties: false
243
- }
244
- },
245
- additionalProperties: false
246
- });
247
- case "enums": return convert({
248
- type: "string",
249
- enum: Object.values(model.variants)
250
- });
338
+ required: model.required,
339
+ additionalProperties: false,
340
+ properties: result.properties
341
+ },
342
+ registry: result.registry
343
+ };
344
+ }
345
+ case "union": {
346
+ const result = Object.entries(model.variants).reduce((acc, [key, variantModel]) => {
347
+ const ref = acc.registry.getRef(variantModel);
348
+ if (ref) return {
349
+ registry: acc.registry,
350
+ oneOf: [...acc.oneOf, {
351
+ type: "object",
352
+ required: [key],
353
+ properties: { [key]: { $ref: ref } }
354
+ }]
355
+ };
356
+ const generated = generateJsonSchema({
357
+ model: variantModel,
358
+ registry: acc.registry,
359
+ toJsonSchema
360
+ });
361
+ return {
362
+ registry: generated.registry,
363
+ oneOf: [...acc.oneOf, {
364
+ type: "object",
365
+ required: [key],
366
+ properties: { [key]: generated.jsonSchema }
367
+ }]
368
+ };
369
+ }, {
370
+ registry,
371
+ oneOf: []
372
+ });
373
+ return {
374
+ jsonSchema: {
375
+ ...schema,
376
+ oneOf: result.oneOf
377
+ },
378
+ registry: result.registry
379
+ };
380
+ }
381
+ case "taggedUnion": {
382
+ const result = Object.entries(model.variants).reduce((acc, [key, variantModel]) => {
383
+ const ref = acc.registry.getRef(variantModel);
384
+ if (ref) return {
385
+ registry: acc.registry,
386
+ oneOf: [...acc.oneOf, {
387
+ type: "object",
388
+ required: [model.variantKey, model.payloadKey],
389
+ properties: {
390
+ [model.variantKey]: { const: key },
391
+ [model.payloadKey]: { $ref: ref }
392
+ }
393
+ }]
394
+ };
395
+ const generated = generateJsonSchema({
396
+ model: variantModel,
397
+ registry: acc.registry,
398
+ toJsonSchema
399
+ });
400
+ return {
401
+ registry: generated.registry,
402
+ oneOf: [...acc.oneOf, {
403
+ type: "object",
404
+ required: [model.variantKey, model.payloadKey],
405
+ properties: {
406
+ [model.variantKey]: { const: key },
407
+ [model.payloadKey]: generated.jsonSchema
408
+ }
409
+ }]
410
+ };
411
+ }, {
412
+ registry,
413
+ oneOf: []
414
+ });
415
+ return {
416
+ jsonSchema: {
417
+ ...schema,
418
+ oneOf: result.oneOf
419
+ },
420
+ registry: result.registry
421
+ };
422
+ }
251
423
  }
252
424
  }
253
- function getSchemaRef(target) {
254
- switch (target) {
255
- case "draft-2020-12": return "https://json-schema.org/draft/2020-12/schema";
256
- }
425
+ function createJsonSchemaRegistry(models) {
426
+ const map = new Map(models);
427
+ const registry = {
428
+ getRef(model) {
429
+ const entry = map.get(model);
430
+ return entry ? "#/$defs/" + entry.id : void 0;
431
+ },
432
+ add(id, model) {
433
+ const { jsonSchema } = generateJsonSchema({
434
+ model,
435
+ registry
436
+ });
437
+ return createJsonSchemaRegistry(map.set(model, {
438
+ id,
439
+ schema: jsonSchema
440
+ }));
441
+ }
442
+ };
443
+ return registry;
444
+ }
445
+ function createOpenapiSchemaRegistry(models) {
446
+ const map = new Map(models);
447
+ const registry = {
448
+ getRef(model) {
449
+ const entry = map.get(model);
450
+ return entry ? "#/components/schemas/" + entry.id : void 0;
451
+ },
452
+ add(id, model) {
453
+ const { jsonSchema } = generateJsonSchema({
454
+ model,
455
+ registry
456
+ });
457
+ return createOpenapiSchemaRegistry(map.set(model, {
458
+ id,
459
+ schema: jsonSchema
460
+ }));
461
+ }
462
+ };
463
+ return registry;
257
464
  }
258
465
  //#endregion
259
- //#region src/type-system/http.ts
466
+ //#region src/generate-openapi.ts
467
+ function joinPath$1(basePath, routePath) {
468
+ if (!basePath) return routePath;
469
+ return `${basePath.endsWith("/") ? basePath.slice(0, -1) : basePath}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
470
+ }
260
471
  function generateOpenapi(options) {
261
- const { info, routes, securitySchemes, tags, servers } = options;
262
- const models = collectModelFromRoutes(routes);
263
- return {
264
- openapi: "3.2.0",
265
- info,
266
- paths: generatePathsObject(routes),
267
- components: {
268
- schemas: Object.fromEntries(generateComponentSchemasFromModels(models)),
269
- securitySchemes
472
+ const { info, servers, routers, security } = options;
473
+ const flatRoutes = routers.flatMap((rm) => Object.entries(rm.routes).map(([_key, route]) => ({
474
+ route,
475
+ group: rm.name,
476
+ fullPath: joinPath$1(rm.basePath ?? "", route.path)
477
+ })));
478
+ const namedModels = collectNamedModels$1(flatRoutes.map((fr) => fr.route));
479
+ const registry = [...namedModels.entries()].reduce((reg, [id, model]) => reg.add(id, model), createOpenapiSchemaRegistry());
480
+ const schemas = [...namedModels.entries()].reduce((acc, [id, model]) => ({
481
+ ...acc,
482
+ [id]: generateJsonSchema({
483
+ model,
484
+ registry
485
+ }).jsonSchema
486
+ }), {});
487
+ const hasSchemas = Object.keys(schemas).length > 0;
488
+ const paths = generatePaths(flatRoutes, registry);
489
+ let components = hasSchemas ? { schemas } : void 0;
490
+ if (security?.policy) {
491
+ const schemeResult = buildSecuritySchemes(security.policy, security.deployments);
492
+ if (schemeResult.schemes && Object.keys(schemeResult.schemes).length > 0) components = {
493
+ ...components,
494
+ securitySchemes: schemeResult.schemes
495
+ };
496
+ const securedPaths = applySecurityToPaths(paths, security.policy);
497
+ if (securedPaths) return {
498
+ openapi: {
499
+ openapi: "3.2.0",
500
+ info,
501
+ servers,
502
+ paths: securedPaths,
503
+ components
504
+ },
505
+ registry
506
+ };
507
+ }
508
+ return {
509
+ openapi: {
510
+ openapi: "3.2.0",
511
+ info,
512
+ servers,
513
+ paths,
514
+ components
270
515
  },
271
- tags,
272
- servers
516
+ registry
273
517
  };
274
518
  }
275
- function collectModelFromRoutes(routes) {
276
- const models = /* @__PURE__ */ new Map();
277
- for (const route of routes) for (const [id, model] of collectModelsFromRoute(route)) models.set(id, model);
278
- return models;
279
- }
280
- function collectModelDeep(model) {
281
- const ret = /* @__PURE__ */ new Map();
282
- if (model.id != null) ret.set(model.id, model);
283
- switch (model.kind) {
284
- case "optional":
285
- case "array":
286
- case "set":
287
- case "map":
288
- for (const [id, nestModel] of collectModelDeep(model.base)) ret.set(id, nestModel);
289
- return ret;
290
- case "record":
291
- for (const property of Object.values(model.properties)) for (const [id, nestedModel] of collectModelDeep(property)) ret.set(id, nestedModel);
292
- return ret;
293
- case "union":
294
- for (const variant of Object.values(model.variants)) for (const [id, nestedModel] of collectModelDeep(variant)) ret.set(id, nestedModel);
295
- return ret;
296
- case "error":
297
- if (model.context) for (const detail of Object.values(model.context)) for (const [id, nestedModel] of collectModelDeep(detail)) ret.set(id, nestedModel);
298
- return ret;
299
- default: return ret;
300
- }
519
+ function collectNamedModels$1(routes) {
520
+ return routes.reduce((models, route) => {
521
+ return [...collectModelsFromRoute(route).entries()].reduce((acc, [id, model]) => {
522
+ if (!acc.has(id)) acc.set(id, model);
523
+ return acc;
524
+ }, models);
525
+ }, /* @__PURE__ */ new Map());
301
526
  }
302
527
  function collectModelsFromRoute(route) {
303
- const models = /* @__PURE__ */ new Map();
304
- for (const model of [
305
- route.variables,
306
- route.queries,
307
- route.headers,
308
- route.cookies
309
- ].filter((o) => !!o).flatMap(Object.values)) for (const [id, subModel] of collectModelDeep(model)) models.set(id, subModel);
310
- if (route.content) for (const [id, subModel] of collectModelsFromContent(route.content)) models.set(id, subModel);
311
- if (route.responses) for (const response of Object.values(route.responses)) for (const [id, subModel] of collectModelsFromResponse(response)) models.set(id, subModel);
312
- return models;
313
- }
314
- function collectModelsFromContent(content) {
315
- const models = /* @__PURE__ */ new Map();
316
- if (content.kind !== "binary-stream-content") {
317
- if (content.model) for (const [id, subModel] of collectModelDeep(content.model)) models.set(id, subModel);
318
- }
319
- return models;
528
+ return [route.body, ...Object.values(route.responses).flatMap(collectModelsFromResponse)].filter((m) => m != null).reduce((acc, model) => {
529
+ return collectModelDeep(model).reduce((inner, [id, m]) => {
530
+ if (!inner.has(id)) inner.set(id, m);
531
+ return inner;
532
+ }, acc);
533
+ }, /* @__PURE__ */ new Map());
320
534
  }
321
535
  function collectModelsFromResponse(response) {
322
- const models = /* @__PURE__ */ new Map();
323
- for (const model of [response.cookies, response.headers].filter((o) => !!o).flatMap(Object.values)) for (const [id, subModel] of collectModelDeep(model)) models.set(id, subModel);
324
- if (response.content) for (const [id, subModel] of collectModelsFromContent(response.content)) models.set(id, subModel);
325
- return models;
326
- }
327
- function generateComponentSchemasFromModels(models) {
328
- const ret = /* @__PURE__ */ new Map();
329
- for (const [id, model] of models) {
330
- const schema = generateJsonSchema({
331
- model,
332
- reference: "openapi"
333
- });
334
- ret.set(id, schema);
536
+ switch (response.kind) {
537
+ case "json-response":
538
+ case "stream-response":
539
+ case "sse-response": return response.body ? [response.body] : [];
540
+ case "binary": return [];
335
541
  }
336
- return ret;
337
542
  }
338
- function generatePathsObject(routes) {
339
- const pathItems = /* @__PURE__ */ new Map();
340
- for (const route of routes) {
341
- const item = pathItems.get(route.path) ?? {};
342
- pathItems.set(route.path, {
343
- ...item,
344
- ...generatePathItemObject(route)
345
- });
346
- }
347
- return Object.fromEntries(pathItems);
348
- }
349
- function generatePathItemObject(route) {
350
- return { [route.method]: generateOperationObject(route) };
351
- }
352
- function getSchemaReference(model) {
353
- return model.id != null ? { $ref: "#/components/schemas/" + model.id } : generateJsonSchema({
354
- model,
355
- reference: "openapi"
356
- });
543
+ function collectModelDeep(model) {
544
+ const self = "id" in model && model.id != null ? [[model.id, model]] : [];
545
+ const nested = collectNestedModels(model);
546
+ return [...self, ...nested];
357
547
  }
358
- function generateOperationObject(route) {
359
- const { id, summary, description, variables, queries, headers, cookies, content, responses } = route;
360
- const parameterObjects = [];
361
- for (const [name, model] of Object.entries(variables)) parameterObjects.push({
362
- in: "path",
363
- name,
364
- required: true,
365
- schema: getSchemaReference(model)
366
- });
367
- if (queries) for (const [name, model] of Object.entries(queries)) parameterObjects.push({
368
- in: "query",
369
- name,
370
- required: model.kind !== "optional",
371
- schema: getSchemaReference(model)
372
- });
373
- if (headers) for (const [name, model] of Object.entries(headers)) parameterObjects.push({
374
- in: "header",
375
- name,
376
- required: model.kind !== "optional",
377
- schema: getSchemaReference(model)
378
- });
379
- if (cookies) for (const [name, model] of Object.entries(cookies)) parameterObjects.push({
380
- in: "cookie",
381
- name,
382
- required: model.kind !== "optional",
383
- schema: getSchemaReference(model)
384
- });
385
- const responseObjects = /* @__PURE__ */ new Map();
386
- if (responses) for (const [name, response] of Object.entries(responses)) responseObjects.set(response.status.toString(), generateResponseObjectFromResponse(name, response));
387
- return {
388
- operationId: id,
389
- summary: summary ?? route.id,
390
- description,
391
- parameters: parameterObjects,
392
- responses: Object.fromEntries(responseObjects),
393
- requestBody: content == null ? void 0 : generateRequestBodyObject(content)
394
- };
395
- }
396
- function generateRequestBodyObject(content) {
397
- switch (content.kind) {
398
- case "plain-text-content":
399
- case "json-content":
400
- case "form-content":
401
- case "server-sent-event-content": return { content: { [content.type]: generateMeidaTypeObject(content) } };
402
- default: return {
403
- description: content.description,
404
- content: { [content.type]: generateMeidaTypeObject(content) }
405
- };
548
+ function collectNestedModels(model) {
549
+ switch (model.kind) {
550
+ case "array":
551
+ case "set":
552
+ case "map": return collectModelDeep(model.base);
553
+ case "record": return Object.values(model.properties).flatMap(collectModelDeep);
554
+ case "union": return Object.values(model.variants).flatMap(collectModelDeep);
555
+ case "taggedUnion": return Object.values(model.variants).flatMap(collectModelDeep);
556
+ default: return [];
406
557
  }
407
558
  }
408
- function generateMeidaTypeObject(content) {
409
- switch (content.kind) {
410
- case "plain-text-content": return { schema: content.model ? getSchemaReference(content.model) : { type: "string" } };
411
- case "json-content": return { schema: getSchemaReference(content.model) };
412
- case "json-stream-content": return {
413
- schema: {
414
- type: "string",
415
- format: "binary"
416
- },
417
- itemSchema: getSchemaReference(content.model)
418
- };
419
- case "binary-stream-content": return {
420
- schema: {
421
- type: "string",
422
- format: "binary"
423
- },
424
- itemSchema: {
425
- type: "string",
426
- format: "binary"
559
+ function generatePaths(flatRoutes, registry) {
560
+ return flatRoutes.reduce((paths, { route, group, fullPath }) => {
561
+ const existing = paths[fullPath] ?? {};
562
+ const method = route.method.toLowerCase();
563
+ return {
564
+ ...paths,
565
+ [fullPath]: {
566
+ ...existing,
567
+ [method]: generateOperation(route, group, registry)
427
568
  }
428
569
  };
429
- case "server-sent-event-content": return {
430
- schema: {
431
- type: "string",
432
- format: "binary"
433
- },
434
- itemSchema: content.model ? getSchemaReference(content.model) : { type: "string" }
435
- };
436
- case "form-content": return { schema: getSchemaReference(content.model) };
437
- }
570
+ }, {});
438
571
  }
439
- function generateResponseObjectFromResponse(name, response) {
440
- const headerObjects = /* @__PURE__ */ new Map();
441
- if (response.headers) for (const [name, model] of Object.entries(response.headers)) headerObjects.set(name, {
442
- schema: getSchemaReference(model),
443
- required: model.kind !== "optional"
444
- });
572
+ function generateOperation(route, group, registry) {
573
+ const tags = route.tags?.includes(group) ? route.tags : [...route.tags ?? [], group];
445
574
  return {
446
- description: response.description ?? name,
447
- headers: headerObjects.size > 0 ? Object.fromEntries(headerObjects) : void 0,
448
- content: response.content == null ? void 0 : { [response.content.type]: generateMeidaTypeObject(response.content) }
575
+ summary: route.summary,
576
+ description: route.description,
577
+ tags,
578
+ parameters: generateParameters(route, registry),
579
+ requestBody: generateRequestBody(route, registry),
580
+ responses: generateResponses(route.responses, registry)
449
581
  };
450
582
  }
451
- function route(options) {
583
+ function generateParameters(route, registry) {
584
+ const pathParams = Object.entries(route.variables ?? {}).map(([name, model]) => generateParameter(name, model, "path", true, registry));
585
+ const queries = route.queries;
586
+ const headers = route.headers;
587
+ const queryParams = queries ? Object.entries(queries.properties).map(([name, model]) => generateParameter(name, model, "query", queries.required.includes(name), registry)) : [];
588
+ const headerParams = headers ? Object.entries(headers.properties).map(([name, model]) => generateParameter(name, model, "header", headers.required.includes(name), registry)) : [];
589
+ const all = [
590
+ ...pathParams,
591
+ ...queryParams,
592
+ ...headerParams
593
+ ];
594
+ return all.length > 0 ? all : void 0;
595
+ }
596
+ function generateParameter(name, model, location, required, registry) {
452
597
  return {
453
- kind: "http-route",
454
- ...options
598
+ name,
599
+ in: location,
600
+ required,
601
+ schema: getSchema(model, registry)
455
602
  };
456
603
  }
457
- function plainText(options) {
458
- return {
459
- kind: "plain-text-content",
460
- ...options
461
- };
604
+ function generateRequestBody(route, registry) {
605
+ if (route.body == null || route.body.kind === "null") return void 0;
606
+ return { content: { [route.contentType ?? "application/json"]: generateMediaType(route.body, registry) } };
462
607
  }
463
- function json(options) {
464
- return {
465
- kind: "json-content",
466
- ...options
467
- };
608
+ function generateMediaType(model, registry) {
609
+ return { schema: getSchema(model, registry) };
468
610
  }
469
- function jsonStream(options) {
470
- return {
471
- kind: "json-stream-content",
472
- ...options
473
- };
611
+ function generateResponses(responses, registry) {
612
+ return Object.entries(responses).reduce((acc, [status, response]) => ({
613
+ ...acc,
614
+ [status]: generateResponseObject(response, registry)
615
+ }), {});
474
616
  }
475
- function binary(options) {
617
+ function generateResponseObject(response, registry) {
476
618
  return {
477
- kind: "binary-stream-content",
478
- ...options
619
+ description: response.summary ?? "",
620
+ headers: response.kind !== "binary" && response.headers ? generateResponseHeaders(response.headers, registry) : void 0,
621
+ content: generateResponseContent(response, registry)
479
622
  };
480
623
  }
481
- function form(options) {
624
+ function generateResponseHeaders(headers, registry) {
625
+ return Object.entries(headers.properties).reduce((acc, [name, model]) => ({
626
+ ...acc,
627
+ [name]: { schema: getSchema(model, registry) }
628
+ }), {});
629
+ }
630
+ function generateResponseContent(response, registry) {
631
+ switch (response.kind) {
632
+ case "json-response": {
633
+ if (response.body == null) return void 0;
634
+ const contentType = response.contentType ?? "application/json";
635
+ const mediaType = { schema: getSchema(response.body, registry) };
636
+ return { [contentType]: mediaType };
637
+ }
638
+ case "stream-response": {
639
+ if (response.body == null) return void 0;
640
+ const contentType = response.contentType ?? "application/x-ndjson";
641
+ const mediaType = {
642
+ schema: {
643
+ type: "string",
644
+ format: "binary"
645
+ },
646
+ itemSchema: getSchema(response.body, registry)
647
+ };
648
+ return { [contentType]: mediaType };
649
+ }
650
+ case "sse-response": {
651
+ const contentType = response.contentType ?? "text/event-stream";
652
+ const mediaType = {
653
+ schema: {
654
+ type: "string",
655
+ format: "binary"
656
+ },
657
+ itemSchema: response.body ? getSchema(response.body, registry) : { type: "string" }
658
+ };
659
+ return { [contentType]: mediaType };
660
+ }
661
+ case "binary": return { [response.contentType ?? "application/octet-stream"]: { schema: {
662
+ type: "string",
663
+ format: "binary"
664
+ } } };
665
+ }
666
+ }
667
+ function getSchema(model, registry) {
668
+ const ref = registry.getRef(model);
669
+ return ref ? { $ref: ref } : generateJsonSchema({
670
+ model,
671
+ registry
672
+ }).jsonSchema;
673
+ }
674
+ function buildSecuritySchemes(policy, deployments) {
675
+ const components = collectSecurityComponents(policy);
676
+ if (components.length === 0) return { schemes: void 0 };
677
+ const schemes = {};
678
+ for (const component of components) switch (component.kind) {
679
+ case "apikey":
680
+ schemes[component.id] = toApiKeyScheme(component);
681
+ break;
682
+ case "openIdConnect": {
683
+ const deployment = findDeployment(component, deployments);
684
+ if (!deployment || deployment.kind !== "openIdConnectDeployment") {
685
+ console.warn(`[generateOpenapi] Security scheme "${component.id}" (openIdConnect) has no matching deployment, skipping`);
686
+ break;
687
+ }
688
+ schemes[component.id] = toOpenIdConnectScheme(component, deployment);
689
+ break;
690
+ }
691
+ }
692
+ return { schemes: Object.keys(schemes).length > 0 ? schemes : void 0 };
693
+ }
694
+ function findDeployment(component, deployments) {
695
+ if (!deployments) return void 0;
696
+ for (const dep of Object.values(deployments)) if (dep.component.id === component.id) return dep;
697
+ }
698
+ function toApiKeyScheme(component) {
482
699
  return {
483
- kind: "form-content",
484
- ...options
700
+ type: "apiKey",
701
+ name: component.name,
702
+ in: "header",
703
+ description: component.description
485
704
  };
486
705
  }
487
- function sse(options) {
488
- const { kind = "server-sent-event-content", type = "text/event-stream", model } = options ?? {};
706
+ function toOpenIdConnectScheme(component, deployment) {
489
707
  return {
490
- kind,
491
- type,
492
- model
708
+ type: "openIdConnect",
709
+ openIdConnectUrl: deployment.issuer.endsWith("/") ? `${deployment.issuer}.well-known/openid-configuration` : `${deployment.issuer}/.well-known/openid-configuration`,
710
+ description: component.description
493
711
  };
494
712
  }
495
- //#endregion
496
- //#region src/generator/java.ts
497
- function generateJavaClass(options) {
498
- const { model, package: packageName } = options;
499
- switch (model.kind) {
500
- case "record": return `
501
- package ${packageName};
502
-
503
- import org.jspecify.annotations.NullMarked;
504
-
505
- @NullMarked
506
- public record ${model.id}(
507
- ${Object.entries(model.properties).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
508
- ){
509
- ${Object.entries(model.properties).filter(([__dirname, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
510
- }
511
- `;
512
- case "union": return `
513
- package ${packageName};
514
-
515
- import org.jspecify.annotations.NullMarked;
516
-
517
- @NullMarked
518
- public sealed interface ${model.id}{
519
- ${Object.entries(model.variants).map(([k, v]) => generateUnionVariantCode(packageName, model.id, k, v)).join("\n")}
520
- }
521
- `;
522
- case "error": return `
523
- package ${packageName};
524
-
525
- import org.jspecify.annotations.NullMarked;
526
-
527
- @NullMarked
528
- public record ${model.id}(Context context){
529
- ${generateStaticPropertyCode(packageName, "code", literal({ value: model.code }))}
530
-
531
- public ${model.id}(
532
- ${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
533
- ){
534
- this(new Context(${getPropertyNames(model.context)}));
535
- }
536
-
537
- public record Context(
538
- ${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
539
- ){
540
- ${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
541
- }
542
- }
543
- `;
544
- case "enums": return `
545
-
546
- package ${packageName};
547
-
548
- import org.jspecify.annotations.NullMarked;
549
-
550
- public enum ${model.id} {
551
-
552
- ${Object.entries(model.variants).map(([k, v]) => `${k}("${v}")`)};
553
-
554
- private final String value;
555
-
556
- public ${model.id}(String value){
557
- this.value = value;
558
- }
559
-
560
- @com.fasterxml.jackson.annotation.JsonValue
561
- public String getValue() {
562
- return value;
563
- }
564
- }
565
- `;
566
- }
567
- }
568
- function getPropertyNames(properties) {
569
- if (properties == null) return "";
570
- return Object.keys(properties).join(",");
571
- }
572
- function generateUnionVariantCode(packageName, unionId, variantName, variant) {
573
- switch (variant.kind) {
574
- case "literal": return `
575
- record ${variantName}() implements ${unionId} {
576
- @com.fasterxml.jackson.annotation.JsonValue
577
- ${generateStaticPropertyCode(packageName, "value", variant)}
578
- }
579
- `;
580
- default: return `
581
- record ${variantName}(
582
- @com.fasterxml.jackson.annotation.JsonCreator
583
- @com.fasterxml.jackson.annotation.JsonValue
584
- ${generateModelSignature$1(packageName, variant)} value
585
- ) implements ${unionId} {}
586
- `;
587
- }
588
- }
589
- function generatePropertyCode(packageName, name, property) {
590
- return `${generateModelSignature$1(packageName, property)} ${name}`;
591
- }
592
- function generateStaticPropertyCode(packageName, name, property) {
593
- return `public ${generateModelSignature$1(packageName, property)} ${name} = ${generateStaticValue$1(property.value)}`;
594
- }
595
- function generateStaticValue$1(value) {
596
- switch (typeof value) {
597
- case "string": return `"${value}"`;
598
- case "boolean": return String(value);
599
- case "number": return Number.isInteger(value) ? String(value) : value + "d";
600
- default: throw Error("Unsupported literal value");
601
- }
602
- }
603
- function generateModelSignature$1(packageName, model) {
604
- switch (model.kind) {
605
- case "string": return "String";
606
- case "int32": return "int";
607
- case "int64": return "long";
608
- case "float32": return "float";
609
- case "float64": return "double";
610
- case "boolean": return "boolean";
611
- case "date": return "java.time.LocalDate";
612
- case "datetime": return "java.time.LocalDateTime";
613
- case "duration": return "java.time.Duration";
614
- case "array": return `Array<${generateModelSignature$1(packageName, model.base)}>`;
615
- case "set": return `Set<${generateModelSignature$1(packageName, model.base)}>`;
616
- case "map": return `Map<String, ${generateModelSignature$1(packageName, model.base)}>`;
617
- case "optional": switch (model.base.kind) {
618
- case "int32": return "@Nullable Integer";
619
- case "int64": return "@Nullable Long";
620
- case "float32": return "@Nullable Float";
621
- case "float64": return "@Nullable Double";
622
- case "boolean": return "@Nullable Boolean";
623
- case "optional": return generateModelSignature$1(packageName, model.base);
624
- default: return `@Nullable ${generateModelSignature$1(packageName, model.base)}`;
625
- }
626
- case "literal": switch (typeof model.value) {
627
- case "string": return "String";
628
- case "boolean": return "boolean";
629
- case "number": return Number.isInteger(model.value) ? "int" : "double";
630
- default: throw Error("Unsupported literal value");
713
+ function collectSecurityComponents(policy) {
714
+ const seen = /* @__PURE__ */ new Set();
715
+ const result = [];
716
+ for (const pathItem of Object.values(policy.paths)) {
717
+ if (!pathItem.pipeline) continue;
718
+ for (const apply of pathItem.pipeline) if (!seen.has(apply.component.id)) {
719
+ seen.add(apply.component.id);
720
+ result.push(apply.component);
631
721
  }
632
- default: return packageName + "." + model.id;
633
722
  }
723
+ return result;
724
+ }
725
+ function applySecurityToPaths(paths, policy) {
726
+ if (!paths) return void 0;
727
+ const operationMethods = [
728
+ "get",
729
+ "put",
730
+ "post",
731
+ "delete",
732
+ "options",
733
+ "head",
734
+ "patch",
735
+ "trace"
736
+ ];
737
+ return Object.entries(paths).reduce((acc, [pathKey, pathItem]) => {
738
+ const securedItem = operationMethods.reduce((methodAcc, method) => {
739
+ const op = pathItem[method];
740
+ if (!op) return methodAcc;
741
+ const requirements = resolveSecurityRequirements(policy, pathKey, method);
742
+ if (requirements.length === 0) return methodAcc;
743
+ return {
744
+ ...methodAcc,
745
+ [method]: {
746
+ ...op,
747
+ security: requirements
748
+ }
749
+ };
750
+ }, pathItem);
751
+ return {
752
+ ...acc,
753
+ [pathKey]: securedItem
754
+ };
755
+ }, {});
756
+ }
757
+ function resolveSecurityRequirements(policy, pathKey, method) {
758
+ const requirements = [];
759
+ for (const [pattern, pathItem] of Object.entries(policy.paths)) {
760
+ if (!matchesPath(pattern, pathKey)) continue;
761
+ const methods = pathItem.methods;
762
+ if (methods && methods.length > 0 && !methods.includes(method.toUpperCase())) continue;
763
+ if (!pathItem.pipeline) continue;
764
+ for (const apply of pathItem.pipeline) requirements.push({ [apply.component.id]: apply.scopes });
765
+ }
766
+ return requirements;
634
767
  }
635
- async function formatJava(code) {
768
+ function matchesPath(pattern, path) {
636
769
  try {
637
- return await format(code, {
638
- parser: "java",
639
- plugins: [PrettierJavaPlugin]
640
- });
641
- } catch (error) {
642
- console.error(error);
643
- return code;
644
- }
645
- }
646
- async function generateJavaCodes(options) {
647
- const { package: packageName, srcDir, routes = [], models = [] } = options;
648
- const codes = /* @__PURE__ */ new Map();
649
- const namedModels = collectModelFromRoutes(routes);
650
- for (const model of models) for (const [id, subModel] of collectModelDeep(model)) namedModels.set(id, subModel);
651
- for (const [id, model] of namedModels) {
652
- if (model.kind !== "union" && model.kind !== "record" && model.kind !== "enums" && model.kind !== "error") continue;
653
- const path = resolve(srcDir, ...(packageName + "." + id).split("."), ".java");
654
- const code = await formatJava(generateJavaClass({
655
- package: packageName,
656
- model
657
- }));
658
- codes.set(path, code);
659
- }
660
- return codes.entries().map(([path, code]) => ({
661
- path,
662
- code
663
- })).toArray();
770
+ return new RegExp(pattern).test(path);
771
+ } catch {
772
+ return false;
773
+ }
664
774
  }
665
775
  //#endregion
666
- //#region src/generator/rust.ts
667
- function generateRustCode(options) {
668
- const { model } = options;
669
- switch (model.kind) {
670
- case "record": return generateRecordCode(model);
671
- case "union": return generateUnionCode(model);
672
- case "error": return generateErrorCode(model);
673
- case "enums": return generateEnumCode(model);
674
- }
675
- }
676
- function generateRecordCode(model) {
677
- const structName = toPascalCase(model.id || "Unknown");
678
- const structFields = Object.entries(model.properties).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generateStructField(k, v));
679
- const constFields = Object.entries(model.properties).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateConstField(k, v));
680
- const hasConstants = constFields.length > 0;
681
- const hasFields = structFields.length > 0;
682
- let code = `use serde::Serialize;\n\n`;
683
- if (hasConstants && hasFields) {
684
- code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
685
- structFields.forEach((f) => {
686
- code += ` ${f};\n`;
776
+ //#region src/codegen/collect.ts
777
+ function collectNamedModels(models, options) {
778
+ const identifier = options?.identifier ?? pascalCase;
779
+ const namespace = options?.namespace;
780
+ return models.reduce((acc, model) => {
781
+ if (model.kind === "record") return [...acc, toRecordDescriptor(model, identifier, namespace)];
782
+ if (model.kind === "enums") return [...acc, toEnumsDescriptor(model, identifier, namespace)];
783
+ if (model.kind === "union") return [...acc, {
784
+ kind: "union",
785
+ originalId: model.id,
786
+ identifier: identifier(model.id),
787
+ namespace,
788
+ title: model.title,
789
+ description: model.description,
790
+ deprecated: model.deprecated,
791
+ examples: model.examples,
792
+ variants: model.variants
793
+ }];
794
+ if (model.kind === "taggedUnion") return [...acc, {
795
+ kind: "taggedUnion",
796
+ originalId: model.id,
797
+ identifier: identifier(model.id),
798
+ namespace,
799
+ title: model.title,
800
+ description: model.description,
801
+ deprecated: model.deprecated,
802
+ examples: model.examples,
803
+ variants: model.variants,
804
+ variantKey: model.variantKey,
805
+ payloadKey: model.payloadKey
806
+ }];
807
+ return acc;
808
+ }, []);
809
+ }
810
+ function toRecordDescriptor(model, identifier, namespace) {
811
+ return {
812
+ kind: "record",
813
+ originalId: model.id,
814
+ identifier: identifier(model.id),
815
+ namespace,
816
+ title: model.title,
817
+ description: model.description,
818
+ deprecated: "deprecated" in model ? model.deprecated : void 0,
819
+ examples: model.examples,
820
+ fields: Object.entries(model.properties).map(([name, propModel]) => ({
821
+ name,
822
+ model: propModel,
823
+ required: model.required.includes(name),
824
+ title: propModel.title,
825
+ description: propModel.description,
826
+ deprecated: propModel.deprecated
827
+ }))
828
+ };
829
+ }
830
+ function toEnumsDescriptor(model, identifier, namespace) {
831
+ return {
832
+ kind: "enums",
833
+ originalId: model.id,
834
+ identifier: identifier(model.id),
835
+ namespace,
836
+ title: model.title,
837
+ description: model.description,
838
+ values: model.variants
839
+ };
840
+ }
841
+ function joinPath(basePath, routePath) {
842
+ if (!basePath) return routePath;
843
+ return `${basePath.endsWith("/") ? basePath.slice(0, -1) : basePath}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
844
+ }
845
+ function collectOperations(routers) {
846
+ return routers.flatMap((routerModel) => {
847
+ const basePath = routerModel.basePath ?? "";
848
+ return Object.entries(routerModel.routes).map(([id, route]) => {
849
+ const queries = {};
850
+ if (route.queries) for (const [name, propModel] of Object.entries(route.queries.properties)) queries[name] = {
851
+ model: propModel,
852
+ name,
853
+ required: route.queries.required.includes(name)
854
+ };
855
+ const headers = {};
856
+ if (route.headers) for (const [name, propModel] of Object.entries(route.headers.properties)) headers[name] = {
857
+ model: propModel,
858
+ name,
859
+ required: route.headers.required.includes(name)
860
+ };
861
+ const pathVariables = {};
862
+ if (route.variables) for (const [name, model] of Object.entries(route.variables)) pathVariables[name] = {
863
+ model,
864
+ name
865
+ };
866
+ const responses = {};
867
+ const responseKinds = {};
868
+ for (const [status, resp] of Object.entries(route.responses)) {
869
+ responses[Number(status)] = "body" in resp && resp.body != null ? resp.body : null;
870
+ responseKinds[Number(status)] = resp.kind;
871
+ }
872
+ return {
873
+ id,
874
+ group: routerModel.name,
875
+ method: route.method,
876
+ path: joinPath(basePath, route.path),
877
+ summary: route.summary,
878
+ description: route.description,
879
+ tags: route.tags,
880
+ requestModel: route.body ?? null,
881
+ responses,
882
+ responseKinds,
883
+ pathVariables,
884
+ queries,
885
+ headers
886
+ };
687
887
  });
688
- code += `}\n\n`;
689
- code += `impl ${structName} {\n`;
690
- code += ` pub const CODE: &'static str = "${model.id}";\n`;
691
- constFields.forEach((f) => {
692
- code += ` pub ${f};\n`;
888
+ });
889
+ }
890
+ function collectSchemaMap(ops) {
891
+ const all = collectAll(ops);
892
+ const map = /* @__PURE__ */ new Map();
893
+ for (const m of all) {
894
+ if ("id" in m && map.has(m.id)) continue;
895
+ if (m.kind === "record") map.set(m.id, {
896
+ kind: "record",
897
+ fields: Object.entries(m.properties).map(([name, p]) => ({
898
+ name,
899
+ model: p,
900
+ required: m.required.includes(name)
901
+ }))
693
902
  });
694
- code += `}\n`;
695
- } else if (hasFields) {
696
- code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
697
- structFields.forEach((f) => {
698
- code += ` ${f},\n`;
903
+ else if (m.kind === "enums") map.set(m.id, {
904
+ kind: "enums",
905
+ variants: m.variants
699
906
  });
700
- code += `}\n`;
701
- } else if (hasConstants) {
702
- code += `#[derive(Serialize)]\npub struct ${structName};\n\n`;
703
- code += `impl ${structName} {\n`;
704
- code += ` pub const CODE: &'static str = "${model.id}";\n`;
705
- constFields.forEach((f) => {
706
- code += ` pub ${f};\n`;
907
+ else if (m.kind === "union") map.set(m.id, {
908
+ kind: "union",
909
+ unionVariants: m.variants
707
910
  });
708
- code += `}\n`;
709
- }
710
- return code;
711
- }
712
- function generateUnionCode(model) {
713
- const enumName = toPascalCase(model.id || "Unknown");
714
- let code = `use serde::Serialize;\n\n`;
715
- code += `#[derive(Serialize)]\n#[serde(tag = "type", content = "value")]\npub enum ${enumName} {\n`;
716
- Object.entries(model.variants).forEach(([k, v]) => {
717
- const variantName = toPascalCase(k);
718
- if (v.kind === "literal") code += ` ${variantName},\n`;
719
- else {
720
- const variantType = generateModelSignature(v);
721
- code += ` ${variantName}(${variantType}),\n`;
722
- }
723
- });
724
- code += `}\n`;
725
- return code;
726
- }
727
- function generateErrorCode(model) {
728
- const structName = toPascalCase(model.id || "Unknown");
729
- const contextFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generateStructField(k, v));
730
- const constFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateConstField(k, v));
731
- let code = `use serde::Serialize;\n\n`;
732
- if (contextFields.length > 0 || constFields.length > 0) {
733
- code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
734
- code += ` pub code: &'static str,\n`;
735
- if (contextFields.length > 0) code += ` pub context: ${structName}Context,\n`;
736
- code += `}\n\n`;
737
- if (contextFields.length > 0) {
738
- code += `#[derive(Serialize)]\npub struct ${structName}Context {\n`;
739
- contextFields.forEach((f) => {
740
- code += ` ${f};\n`;
741
- });
742
- code += `}\n\n`;
743
- }
744
- code += `impl ${structName} {\n`;
745
- code += ` pub const CODE: &'static str = "${model.code}";\n`;
746
- constFields.forEach((f) => {
747
- code += ` pub ${f};\n`;
911
+ else if (m.kind === "taggedUnion") map.set(m.id, {
912
+ kind: "taggedUnion",
913
+ unionVariants: m.variants,
914
+ variantKey: m.variantKey,
915
+ payloadKey: m.payloadKey
748
916
  });
749
- code += `}\n`;
750
- } else {
751
- code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
752
- code += ` pub code: &'static str,\n`;
753
- code += `}\n\n`;
754
- code += `impl ${structName} {\n`;
755
- code += ` pub const CODE: &'static str = "${model.code}";\n`;
756
- code += `}\n`;
757
- }
758
- return code;
759
- }
760
- function generateEnumCode(model) {
761
- const enumName = toPascalCase(model.id || "Unknown");
762
- let code = `use serde::Serialize;\n\n`;
763
- code += `#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]\npub enum ${enumName} {\n`;
764
- Object.entries(model.variants).forEach(([k, v]) => {
765
- const variantName = toUpperSnakeCase(k);
766
- code += ` #[serde(rename = "${v}")]\n`;
767
- code += ` ${variantName},\n`;
768
- });
769
- code += `}\n`;
770
- return code;
917
+ }
918
+ return map;
919
+ }
920
+ function collectAll(ops) {
921
+ const seen = /* @__PURE__ */ new Set();
922
+ const out = [];
923
+ const add = (m) => {
924
+ if (m == null || seen.has(m)) return;
925
+ seen.add(m);
926
+ out.push(m);
927
+ if (m.kind === "array" || m.kind === "set" || m.kind === "map") add(m.base);
928
+ if (m.kind === "record") Object.values(m.properties).forEach((v) => add(v));
929
+ if (m.kind === "union" || m.kind === "taggedUnion") Object.values(m.variants).forEach((v) => add(v));
930
+ };
931
+ for (const op of ops) {
932
+ add(op.requestModel);
933
+ for (const v of Object.values(op.responses)) add(v);
934
+ for (const v of Object.values(op.pathVariables)) add(v.model);
935
+ for (const v of Object.values(op.queries)) add(v.model);
936
+ for (const v of Object.values(op.headers)) add(v.model);
937
+ }
938
+ return out;
771
939
  }
772
- function generateStructField(name, property) {
773
- return `pub ${toSnakeCase(name)}: ${generateModelSignature(property)}`;
940
+ function resolveNamedRoot(m) {
941
+ switch (m.kind) {
942
+ case "record":
943
+ case "enums":
944
+ case "union":
945
+ case "taggedUnion": return m;
946
+ case "array":
947
+ case "set":
948
+ case "map": return resolveNamedRoot(m.base);
949
+ default: return null;
950
+ }
774
951
  }
775
- function generateConstField(name, property) {
776
- return `const ${toUpperSnakeCase(name)}: ${generateModelSignature(property)} = ${generateStaticValue(property.value)}`;
952
+ //#endregion
953
+ //#region src/codegen/json-schema.ts
954
+ function mergeJsonSchemas(schemas) {
955
+ const entries = Object.entries(schemas);
956
+ const registry = entries.reduce((reg, [id, model]) => reg.add(id, model), createJsonSchemaRegistry());
957
+ return {
958
+ $schema: "https://json-schema.org/draft/2020-12/schema",
959
+ $defs: entries.reduce((acc, [id, model]) => {
960
+ const { jsonSchema } = generateJsonSchema({
961
+ model,
962
+ registry
963
+ });
964
+ return typeof jsonSchema === "object" && jsonSchema != null ? {
965
+ ...acc,
966
+ [id]: jsonSchema
967
+ } : acc;
968
+ }, {})
969
+ };
777
970
  }
778
- function generateStaticValue(value) {
779
- switch (typeof value) {
780
- case "string": return `"${value}"`;
781
- case "boolean": return String(value);
782
- case "number": return Number.isInteger(value) ? String(value) : `${value}.0`;
783
- default: throw Error("Unsupported literal value");
971
+ //#endregion
972
+ //#region src/codegen/hono-server.ts
973
+ function generateHonoServer(options) {
974
+ const { routers, identifier = pascalCase, namespace } = options;
975
+ const operations = collectOperations(routers);
976
+ const schemaMap = collectSchemaMap(operations);
977
+ const files = {};
978
+ files["models.ts"] = generateModels$1(schemaMap, identifier, namespace);
979
+ for (const operation of operations) files[`${camelCase(operation.id)}.ts`] = generateOpFile(operation, schemaMap, identifier, namespace);
980
+ files["index.ts"] = generateIndex(operations, identifier);
981
+ return files;
982
+ }
983
+ function generateModels$1(schemaMap, identifier, namespace) {
984
+ const lines = [];
985
+ lines.push(`import { z } from "zod"`);
986
+ lines.push("");
987
+ for (const [id, schemaInfo] of schemaMap) {
988
+ const schemaName = camelCase(id) + "Schema";
989
+ const tsName = identifier(id);
990
+ switch (schemaInfo.kind) {
991
+ case "record":
992
+ lines.push(`export const ${schemaName} = z.object({`);
993
+ for (const field of schemaInfo.fields) lines.push(` ${field.name}: ${toZod$1(field.model, schemaMap)}${field.required ? "" : ".optional()"},`);
994
+ lines.push(`})`);
995
+ lines.push("");
996
+ lines.push(`export interface ${tsName} {`);
997
+ for (const field of schemaInfo.fields) lines.push(` ${field.name}${field.required ? "" : "?"}: ${toTs$1(field.model, schemaMap, identifier, namespace)};`);
998
+ lines.push(`}`);
999
+ lines.push("");
1000
+ break;
1001
+ case "enums":
1002
+ lines.push(`export const ${schemaName} = z.enum(${JSON.stringify(Object.values(schemaInfo.variants))})`);
1003
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1004
+ lines.push("");
1005
+ break;
1006
+ case "union": {
1007
+ const unionItems = Object.entries(schemaInfo.unionVariants).map(([, v]) => toZod$1(v, schemaMap)).join(", ");
1008
+ lines.push(`export const ${schemaName} = z.union([${unionItems}])`);
1009
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1010
+ lines.push("");
1011
+ break;
1012
+ }
1013
+ case "taggedUnion": {
1014
+ const variantKey = schemaInfo.variantKey;
1015
+ const payloadKey = schemaInfo.payloadKey;
1016
+ const unionItems = Object.entries(schemaInfo.unionVariants).map(([key, v]) => `z.object({ ${JSON.stringify(variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(payloadKey)}: ${toZod$1(v, schemaMap)} })`);
1017
+ lines.push(`export const ${schemaName} = z.discriminatedUnion(${JSON.stringify(variantKey)}, [${unionItems.join(", ")}])`);
1018
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1019
+ lines.push("");
1020
+ break;
1021
+ }
1022
+ }
784
1023
  }
785
- }
786
- function generateModelSignature(model) {
787
- switch (model.kind) {
788
- case "string": return "String";
789
- case "int32": return "i32";
790
- case "int64": return "i64";
791
- case "float32": return "f32";
792
- case "float64": return "f64";
793
- case "boolean": return "bool";
794
- case "date": return "chrono::NaiveDate";
795
- case "datetime": return "chrono::DateTime<chrono::Utc>";
796
- case "duration": return "serde_json::Value";
797
- case "array": return `Vec<${generateModelSignature(model.base)}>`;
798
- case "set": return `Vec<${generateModelSignature(model.base)}>`;
799
- case "map": return `std::collections::HashMap<String, ${generateModelSignature(model.base)}>`;
800
- case "optional": return `Option<${generateModelSignature(model.base)}>`;
801
- case "literal": switch (typeof model.value) {
802
- case "string": return "&str";
803
- case "boolean": return "bool";
804
- case "number": return Number.isInteger(model.value) ? "i64" : "f64";
805
- default: throw Error("Unsupported literal value");
1024
+ return lines.join("\n");
1025
+ }
1026
+ function generateOpFile(operation, schemaMap, identifier, namespace) {
1027
+ const lines = [];
1028
+ const OperationName = pascalCase(operation.id);
1029
+ const operationName = camelCase(operation.id);
1030
+ const hasBody = operation.requestModel != null && operation.requestModel.kind !== "null";
1031
+ const hasParams = Object.keys(operation.pathVariables).length > 0;
1032
+ const hasQuery = Object.keys(operation.queries).length > 0;
1033
+ const hasHeaders = Object.keys(operation.headers).length > 0;
1034
+ if (hasParams || hasQuery || hasHeaders) lines.push(`import { z } from "zod"`);
1035
+ const schemaImports = [];
1036
+ if (hasBody && "id" in operation.requestModel) {
1037
+ const root = resolveNamedRoot(operation.requestModel);
1038
+ if (root && schemaMap.has(root.id)) schemaImports.push(camelCase(root.id) + "Schema");
1039
+ }
1040
+ const typeImports = [];
1041
+ if (hasBody && "id" in operation.requestModel) {
1042
+ const root = resolveNamedRoot(operation.requestModel);
1043
+ if (root) {
1044
+ const typeName = identifier(root.id);
1045
+ if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
1046
+ }
1047
+ }
1048
+ for (const responseModel of Object.values(operation.responses)) {
1049
+ if (responseModel == null) continue;
1050
+ const root = resolveNamedRoot(responseModel);
1051
+ if (root) {
1052
+ const typeName = identifier(root.id);
1053
+ if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
1054
+ }
1055
+ }
1056
+ const allImports = [...new Set([...schemaImports, ...typeImports])];
1057
+ lines.push(`import type { Context } from "hono"`);
1058
+ if (allImports.length > 0 || schemaImports.length > 0) {
1059
+ const parts = [];
1060
+ if (typeImports.length > 0) parts.push(typeImports.join(", "));
1061
+ if (schemaImports.length > 0) parts.push(schemaImports.join(", "));
1062
+ lines.push(`import { ${parts.join(", ")} } from "./models"`);
1063
+ }
1064
+ lines.push("");
1065
+ if (hasParams) {
1066
+ const fields = Object.entries(operation.pathVariables).map(([key, value]) => ` ${key}: ${toZod$1(value.model, schemaMap)},`);
1067
+ lines.push(`const ${operationName}Params = z.object({`);
1068
+ lines.push(...fields);
1069
+ lines.push(`})`);
1070
+ lines.push("");
1071
+ }
1072
+ if (hasQuery) {
1073
+ const fields = Object.entries(operation.queries).map(([key, query]) => ` ${key}: ${toZod$1(query.model, schemaMap)}${query.required ? "" : ".optional()"},`);
1074
+ lines.push(`const ${operationName}Query = z.object({`);
1075
+ lines.push(...fields);
1076
+ lines.push(`})`);
1077
+ lines.push("");
1078
+ }
1079
+ if (hasHeaders) {
1080
+ const fields = Object.entries(operation.headers).map(([key, header]) => ` "${key}": ${toZod$1(header.model, schemaMap)}${header.required ? "" : ".optional()"},`);
1081
+ lines.push(`const ${operationName}Headers = z.object({`);
1082
+ lines.push(...fields);
1083
+ lines.push(`})`);
1084
+ lines.push("");
1085
+ }
1086
+ const requestFields = [];
1087
+ if (hasParams) {
1088
+ const field = Object.entries(operation.pathVariables).map(([key, value]) => `${key}: ${toTs$1(value.model, schemaMap, identifier, namespace)}`).join("; ");
1089
+ requestFields.push(`params: { ${field} }`);
1090
+ }
1091
+ if (hasQuery) {
1092
+ const field = Object.entries(operation.queries).map(([key, query]) => `${key}${query.required ? "" : "?"}: ${toTs$1(query.model, schemaMap, identifier, namespace)}`).join("; ");
1093
+ requestFields.push(`query: { ${field} }`);
1094
+ }
1095
+ if (hasHeaders) {
1096
+ const field = Object.entries(operation.headers).map(([key, header]) => `"${key}"${header.required ? "" : "?"}: ${toTs$1(header.model, schemaMap, identifier, namespace)}`).join("; ");
1097
+ requestFields.push(`headers: { ${field} }`);
1098
+ }
1099
+ if (hasBody) requestFields.push(`body: ${toTs$1(operation.requestModel, schemaMap, identifier, namespace)}`);
1100
+ lines.push(`export interface ${OperationName}Request {`);
1101
+ for (const field of requestFields) lines.push(` ${field}`);
1102
+ lines.push(`}`);
1103
+ lines.push("");
1104
+ const responseEntries = Object.entries(operation.responses);
1105
+ const responseKind = (status) => operation.responseKinds[Number(status)] ?? "json-response";
1106
+ const responseBodyField = (kind, model) => {
1107
+ if (model == null) {
1108
+ if (kind === "binary") return "body: Blob";
1109
+ return null;
1110
+ }
1111
+ if (kind === "stream-response" || kind === "sse-response") return `stream: ReadableStream<${toTs$1(model, schemaMap, identifier, namespace)}>`;
1112
+ if (kind === "binary") return "body: Blob";
1113
+ return `body: ${toTs$1(model, schemaMap, identifier, namespace)}`;
1114
+ };
1115
+ if (responseEntries.length === 1) {
1116
+ const [status, responseModel] = responseEntries[0];
1117
+ const bodyField = responseBodyField(responseKind(status), responseModel);
1118
+ if (bodyField != null) lines.push(`export type ${OperationName}Response = { status: ${status}; ${bodyField} }`);
1119
+ else lines.push(`export type ${OperationName}Response = { status: ${status} }`);
1120
+ } else {
1121
+ lines.push(`export type ${OperationName}Response =`);
1122
+ const parts = responseEntries.map(([status, responseModel]) => {
1123
+ const bodyField = responseBodyField(responseKind(status), responseModel);
1124
+ if (bodyField != null) return ` | { status: ${status}; ${bodyField} }`;
1125
+ return ` | { status: ${status} }`;
1126
+ });
1127
+ lines.push(parts.join("\n"));
1128
+ }
1129
+ lines.push("");
1130
+ if (requestFields.length > 0) lines.push(`export type ${OperationName}Handler = (req: ${OperationName}Request) => Promise<${OperationName}Response>`);
1131
+ else lines.push(`export type ${OperationName}Handler = () => Promise<${OperationName}Response>`);
1132
+ lines.push("");
1133
+ lines.push(`export function ${operationName}(handler: ${OperationName}Handler) {`);
1134
+ lines.push(` return {`);
1135
+ lines.push(` method: "${operation.method.toUpperCase()}",`);
1136
+ lines.push(` path: "${toHonoPath(operation.path)}",`);
1137
+ lines.push(` async handler(context: Context): Promise<Response> {`);
1138
+ const requestArgs = [];
1139
+ if (hasParams) {
1140
+ lines.push(` const params = ${operationName}Params.parse(context.req.param())`);
1141
+ requestArgs.push("params");
1142
+ }
1143
+ if (hasQuery) {
1144
+ lines.push(` const query = ${operationName}Query.parse(context.req.query())`);
1145
+ requestArgs.push("query");
1146
+ }
1147
+ if (hasHeaders) {
1148
+ const headerParts = Object.keys(operation.headers).map((key) => ` "${key}": context.req.header("${key}"),`);
1149
+ lines.push(` const headers = ${operationName}Headers.parse({`);
1150
+ lines.push(...headerParts);
1151
+ lines.push(` })`);
1152
+ requestArgs.push("headers");
1153
+ }
1154
+ if (hasBody) {
1155
+ const schemaName = camelCase(operation.requestModel.id) + "Schema";
1156
+ lines.push(` const body = ${schemaName}.parse(await context.req.json())`);
1157
+ requestArgs.push("body");
1158
+ }
1159
+ if (requestArgs.length > 0) lines.push(` const result = await handler({ ${requestArgs.join(", ")} })`);
1160
+ else lines.push(` const result = await handler()`);
1161
+ lines.push(` switch (result.status) {`);
1162
+ for (const [status, responseModel] of Object.entries(operation.responses)) {
1163
+ const kind = operation.responseKinds[Number(status)] ?? "json-response";
1164
+ if (responseModel != null) if (kind === "json-response") lines.push(` case ${status}: return new Response(JSON.stringify(result.body), { status: ${status}, headers: { "Content-Type": "application/json" } })`);
1165
+ else if (kind === "binary") lines.push(` case ${status}: return new Response(result.body, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
1166
+ else lines.push(` case ${status}: return new Response(result.stream, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
1167
+ else if (kind === "binary") lines.push(` case ${status}: return new Response(result.body, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
1168
+ else lines.push(` case ${status}: return new Response(null, { status: ${status} })`);
1169
+ }
1170
+ lines.push(` default: return new Response(JSON.stringify({ message: \`Unexpected response status \${(result as { status: number }).status}\` }), { status: 500, headers: { "Content-Type": "application/json" } })`);
1171
+ lines.push(` }`);
1172
+ lines.push(` },`);
1173
+ lines.push(` }`);
1174
+ lines.push(`}`);
1175
+ return lines.join("\n");
1176
+ }
1177
+ function generateIndex(operations, _identifier) {
1178
+ const lines = [];
1179
+ lines.push(`import { Hono } from "hono"`);
1180
+ lines.push("");
1181
+ for (const operation of operations) {
1182
+ const operationName = camelCase(operation.id);
1183
+ const OperationName = pascalCase(operation.id);
1184
+ lines.push(`import { ${operationName}, type ${OperationName}Handler, type ${OperationName}Request, type ${OperationName}Response } from "./${operationName}"`);
1185
+ lines.push(`export type { ${OperationName}Handler, ${OperationName}Request, ${OperationName}Response }`);
1186
+ }
1187
+ lines.push("");
1188
+ const groups = groupBy$1(operations, (operation) => operation.group);
1189
+ lines.push(`export function mountRoutes(`);
1190
+ lines.push(` app: Hono,`);
1191
+ lines.push(` handlers: {`);
1192
+ for (const [group, groupOps] of Object.entries(groups)) {
1193
+ lines.push(` ${camelCase(group)}: {`);
1194
+ for (const operation of groupOps) {
1195
+ const operationName = camelCase(operation.id);
1196
+ lines.push(` ${operationName}: ${pascalCase(operation.id)}Handler,`);
806
1197
  }
807
- default: return model.id ? toPascalCase(model.id) : "()";
1198
+ lines.push(` },`);
1199
+ }
1200
+ lines.push(` },`);
1201
+ lines.push(`) {`);
1202
+ for (const [group, groupOps] of Object.entries(groups)) {
1203
+ lines.push(` // ── ${group} ──`);
1204
+ for (const operation of groupOps) {
1205
+ const operationName = camelCase(operation.id);
1206
+ lines.push(` const ${operationName}Def = ${operationName}(handlers.${camelCase(group)}.${operationName})`);
1207
+ lines.push(` app.on(${operationName}Def.method, ${operationName}Def.path, ${operationName}Def.handler)`);
1208
+ }
1209
+ lines.push("");
1210
+ }
1211
+ lines.push(`}`);
1212
+ lines.push("");
1213
+ return lines.join("\n");
1214
+ }
1215
+ function groupBy$1(items, keyFn) {
1216
+ const map = {};
1217
+ for (const item of items) {
1218
+ const key = keyFn(item);
1219
+ if (!map[key]) map[key] = [];
1220
+ map[key].push(item);
808
1221
  }
1222
+ return map;
809
1223
  }
810
- function toPascalCase(str) {
811
- return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c ? c.toUpperCase() : "");
1224
+ function toHonoPath(path) {
1225
+ return path.replace(/\{(\w+)\}/g, ":$1");
812
1226
  }
813
- function toSnakeCase(str) {
814
- return str.replace(/([A-Z])/g, "_$1").replace(/[-\s]+/g, "_").replace(/^_/, "").toLowerCase();
1227
+ function contentTypeForKind(kind) {
1228
+ switch (kind) {
1229
+ case "binary": return "application/octet-stream";
1230
+ case "stream-response": return "application/x-ndjson";
1231
+ case "sse-response": return "text/event-stream";
1232
+ default: return "application/json";
1233
+ }
815
1234
  }
816
- function toUpperSnakeCase(str) {
817
- return toSnakeCase(str).toUpperCase();
1235
+ function toZod$1(model, schemaMap) {
1236
+ switch (model.kind) {
1237
+ case "int32": return "z.coerce.number().int()";
1238
+ case "float32":
1239
+ case "float64": return "z.coerce.number()";
1240
+ case "boolean": return "z.coerce.boolean()";
1241
+ case "string": return "z.string()";
1242
+ case "datetime": return "z.string().datetime()";
1243
+ case "date": return "z.string().date()";
1244
+ case "duration": return "z.string()";
1245
+ case "literal": return `z.literal(${JSON.stringify(model.value)})`;
1246
+ case "null": return "z.null()";
1247
+ case "array": return `${toZod$1(model.base, schemaMap)}.array()`;
1248
+ case "set": return `${toZod$1(model.base, schemaMap)}.array()`;
1249
+ case "map": return `z.record(z.string(), ${toZod$1(model.base, schemaMap)})`;
1250
+ case "enums": return `z.enum(${JSON.stringify(Object.values(model.variants))})`;
1251
+ case "record": return schemaMap.get(model.id)?.fields ? camelCase(model.id) + "Schema" : "z.unknown()";
1252
+ case "union":
1253
+ if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
1254
+ return `z.union([${Object.values(model.variants).map((v) => toZod$1(v, schemaMap)).join(", ")}])`;
1255
+ case "taggedUnion": {
1256
+ if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
1257
+ const unionItems = Object.entries(model.variants).map(([key, v]) => `z.object({ ${JSON.stringify(model.variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(model.payloadKey)}: ${toZod$1(v, schemaMap)} })`);
1258
+ return `z.discriminatedUnion(${JSON.stringify(model.variantKey)}, [${unionItems.join(", ")}])`;
1259
+ }
1260
+ default: return "z.unknown()";
1261
+ }
818
1262
  }
819
- async function formatRust(code) {
820
- try {
821
- return await format(code, {
822
- parser: "jinx-rust",
823
- plugins: [PrettierRustPlugin]
824
- });
825
- } catch (error) {
826
- console.error(error);
827
- return code;
828
- }
829
- }
830
- async function generateRustCodes(options) {
831
- const { module_name, srcDir, routes = [], models = [] } = options;
832
- const codes = /* @__PURE__ */ new Map();
833
- const namedModels = collectModelFromRoutes(routes);
834
- for (const model of models) for (const [id, subModel] of collectModelDeep(model)) namedModels.set(id, subModel);
835
- for (const [id, model] of namedModels) {
836
- if (model.kind !== "union" && model.kind !== "record" && model.kind !== "enums" && model.kind !== "error") continue;
837
- const fullname = module_name + "::" + toSnakeCase(id);
838
- const path = resolve(srcDir, ...fullname.split("::"), ".rs");
839
- const code = await formatRust(generateRustCode({
840
- module_name: fullname,
841
- model
842
- }));
843
- codes.set(path, code);
844
- }
845
- return codes.entries().map(([path, code]) => ({
846
- path,
847
- code
848
- })).toArray();
1263
+ function toTs$1(model, schemaMap, identifier, namespace) {
1264
+ switch (model.kind) {
1265
+ case "int32":
1266
+ case "float32":
1267
+ case "float64": return "number";
1268
+ case "boolean": return "boolean";
1269
+ case "string":
1270
+ case "datetime":
1271
+ case "date":
1272
+ case "duration": return "string";
1273
+ case "literal": return JSON.stringify(model.value);
1274
+ case "null": return "null";
1275
+ case "array":
1276
+ case "set": return `${toTs$1(model.base, schemaMap, identifier, namespace)}[]`;
1277
+ case "map": return `Record<string, ${toTs$1(model.base, schemaMap, identifier, namespace)}>`;
1278
+ case "enums":
1279
+ if (schemaMap.get(model.id)?.variants) return identifier(model.id);
1280
+ return Object.values(model.variants).map((v) => JSON.stringify(v)).join(" | ");
1281
+ case "record": return schemaMap.get(model.id)?.fields ? identifier(model.id) : "unknown";
1282
+ case "union":
1283
+ case "taggedUnion":
1284
+ if (schemaMap.get(model.id)?.unionVariants) return identifier(model.id);
1285
+ return Object.values(model.variants).map((v) => toTs$1(v, schemaMap, identifier, namespace)).join(" | ");
1286
+ default: return "unknown";
1287
+ }
849
1288
  }
850
1289
  //#endregion
851
- //#region src/type-system/serialize.ts
852
- function deserialize(model, json) {
853
- return deserializeInner(model, JSON.parse(json));
854
- }
855
- function deserializeInner(model, value) {
856
- const success = (value) => ({
857
- isSuccess: true,
858
- value
859
- });
860
- if (model.kind === "string") {
861
- if (typeof value === "string") return success(value);
862
- return deserializeFailed(model, value);
863
- }
864
- if (model.kind === "boolean") {
865
- if (typeof value === "boolean") return success(value);
866
- return deserializeFailed(model, value);
867
- }
868
- if (model.kind === "int32" || model.kind === "float32" || model.kind === "float64") {
869
- if (typeof value === "number") return success(value);
870
- return deserializeFailed(model, value);
871
- }
872
- if (model.kind === "int64") {
873
- if (typeof value === "string") try {
874
- return success(BigInt(value));
875
- } catch {
876
- return deserializeFailed(model, value);
1290
+ //#region src/codegen/ts-client.ts
1291
+ function generateTsClient(options) {
1292
+ const { routers, identifier = pascalCase, namespace } = options;
1293
+ const operations = collectOperations(routers);
1294
+ const schemaMap = collectSchemaMap(operations);
1295
+ const files = {};
1296
+ files["models.ts"] = generateModels(schemaMap, identifier, namespace);
1297
+ for (const operation of operations) files[`${camelCase(operation.id)}.ts`] = generateClientFn(operation, schemaMap, identifier, namespace);
1298
+ files["index.ts"] = generateClientIndex(operations, identifier);
1299
+ return files;
1300
+ }
1301
+ function generateModels(schemaMap, identifier, namespace) {
1302
+ const lines = [];
1303
+ lines.push(`import { z } from "zod"`);
1304
+ lines.push("");
1305
+ for (const [id, schemaInfo] of schemaMap) {
1306
+ const schemaName = camelCase(id) + "Schema";
1307
+ const tsName = identifier(id);
1308
+ switch (schemaInfo.kind) {
1309
+ case "record":
1310
+ lines.push(`export const ${schemaName} = z.object({`);
1311
+ for (const f of schemaInfo.fields) lines.push(` ${f.name}: ${toZod(f.model, schemaMap)}${f.required ? "" : ".optional()"},`);
1312
+ lines.push(`})`);
1313
+ lines.push("");
1314
+ lines.push(`export interface ${tsName} {`);
1315
+ for (const f of schemaInfo.fields) lines.push(` ${f.name}${f.required ? "" : "?"}: ${toTs(f.model, schemaMap, identifier, namespace)};`);
1316
+ lines.push(`}`);
1317
+ lines.push("");
1318
+ break;
1319
+ case "enums":
1320
+ lines.push(`export const ${schemaName} = z.enum(${JSON.stringify(Object.values(schemaInfo.variants))})`);
1321
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1322
+ lines.push("");
1323
+ break;
1324
+ case "union": {
1325
+ const items = Object.entries(schemaInfo.unionVariants).map(([, v]) => toZod(v, schemaMap)).join(", ");
1326
+ lines.push(`export const ${schemaName} = z.union([${items}])`);
1327
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1328
+ lines.push("");
1329
+ break;
1330
+ }
1331
+ case "taggedUnion": {
1332
+ const vk = schemaInfo.variantKey;
1333
+ const pk = schemaInfo.payloadKey;
1334
+ const items = Object.entries(schemaInfo.unionVariants).map(([key, v]) => `z.object({ ${JSON.stringify(vk)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(pk)}: ${toZod(v, schemaMap)} })`);
1335
+ lines.push(`export const ${schemaName} = z.discriminatedUnion(${JSON.stringify(vk)}, [${items.join(", ")}])`);
1336
+ lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
1337
+ lines.push("");
1338
+ break;
1339
+ }
877
1340
  }
878
- return deserializeFailed(model, value);
879
1341
  }
880
- if (model.kind === "date" || model.kind === "datetime") {
881
- if (typeof value === "string") try {
882
- return success(Temporal.Instant.from(value));
883
- } catch {
884
- return deserializeFailed(model, value);
1342
+ return lines.join("\n");
1343
+ }
1344
+ function generateClientFn(operation, schemaMap, identifier, namespace) {
1345
+ const lines = [];
1346
+ const functionName = camelCase(operation.id);
1347
+ const OperationName = pascalCase(operation.id);
1348
+ const hasBody = operation.requestModel != null && operation.requestModel.kind !== "null";
1349
+ const hasParams = Object.keys(operation.pathVariables).length > 0;
1350
+ const hasQuery = Object.keys(operation.queries).length > 0;
1351
+ const hasHeaders = Object.keys(operation.headers).length > 0;
1352
+ const typeImports = [];
1353
+ const addNamedRef = (model) => {
1354
+ const root = resolveNamedRoot(model);
1355
+ if (root) {
1356
+ const typeName = identifier(root.id);
1357
+ if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
885
1358
  }
886
- return deserializeFailed(model, value);
1359
+ };
1360
+ if (hasBody) addNamedRef(operation.requestModel);
1361
+ for (const responseModel of Object.values(operation.responses)) if (responseModel != null) addNamedRef(responseModel);
1362
+ const okStatus = Object.keys(operation.responses).find((s) => Number(s) >= 200 && Number(s) < 300);
1363
+ const okModel = okStatus ? operation.responses[Number(okStatus)] : null;
1364
+ const okKind = okStatus ? operation.responseKinds[Number(okStatus)] ?? "json-response" : "json-response";
1365
+ const isStreamLike = okKind === "stream-response" || okKind === "sse-response" || okKind === "binary";
1366
+ const okSchema = okModel ? resolveZodSchema(okModel, schemaMap) : null;
1367
+ const allModelImports = [...typeImports];
1368
+ if (okSchema && !isStreamLike) {
1369
+ const schemaRefs = collectSchemaRefs(okModel, schemaMap);
1370
+ for (const ref of schemaRefs) if (!allModelImports.includes(ref)) allModelImports.push(ref);
887
1371
  }
888
- if (model.kind === "duration") {
889
- if (typeof value === "string") try {
890
- return success(Temporal.Duration.from(value));
891
- } catch {
892
- return deserializeFailed(model, value);
893
- }
894
- return deserializeFailed(model, value);
895
- }
896
- if (model.kind === "optional") return value == null ? success(void 0) : deserializeInner(model.base, value);
897
- if (model.kind === "literal") {
898
- if (model.value === value) return success(value);
899
- return deserializeFailed(model, value);
900
- }
901
- if (model.kind === "array") {
902
- if (typeof value === "object" && Array.isArray(value)) return success(value.map((o) => deserializeInner(model.base, o)));
903
- return deserializeFailed(model, value);
904
- }
905
- if (model.kind === "set") {
906
- if (typeof value === "object" && Array.isArray(value)) return success(new Set(value.map((o) => deserializeInner(model.base, o))));
907
- return deserializeFailed(model, value);
908
- }
909
- if (model.kind === "map") {
910
- if (typeof value === "object" && !Array.isArray(value)) {
911
- const properties = /* @__PURE__ */ new Map();
912
- for (const key in value) {
913
- const result = deserializeInner(model.base, value[key]);
914
- if (result.isSuccess) properties.set(key, result.value);
915
- else return result;
916
- }
917
- return success(properties);
918
- }
919
- return deserializeFailed(model, value);
920
- }
921
- if (model.kind === "enums") {
922
- if (typeof value === "string" && Object.values(model.variants).includes(value)) return success(value);
923
- return deserializeFailed(model, value);
924
- }
925
- if (model.kind === "record") {
926
- if (typeof value === "object" && !Array.isArray(value)) {
927
- const properties = /* @__PURE__ */ new Map();
928
- for (const key in model.properties) {
929
- const result = deserializeInner(model.properties[key], value[key]);
930
- if (result.isSuccess) properties.set(key, result.value);
931
- else return deserializeFailed(model, value);
932
- }
933
- return success(Object.fromEntries(properties));
1372
+ if (allModelImports.length > 0) {
1373
+ lines.push(`import { ${allModelImports.join(", ")} } from "./models"`);
1374
+ lines.push("");
1375
+ }
1376
+ const requestFields = [];
1377
+ for (const [n, v] of Object.entries(operation.pathVariables)) requestFields.push(`${n}: ${toTs(v.model, schemaMap, identifier, namespace)}`);
1378
+ for (const [n, q] of Object.entries(operation.queries)) {
1379
+ const required = q.required ? "" : "?";
1380
+ requestFields.push(`${n}${required}: ${toTs(q.model, schemaMap, identifier, namespace)}`);
1381
+ }
1382
+ if (hasBody) requestFields.push(`body: ${toTs(operation.requestModel, schemaMap, identifier, namespace)}`);
1383
+ if (hasHeaders) {
1384
+ const field = Object.entries(operation.headers).map(([key, header]) => `"${key}"${header.required ? "" : "?"}: ${toTs(header.model, schemaMap, identifier, namespace)}`).join("; ");
1385
+ requestFields.push(`headers?: { ${field} } & Record<string, string>`);
1386
+ } else requestFields.push("headers?: Record<string, string>");
1387
+ requestFields.push("baseUrl?: string");
1388
+ lines.push(`export interface ${OperationName}Request {`);
1389
+ for (const field of requestFields) lines.push(` ${field}`);
1390
+ lines.push(`}`);
1391
+ lines.push("");
1392
+ const responseEntries = Object.entries(operation.responses);
1393
+ const responseKind = (status) => operation.responseKinds[Number(status)] ?? "json-response";
1394
+ const responseBodyField = (kind, responseModel) => {
1395
+ if (responseModel == null) {
1396
+ if (kind === "binary") return "body: Blob";
1397
+ return null;
934
1398
  }
935
- return deserializeFailed(model, value);
936
- }
937
- if (model.kind === "union") {
938
- if (typeof value === "object" && !Array.isArray(value)) {
939
- const variantKeys = Object.keys(model.variants);
940
- for (const key of variantKeys) if (key in value) {
941
- const result = deserializeInner(model.variants[key], value[key]);
942
- if (result.isSuccess) return success({ [key]: result.value });
943
- }
1399
+ if (kind === "stream-response" || kind === "sse-response") return `stream: ReadableStream<${toTs(responseModel, schemaMap, identifier, namespace)}>`;
1400
+ if (kind === "binary") return "body: Blob";
1401
+ return `body: ${toTs(responseModel, schemaMap, identifier, namespace)}`;
1402
+ };
1403
+ if (responseEntries.length === 1) {
1404
+ const [status, responseModel] = responseEntries[0];
1405
+ const bodyField = responseBodyField(responseKind(status), responseModel);
1406
+ if (bodyField != null) lines.push(`export type ${OperationName}Response = { status: ${status}; ${bodyField} }`);
1407
+ else lines.push(`export type ${OperationName}Response = { status: ${status} }`);
1408
+ } else {
1409
+ lines.push(`export type ${OperationName}Response =`);
1410
+ for (const [status, responseModel] of responseEntries) {
1411
+ const bodyField = responseBodyField(responseKind(status), responseModel);
1412
+ if (bodyField != null) lines.push(` | { status: ${status}; ${bodyField} }`);
1413
+ else lines.push(` | { status: ${status} }`);
944
1414
  }
945
- return deserializeFailed(model, value);
946
- }
947
- if (model.kind === "tagged-union") {
948
- if (typeof value === "object" && !Array.isArray(value)) {
949
- const tag = value[model.tag];
950
- for (const [key, variant] of Object.entries(model.variants)) {
951
- const tagModel = variant.properties[model.tag];
952
- if (tagModel.kind === "literal" && tagModel.value === tag) {
953
- const result = deserializeInner(variant, value);
954
- if (result.isSuccess) return success({ [key]: result.value });
955
- }
956
- }
1415
+ }
1416
+ lines.push("");
1417
+ const reqParam = hasParams || hasBody ? `req: ${OperationName}Request` : `req?: ${OperationName}Request`;
1418
+ const retType = okModel && !isStreamLike ? toTs(okModel, schemaMap, identifier, namespace) : "Response";
1419
+ const pathExpr = operation.path.replace(/\{(\w+)\}/g, (_, name) => `\${encodeURIComponent(req.${name})}`);
1420
+ lines.push(`export async function ${functionName}(${reqParam}): Promise<${retType}> {`);
1421
+ lines.push(` const baseUrl = req?.baseUrl ?? ""`);
1422
+ if (hasQuery) {
1423
+ lines.push(` const parts: string[] = []`);
1424
+ for (const [n, q] of Object.entries(operation.queries)) if (q.required) lines.push(` parts.push("${n}=" + encodeURIComponent(req.${n}))`);
1425
+ else lines.push(` if (req?.${n} != null) parts.push("${n}=" + encodeURIComponent(req.${n}))`);
1426
+ lines.push(` const qs = parts.length > 0 ? "?" + parts.join("&") : ""`);
1427
+ lines.push(` const url = \`\${baseUrl}${pathExpr}\${qs}\``);
1428
+ } else lines.push(` const url = \`\${baseUrl}${pathExpr}\``);
1429
+ lines.push("");
1430
+ lines.push(` const res = await fetch(url, {`);
1431
+ lines.push(` method: "${operation.method}",`);
1432
+ if (hasBody) {
1433
+ lines.push(` headers: { "Content-Type": "application/json", ...req.headers },`);
1434
+ lines.push(` body: JSON.stringify(req.body),`);
1435
+ } else lines.push(` headers: req?.headers,`);
1436
+ lines.push(` })`);
1437
+ lines.push(` if (!res.ok) throw new Error(\`${operation.method} ${operation.path} failed: \${res.status}\`)`);
1438
+ if (okModel && !isStreamLike) if (okSchema) lines.push(` return ${okSchema}.parse(await res.json())`);
1439
+ else lines.push(` return res.json()`);
1440
+ else lines.push(` return res`);
1441
+ lines.push(`}`);
1442
+ return lines.join("\n");
1443
+ }
1444
+ function generateClientIndex(operations, _identifier) {
1445
+ const lines = [];
1446
+ const groups = groupBy(operations, (operation) => operation.group);
1447
+ for (const [group, groupOps] of Object.entries(groups)) {
1448
+ lines.push(`// ── ${group} ──`);
1449
+ for (const operation of groupOps) {
1450
+ const n = camelCase(operation.id);
1451
+ const OperationName = pascalCase(operation.id);
1452
+ lines.push(`export { ${n}, type ${OperationName}Request, type ${OperationName}Response } from "./${n}"`);
957
1453
  }
958
- return deserializeFailed(model, value);
959
- }
960
- if (typeof value === "object" && !Array.isArray(value) && "code" in value && "context" in value && value.code === model.code && typeof value.context === "object" && !Array.isArray(value.context)) {
961
- const properties = /* @__PURE__ */ new Map();
962
- for (const key in model.context) {
963
- const result = deserializeInner(model.context[key], value[key]);
964
- if (result.isSuccess) properties.set(key, result.value);
965
- else return result;
1454
+ lines.push("");
1455
+ }
1456
+ return lines.join("\n");
1457
+ }
1458
+ function groupBy(items, keyFn) {
1459
+ const map = {};
1460
+ for (const item of items) {
1461
+ const key = keyFn(item);
1462
+ if (!map[key]) map[key] = [];
1463
+ map[key].push(item);
1464
+ }
1465
+ return map;
1466
+ }
1467
+ function toTs(model, schemaMap, identifier, namespace) {
1468
+ switch (model.kind) {
1469
+ case "int32":
1470
+ case "float32":
1471
+ case "float64": return "number";
1472
+ case "boolean": return "boolean";
1473
+ case "string":
1474
+ case "datetime":
1475
+ case "date":
1476
+ case "duration": return "string";
1477
+ case "literal": return JSON.stringify(model.value);
1478
+ case "null": return "null";
1479
+ case "array":
1480
+ case "set": return `${toTs(model.base, schemaMap, identifier, namespace)}[]`;
1481
+ case "map": return `Record<string, ${toTs(model.base, schemaMap, identifier, namespace)}>`;
1482
+ case "enums":
1483
+ if (schemaMap.get(model.id)?.variants) return identifier(model.id);
1484
+ return Object.values(model.variants).map((v) => JSON.stringify(v)).join(" | ");
1485
+ case "record": return schemaMap.get(model.id)?.fields ? identifier(model.id) : "unknown";
1486
+ case "union":
1487
+ case "taggedUnion":
1488
+ if (schemaMap.get(model.id)?.unionVariants) return identifier(model.id);
1489
+ return Object.values(model.variants).map((v) => toTs(v, schemaMap, identifier, namespace)).join(" | ");
1490
+ default: return "unknown";
1491
+ }
1492
+ }
1493
+ function toZod(model, schemaMap) {
1494
+ switch (model.kind) {
1495
+ case "int32": return "z.coerce.number().int()";
1496
+ case "float32":
1497
+ case "float64": return "z.coerce.number()";
1498
+ case "boolean": return "z.coerce.boolean()";
1499
+ case "string": return "z.string()";
1500
+ case "datetime": return "z.string().datetime()";
1501
+ case "date": return "z.string().date()";
1502
+ case "duration": return "z.string()";
1503
+ case "literal": return `z.literal(${JSON.stringify(model.value)})`;
1504
+ case "null": return "z.null()";
1505
+ case "array": return `${toZod(model.base, schemaMap)}.array()`;
1506
+ case "set": return `${toZod(model.base, schemaMap)}.array()`;
1507
+ case "map": return `z.record(z.string(), ${toZod(model.base, schemaMap)})`;
1508
+ case "enums": return `z.enum(${JSON.stringify(Object.values(model.variants))})`;
1509
+ case "record": return schemaMap.get(model.id)?.fields ? camelCase(model.id) + "Schema" : "z.unknown()";
1510
+ case "union":
1511
+ if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
1512
+ return `z.union([${Object.values(model.variants).map((v) => toZod(v, schemaMap)).join(", ")}])`;
1513
+ case "taggedUnion": {
1514
+ if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
1515
+ const items = Object.entries(model.variants).map(([key, v]) => `z.object({ ${JSON.stringify(model.variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(model.payloadKey)}: ${toZod(v, schemaMap)} })`);
1516
+ return `z.discriminatedUnion(${JSON.stringify(model.variantKey)}, [${items.join(", ")}])`;
966
1517
  }
967
- return success({
968
- code: model.code,
969
- context: Object.fromEntries(properties)
970
- });
1518
+ default: return "z.unknown()";
971
1519
  }
972
- return deserializeFailed(model, value);
973
1520
  }
974
- function deserializeFailed(expect, receive) {
975
- return {
976
- isSuccess: false,
977
- code: "deserial-failed",
978
- context: {
979
- expect,
980
- receive
1521
+ function resolveZodSchema(model, schemaMap) {
1522
+ switch (model.kind) {
1523
+ case "record":
1524
+ case "union":
1525
+ case "taggedUnion":
1526
+ case "enums": return schemaMap.get(model.id) ? camelCase(model.id) + "Schema" : null;
1527
+ case "array":
1528
+ case "set": {
1529
+ const inner = resolveZodSchema(model.base, schemaMap);
1530
+ return inner ? `${inner}.array()` : null;
981
1531
  }
982
- };
1532
+ case "map": {
1533
+ const inner = resolveZodSchema(model.base, schemaMap);
1534
+ return inner ? `z.record(z.string(), ${inner})` : null;
1535
+ }
1536
+ default: return null;
1537
+ }
983
1538
  }
984
- function serialize(model, value) {
985
- if (model.kind === "string" || model.kind === "int64") return `"${value.toString()}"`;
986
- if (model.kind === "int32" || model.kind === "float32" || model.kind === "float64" || model.kind === "boolean") return value.toString();
987
- if (model.kind === "array") return value.map((o) => serialize(model.base, o)).join(",");
988
- if (model.kind === "set") return value.values().map((o) => serialize(model.base, o)).toArray().join(",");
989
- if (model.kind === "map") return `{${value.entries().map(([k, v]) => `"${k}": ${serialize(model.base, v)}`)}}`;
990
- if (model.kind === "literal") return model.value.toString();
991
- if (model.kind === "date" || model.kind === "datetime" || model.kind === "duration") return value.toJSON();
992
- if (model.kind === "optional") return value == null ? "null" : serialize(model.base, value);
993
- if (model.kind === "error") {
994
- const properties = [];
995
- for (const key in model.context) properties.push(`"${key}": ${serialize(model.context[key], value[key])}`);
996
- return `{"code":"${model.code}",context:{${properties}}}`;
997
- }
998
- if (model.kind === "record") {
999
- const properties = [];
1000
- for (const key in model.properties) properties.push(`"${key}": ${serialize(model.properties[key], value[key])}`);
1001
- return `{${properties}}`;
1002
- }
1003
- if (model.kind === "union") {
1004
- const variantKeys = Object.keys(model.variants);
1005
- for (const key of variantKeys) if (key in value) return `{"${key}":${serialize(model.variants[key], value[key])}}`;
1006
- }
1007
- if (model.kind === "tagged-union") return JSON.stringify(value);
1008
- return String(value);
1539
+ function collectSchemaRefs(model, schemaMap) {
1540
+ switch (model.kind) {
1541
+ case "record":
1542
+ case "union":
1543
+ case "taggedUnion":
1544
+ case "enums": return schemaMap.get(model.id) ? [camelCase(model.id) + "Schema"] : [];
1545
+ case "array":
1546
+ case "set": return collectSchemaRefs(model.base, schemaMap);
1547
+ case "map": return collectSchemaRefs(model.base, schemaMap);
1548
+ default: return [];
1549
+ }
1009
1550
  }
1010
1551
  //#endregion
1011
- export { array, binary, boolean, collectModelDeep, collectModelFromRoutes, date, datetime, deserialize, deserializeInner, duration, enums, error, float32, float64, form, formatJava, formatRust, generateJavaClass, generateJavaCodes, generateJsonSchema, generateOpenapi, generateRustCode, generateRustCodes, int32, int64, json, jsonStream, literal, map, optional, plainText, record, route, serialize, set, sse, string, taggedUnion, union };
1552
+ export { apikey, array, binary, boolean, collectNamedModels, collectOperations, collectSchemaMap, createJsonSchemaRegistry, createOpenapiSchemaRegistry, date, datetime, deployOpenIdConnect, duration, enums, float32, float64, generateHonoServer, generateJsonSchema, generateOpenapi, generateTsClient, int32, json, jsonStream, literal, map, mergeJsonSchemas, nullLike, openIdConnect, record, resolveNamedRoot, route, set, sseStream, string, taggedUnion, union };
1012
1553
 
1013
1554
  //# sourceMappingURL=index.mjs.map