@huanglangjian/specs 0.3.1 → 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.d.mts +1725 -480
- package/dist/index.mjs +1382 -838
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -12
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
33
|
+
function array(options) {
|
|
43
34
|
return {
|
|
44
|
-
kind: "
|
|
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: "
|
|
58
|
-
base,
|
|
41
|
+
kind: "set",
|
|
59
42
|
...options
|
|
60
43
|
};
|
|
61
44
|
}
|
|
62
|
-
function
|
|
45
|
+
function map(options) {
|
|
63
46
|
return {
|
|
64
|
-
kind: "
|
|
65
|
-
base,
|
|
47
|
+
kind: "map",
|
|
66
48
|
...options
|
|
67
49
|
};
|
|
68
50
|
}
|
|
69
|
-
function
|
|
51
|
+
function record(options) {
|
|
70
52
|
return {
|
|
71
|
-
kind: "
|
|
72
|
-
base,
|
|
53
|
+
kind: "record",
|
|
73
54
|
...options
|
|
74
55
|
};
|
|
75
56
|
}
|
|
76
|
-
function
|
|
57
|
+
function union(options) {
|
|
77
58
|
return {
|
|
78
|
-
kind: "
|
|
79
|
-
base,
|
|
59
|
+
kind: "union",
|
|
80
60
|
...options
|
|
81
61
|
};
|
|
82
62
|
}
|
|
83
|
-
function
|
|
63
|
+
function taggedUnion(options) {
|
|
84
64
|
return {
|
|
85
|
-
kind: "
|
|
65
|
+
kind: "taggedUnion",
|
|
86
66
|
...options
|
|
87
67
|
};
|
|
88
68
|
}
|
|
89
|
-
function
|
|
69
|
+
function literal(value) {
|
|
90
70
|
return {
|
|
91
|
-
kind: "
|
|
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,901 +99,1456 @@ function duration(options) {
|
|
|
110
99
|
...options
|
|
111
100
|
};
|
|
112
101
|
}
|
|
113
|
-
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/api.ts
|
|
104
|
+
function route(options) {
|
|
114
105
|
return {
|
|
115
|
-
kind: "
|
|
106
|
+
kind: "route",
|
|
116
107
|
...options
|
|
117
108
|
};
|
|
118
109
|
}
|
|
119
|
-
function
|
|
110
|
+
function json(options) {
|
|
120
111
|
return {
|
|
121
|
-
kind: "
|
|
112
|
+
kind: "json-response",
|
|
122
113
|
...options
|
|
123
114
|
};
|
|
124
115
|
}
|
|
125
|
-
function
|
|
116
|
+
function jsonStream(options) {
|
|
117
|
+
return {
|
|
118
|
+
kind: "stream-response",
|
|
119
|
+
...options
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function sseStream(options) {
|
|
126
123
|
return {
|
|
127
|
-
kind: "
|
|
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,
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 "
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
242
|
-
additionalProperties: false
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
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/
|
|
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,
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
servers
|
|
516
|
+
registry
|
|
273
517
|
};
|
|
274
518
|
}
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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": return { content: { [content.type]: generateMeidaTypeObject(content) } };
|
|
401
|
-
default: return {
|
|
402
|
-
description: content.description,
|
|
403
|
-
content: { [content.type]: generateMeidaTypeObject(content) }
|
|
404
|
-
};
|
|
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 [];
|
|
405
557
|
}
|
|
406
558
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
case "json-content": return { schema: getSchemaReference(content.model) };
|
|
418
|
-
case "json-stream-content": return {
|
|
419
|
-
schema: {
|
|
420
|
-
type: "string",
|
|
421
|
-
format: "binary"
|
|
422
|
-
},
|
|
423
|
-
itemSchema: getSchemaReference(content.model)
|
|
424
|
-
};
|
|
425
|
-
case "binary-stream-content": return {
|
|
426
|
-
schema: {
|
|
427
|
-
type: "string",
|
|
428
|
-
format: "binary"
|
|
429
|
-
},
|
|
430
|
-
itemSchema: {
|
|
431
|
-
type: "string",
|
|
432
|
-
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)
|
|
433
568
|
}
|
|
434
569
|
};
|
|
435
|
-
|
|
436
|
-
}
|
|
570
|
+
}, {});
|
|
437
571
|
}
|
|
438
|
-
function
|
|
439
|
-
const
|
|
440
|
-
if (response.headers) for (const [name, model] of Object.entries(response.headers)) headerObjects.set(name, {
|
|
441
|
-
schema: getSchemaReference(model),
|
|
442
|
-
required: model.kind !== "optional"
|
|
443
|
-
});
|
|
572
|
+
function generateOperation(route, group, registry) {
|
|
573
|
+
const tags = route.tags?.includes(group) ? route.tags : [...route.tags ?? [], group];
|
|
444
574
|
return {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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)
|
|
448
581
|
};
|
|
449
582
|
}
|
|
450
|
-
function route
|
|
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) {
|
|
451
597
|
return {
|
|
452
|
-
|
|
453
|
-
|
|
598
|
+
name,
|
|
599
|
+
in: location,
|
|
600
|
+
required,
|
|
601
|
+
schema: getSchema(model, registry)
|
|
454
602
|
};
|
|
455
603
|
}
|
|
456
|
-
function
|
|
457
|
-
return
|
|
458
|
-
|
|
459
|
-
...options
|
|
460
|
-
};
|
|
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) } };
|
|
461
607
|
}
|
|
462
|
-
function
|
|
463
|
-
return {
|
|
464
|
-
kind: "json-content",
|
|
465
|
-
...options
|
|
466
|
-
};
|
|
608
|
+
function generateMediaType(model, registry) {
|
|
609
|
+
return { schema: getSchema(model, registry) };
|
|
467
610
|
}
|
|
468
|
-
function
|
|
469
|
-
return {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
};
|
|
611
|
+
function generateResponses(responses, registry) {
|
|
612
|
+
return Object.entries(responses).reduce((acc, [status, response]) => ({
|
|
613
|
+
...acc,
|
|
614
|
+
[status]: generateResponseObject(response, registry)
|
|
615
|
+
}), {});
|
|
473
616
|
}
|
|
474
|
-
function
|
|
617
|
+
function generateResponseObject(response, registry) {
|
|
475
618
|
return {
|
|
476
|
-
|
|
477
|
-
|
|
619
|
+
description: response.summary ?? "",
|
|
620
|
+
headers: response.kind !== "binary" && response.headers ? generateResponseHeaders(response.headers, registry) : void 0,
|
|
621
|
+
content: generateResponseContent(response, registry)
|
|
478
622
|
};
|
|
479
623
|
}
|
|
480
|
-
function
|
|
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) {
|
|
481
699
|
return {
|
|
482
|
-
|
|
483
|
-
|
|
700
|
+
type: "apiKey",
|
|
701
|
+
name: component.name,
|
|
702
|
+
in: "header",
|
|
703
|
+
description: component.description
|
|
484
704
|
};
|
|
485
705
|
}
|
|
486
|
-
function
|
|
706
|
+
function toOpenIdConnectScheme(component, deployment) {
|
|
487
707
|
return {
|
|
488
|
-
|
|
489
|
-
|
|
708
|
+
type: "openIdConnect",
|
|
709
|
+
openIdConnectUrl: deployment.issuer.endsWith("/") ? `${deployment.issuer}.well-known/openid-configuration` : `${deployment.issuer}/.well-known/openid-configuration`,
|
|
710
|
+
description: component.description
|
|
490
711
|
};
|
|
491
712
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
import org.jspecify.annotations.NullMarked;
|
|
501
|
-
|
|
502
|
-
@NullMarked
|
|
503
|
-
public record ${model.id}(
|
|
504
|
-
${Object.entries(model.properties).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
505
|
-
){
|
|
506
|
-
${Object.entries(model.properties).filter(([__dirname, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
|
|
507
|
-
}
|
|
508
|
-
`;
|
|
509
|
-
case "union": return `
|
|
510
|
-
package ${packageName};
|
|
511
|
-
|
|
512
|
-
import org.jspecify.annotations.NullMarked;
|
|
513
|
-
|
|
514
|
-
@NullMarked
|
|
515
|
-
public sealed interface ${model.id}{
|
|
516
|
-
${Object.entries(model.variants).map(([k, v]) => generateUnionVariantCode(packageName, model.id, k, v)).join("\n")}
|
|
517
|
-
}
|
|
518
|
-
`;
|
|
519
|
-
case "error": return `
|
|
520
|
-
package ${packageName};
|
|
521
|
-
|
|
522
|
-
import org.jspecify.annotations.NullMarked;
|
|
523
|
-
|
|
524
|
-
@NullMarked
|
|
525
|
-
public record ${model.id}(Context context){
|
|
526
|
-
${generateStaticPropertyCode(packageName, "code", literal({ value: model.code }))}
|
|
527
|
-
|
|
528
|
-
public ${model.id}(
|
|
529
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
530
|
-
){
|
|
531
|
-
this(new Context(${getPropertyNames(model.context)}));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
public record Context(
|
|
535
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
536
|
-
){
|
|
537
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
`;
|
|
541
|
-
case "enums": return `
|
|
542
|
-
|
|
543
|
-
package ${packageName};
|
|
544
|
-
|
|
545
|
-
import org.jspecify.annotations.NullMarked;
|
|
546
|
-
|
|
547
|
-
public enum ${model.id} {
|
|
548
|
-
|
|
549
|
-
${Object.entries(model.variants).map(([k, v]) => `${k}("${v}")`)};
|
|
550
|
-
|
|
551
|
-
private final String value;
|
|
552
|
-
|
|
553
|
-
public ${model.id}(String value){
|
|
554
|
-
this.value = value;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
558
|
-
public String getValue() {
|
|
559
|
-
return value;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
`;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function getPropertyNames(properties) {
|
|
566
|
-
if (properties == null) return "";
|
|
567
|
-
return Object.keys(properties).join(",");
|
|
568
|
-
}
|
|
569
|
-
function generateUnionVariantCode(packageName, unionId, variantName, variant) {
|
|
570
|
-
switch (variant.kind) {
|
|
571
|
-
case "literal": return `
|
|
572
|
-
record ${variantName}() implements ${unionId} {
|
|
573
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
574
|
-
${generateStaticPropertyCode(packageName, "value", variant)}
|
|
575
|
-
}
|
|
576
|
-
`;
|
|
577
|
-
default: return `
|
|
578
|
-
record ${variantName}(
|
|
579
|
-
@com.fasterxml.jackson.annotation.JsonCreator
|
|
580
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
581
|
-
${generateModelSignature$1(packageName, variant)} value
|
|
582
|
-
) implements ${unionId} {}
|
|
583
|
-
`;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
function generatePropertyCode(packageName, name, property) {
|
|
587
|
-
return `${generateModelSignature$1(packageName, property)} ${name}`;
|
|
588
|
-
}
|
|
589
|
-
function generateStaticPropertyCode(packageName, name, property) {
|
|
590
|
-
return `public ${generateModelSignature$1(packageName, property)} ${name} = ${generateStaticValue$1(property.value)}`;
|
|
591
|
-
}
|
|
592
|
-
function generateStaticValue$1(value) {
|
|
593
|
-
switch (typeof value) {
|
|
594
|
-
case "string": return `"${value}"`;
|
|
595
|
-
case "boolean": return String(value);
|
|
596
|
-
case "number": return Number.isInteger(value) ? String(value) : value + "d";
|
|
597
|
-
default: throw Error("Unsupported literal value");
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
function generateModelSignature$1(packageName, model) {
|
|
601
|
-
switch (model.kind) {
|
|
602
|
-
case "string": return "String";
|
|
603
|
-
case "int32": return "int";
|
|
604
|
-
case "int64": return "long";
|
|
605
|
-
case "float32": return "float";
|
|
606
|
-
case "float64": return "double";
|
|
607
|
-
case "boolean": return "boolean";
|
|
608
|
-
case "date": return "java.time.LocalDate";
|
|
609
|
-
case "datetime": return "java.time.LocalDateTime";
|
|
610
|
-
case "duration": return "java.time.Duration";
|
|
611
|
-
case "array": return `Array<${generateModelSignature$1(packageName, model.base)}>`;
|
|
612
|
-
case "set": return `Set<${generateModelSignature$1(packageName, model.base)}>`;
|
|
613
|
-
case "map": return `Map<String, ${generateModelSignature$1(packageName, model.base)}>`;
|
|
614
|
-
case "optional": switch (model.base.kind) {
|
|
615
|
-
case "int32": return "@Nullable Integer";
|
|
616
|
-
case "int64": return "@Nullable Long";
|
|
617
|
-
case "float32": return "@Nullable Float";
|
|
618
|
-
case "float64": return "@Nullable Double";
|
|
619
|
-
case "boolean": return "@Nullable Boolean";
|
|
620
|
-
case "optional": return generateModelSignature$1(packageName, model.base);
|
|
621
|
-
default: return `@Nullable ${generateModelSignature$1(packageName, model.base)}`;
|
|
622
|
-
}
|
|
623
|
-
case "literal": switch (typeof model.value) {
|
|
624
|
-
case "string": return "String";
|
|
625
|
-
case "boolean": return "boolean";
|
|
626
|
-
case "number": return Number.isInteger(model.value) ? "int" : "double";
|
|
627
|
-
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);
|
|
628
721
|
}
|
|
629
|
-
default: return packageName + "." + model.id;
|
|
630
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;
|
|
631
767
|
}
|
|
632
|
-
|
|
768
|
+
function matchesPath(pattern, path) {
|
|
633
769
|
try {
|
|
634
|
-
return
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
} catch (error) {
|
|
639
|
-
console.error(error);
|
|
640
|
-
return code;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
async function generateJavaCodes(options) {
|
|
644
|
-
const { package: packageName, srcDir, routes = [], models = [] } = options;
|
|
645
|
-
const codes = /* @__PURE__ */ new Map();
|
|
646
|
-
const namedModels = collectModelFromRoutes(routes);
|
|
647
|
-
for (const model of models) for (const [id, subModel] of collectModelDeep(model)) namedModels.set(id, subModel);
|
|
648
|
-
for (const [id, model] of namedModels) {
|
|
649
|
-
if (model.kind !== "union" && model.kind !== "record" && model.kind !== "enums" && model.kind !== "error") continue;
|
|
650
|
-
const path = resolve(srcDir, ...(packageName + "." + id).split("."), ".java");
|
|
651
|
-
const code = await formatJava(generateJavaClass({
|
|
652
|
-
package: packageName,
|
|
653
|
-
model
|
|
654
|
-
}));
|
|
655
|
-
codes.set(path, code);
|
|
656
|
-
}
|
|
657
|
-
return codes.entries().map(([path, code]) => ({
|
|
658
|
-
path,
|
|
659
|
-
code
|
|
660
|
-
})).toArray();
|
|
770
|
+
return new RegExp(pattern).test(path);
|
|
771
|
+
} catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
661
774
|
}
|
|
662
775
|
//#endregion
|
|
663
|
-
//#region src/
|
|
664
|
-
function
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
+
};
|
|
684
887
|
});
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
+
}))
|
|
690
902
|
});
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
structFields.forEach((f) => {
|
|
695
|
-
code += ` ${f},\n`;
|
|
903
|
+
else if (m.kind === "enums") map.set(m.id, {
|
|
904
|
+
kind: "enums",
|
|
905
|
+
variants: m.variants
|
|
696
906
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
code += `impl ${structName} {\n`;
|
|
701
|
-
code += ` pub const CODE: &'static str = "${model.id}";\n`;
|
|
702
|
-
constFields.forEach((f) => {
|
|
703
|
-
code += ` pub ${f};\n`;
|
|
907
|
+
else if (m.kind === "union") map.set(m.id, {
|
|
908
|
+
kind: "union",
|
|
909
|
+
unionVariants: m.variants
|
|
704
910
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const enumName = toPascalCase(model.id || "Unknown");
|
|
711
|
-
let code = `use serde::Serialize;\n\n`;
|
|
712
|
-
code += `#[derive(Serialize)]\n#[serde(tag = "type", content = "value")]\npub enum ${enumName} {\n`;
|
|
713
|
-
Object.entries(model.variants).forEach(([k, v]) => {
|
|
714
|
-
const variantName = toPascalCase(k);
|
|
715
|
-
if (v.kind === "literal") code += ` ${variantName},\n`;
|
|
716
|
-
else {
|
|
717
|
-
const variantType = generateModelSignature(v);
|
|
718
|
-
code += ` ${variantName}(${variantType}),\n`;
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
code += `}\n`;
|
|
722
|
-
return code;
|
|
723
|
-
}
|
|
724
|
-
function generateErrorCode(model) {
|
|
725
|
-
const structName = toPascalCase(model.id || "Unknown");
|
|
726
|
-
const contextFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generateStructField(k, v));
|
|
727
|
-
const constFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateConstField(k, v));
|
|
728
|
-
let code = `use serde::Serialize;\n\n`;
|
|
729
|
-
if (contextFields.length > 0 || constFields.length > 0) {
|
|
730
|
-
code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
|
|
731
|
-
code += ` pub code: &'static str,\n`;
|
|
732
|
-
if (contextFields.length > 0) code += ` pub context: ${structName}Context,\n`;
|
|
733
|
-
code += `}\n\n`;
|
|
734
|
-
if (contextFields.length > 0) {
|
|
735
|
-
code += `#[derive(Serialize)]\npub struct ${structName}Context {\n`;
|
|
736
|
-
contextFields.forEach((f) => {
|
|
737
|
-
code += ` ${f};\n`;
|
|
738
|
-
});
|
|
739
|
-
code += `}\n\n`;
|
|
740
|
-
}
|
|
741
|
-
code += `impl ${structName} {\n`;
|
|
742
|
-
code += ` pub const CODE: &'static str = "${model.code}";\n`;
|
|
743
|
-
constFields.forEach((f) => {
|
|
744
|
-
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
|
|
745
916
|
});
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
return
|
|
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;
|
|
768
939
|
}
|
|
769
|
-
function
|
|
770
|
-
|
|
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
|
+
}
|
|
771
951
|
}
|
|
772
|
-
|
|
773
|
-
|
|
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
|
+
};
|
|
774
970
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
1023
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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);
|
|
803
1046
|
}
|
|
804
|
-
default: return model.id ? toPascalCase(model.id) : "()";
|
|
805
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,`);
|
|
1197
|
+
}
|
|
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);
|
|
1221
|
+
}
|
|
1222
|
+
return map;
|
|
806
1223
|
}
|
|
807
|
-
function
|
|
808
|
-
return
|
|
1224
|
+
function toHonoPath(path) {
|
|
1225
|
+
return path.replace(/\{(\w+)\}/g, ":$1");
|
|
809
1226
|
}
|
|
810
|
-
function
|
|
811
|
-
|
|
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
|
+
}
|
|
812
1234
|
}
|
|
813
|
-
function
|
|
814
|
-
|
|
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
|
+
}
|
|
815
1262
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
model
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
return codes.entries().map(([path, code]) => ({
|
|
843
|
-
path,
|
|
844
|
-
code
|
|
845
|
-
})).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
|
+
}
|
|
846
1288
|
}
|
|
847
1289
|
//#endregion
|
|
848
|
-
//#region src/
|
|
849
|
-
function
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
}
|
|
874
1340
|
}
|
|
875
|
-
return deserializeFailed(model, value);
|
|
876
1341
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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);
|
|
882
1358
|
}
|
|
883
|
-
|
|
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);
|
|
884
1371
|
}
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (result.isSuccess) properties.set(key, result.value);
|
|
912
|
-
else return result;
|
|
913
|
-
}
|
|
914
|
-
return success(properties);
|
|
915
|
-
}
|
|
916
|
-
return deserializeFailed(model, value);
|
|
917
|
-
}
|
|
918
|
-
if (model.kind === "enums") {
|
|
919
|
-
if (typeof value === "string" && Object.values(model.variants).includes(value)) return success(value);
|
|
920
|
-
return deserializeFailed(model, value);
|
|
921
|
-
}
|
|
922
|
-
if (model.kind === "record") {
|
|
923
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
924
|
-
const properties = /* @__PURE__ */ new Map();
|
|
925
|
-
for (const key in model.properties) {
|
|
926
|
-
const result = deserializeInner(model.properties[key], value[key]);
|
|
927
|
-
if (result.isSuccess) properties.set(key, result.value);
|
|
928
|
-
else return deserializeFailed(model, value);
|
|
929
|
-
}
|
|
930
|
-
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;
|
|
931
1398
|
}
|
|
932
|
-
return
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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} }`);
|
|
941
1414
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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}"`);
|
|
954
1453
|
}
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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(", ")}])`;
|
|
963
1517
|
}
|
|
964
|
-
return
|
|
965
|
-
code: model.code,
|
|
966
|
-
context: Object.fromEntries(properties)
|
|
967
|
-
});
|
|
1518
|
+
default: return "z.unknown()";
|
|
968
1519
|
}
|
|
969
|
-
return deserializeFailed(model, value);
|
|
970
1520
|
}
|
|
971
|
-
function
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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;
|
|
978
1531
|
}
|
|
979
|
-
|
|
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
|
+
}
|
|
980
1538
|
}
|
|
981
|
-
function
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
for (const key in model.context) properties.push(`"${key}": ${serialize(model.context[key], value[key])}`);
|
|
993
|
-
return `{"code":"${model.code}",context:{${properties}}}`;
|
|
994
|
-
}
|
|
995
|
-
if (model.kind === "record") {
|
|
996
|
-
const properties = [];
|
|
997
|
-
for (const key in model.properties) properties.push(`"${key}": ${serialize(model.properties[key], value[key])}`);
|
|
998
|
-
return `{${properties}}`;
|
|
999
|
-
}
|
|
1000
|
-
if (model.kind === "union") {
|
|
1001
|
-
const variantKeys = Object.keys(model.variants);
|
|
1002
|
-
for (const key of variantKeys) if (key in value) return `{"${key}":${serialize(model.variants[key], value[key])}}`;
|
|
1003
|
-
}
|
|
1004
|
-
if (model.kind === "tagged-union") return JSON.stringify(value);
|
|
1005
|
-
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
|
+
}
|
|
1006
1550
|
}
|
|
1007
1551
|
//#endregion
|
|
1008
|
-
export { array, binary, boolean,
|
|
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 };
|
|
1009
1553
|
|
|
1010
1554
|
//# sourceMappingURL=index.mjs.map
|