@huanglangjian/specs 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1728 -482
- package/dist/index.mjs +1382 -841
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -11
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,904 +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":
|
|
401
|
-
case "server-sent-event-content": return { content: { [content.type]: generateMeidaTypeObject(content) } };
|
|
402
|
-
default: return {
|
|
403
|
-
description: content.description,
|
|
404
|
-
content: { [content.type]: generateMeidaTypeObject(content) }
|
|
405
|
-
};
|
|
548
|
+
function collectNestedModels(model) {
|
|
549
|
+
switch (model.kind) {
|
|
550
|
+
case "array":
|
|
551
|
+
case "set":
|
|
552
|
+
case "map": return collectModelDeep(model.base);
|
|
553
|
+
case "record": return Object.values(model.properties).flatMap(collectModelDeep);
|
|
554
|
+
case "union": return Object.values(model.variants).flatMap(collectModelDeep);
|
|
555
|
+
case "taggedUnion": return Object.values(model.variants).flatMap(collectModelDeep);
|
|
556
|
+
default: return [];
|
|
406
557
|
}
|
|
407
558
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
itemSchema: getSchemaReference(content.model)
|
|
418
|
-
};
|
|
419
|
-
case "binary-stream-content": return {
|
|
420
|
-
schema: {
|
|
421
|
-
type: "string",
|
|
422
|
-
format: "binary"
|
|
423
|
-
},
|
|
424
|
-
itemSchema: {
|
|
425
|
-
type: "string",
|
|
426
|
-
format: "binary"
|
|
559
|
+
function generatePaths(flatRoutes, registry) {
|
|
560
|
+
return flatRoutes.reduce((paths, { route, group, fullPath }) => {
|
|
561
|
+
const existing = paths[fullPath] ?? {};
|
|
562
|
+
const method = route.method.toLowerCase();
|
|
563
|
+
return {
|
|
564
|
+
...paths,
|
|
565
|
+
[fullPath]: {
|
|
566
|
+
...existing,
|
|
567
|
+
[method]: generateOperation(route, group, registry)
|
|
427
568
|
}
|
|
428
569
|
};
|
|
429
|
-
|
|
430
|
-
schema: {
|
|
431
|
-
type: "string",
|
|
432
|
-
format: "binary"
|
|
433
|
-
},
|
|
434
|
-
itemSchema: content.model ? getSchemaReference(content.model) : { type: "string" }
|
|
435
|
-
};
|
|
436
|
-
case "form-content": return { schema: getSchemaReference(content.model) };
|
|
437
|
-
}
|
|
570
|
+
}, {});
|
|
438
571
|
}
|
|
439
|
-
function
|
|
440
|
-
const
|
|
441
|
-
if (response.headers) for (const [name, model] of Object.entries(response.headers)) headerObjects.set(name, {
|
|
442
|
-
schema: getSchemaReference(model),
|
|
443
|
-
required: model.kind !== "optional"
|
|
444
|
-
});
|
|
572
|
+
function generateOperation(route, group, registry) {
|
|
573
|
+
const tags = route.tags?.includes(group) ? route.tags : [...route.tags ?? [], group];
|
|
445
574
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
575
|
+
summary: route.summary,
|
|
576
|
+
description: route.description,
|
|
577
|
+
tags,
|
|
578
|
+
parameters: generateParameters(route, registry),
|
|
579
|
+
requestBody: generateRequestBody(route, registry),
|
|
580
|
+
responses: generateResponses(route.responses, registry)
|
|
449
581
|
};
|
|
450
582
|
}
|
|
451
|
-
function route
|
|
583
|
+
function generateParameters(route, registry) {
|
|
584
|
+
const pathParams = Object.entries(route.variables ?? {}).map(([name, model]) => generateParameter(name, model, "path", true, registry));
|
|
585
|
+
const queries = route.queries;
|
|
586
|
+
const headers = route.headers;
|
|
587
|
+
const queryParams = queries ? Object.entries(queries.properties).map(([name, model]) => generateParameter(name, model, "query", queries.required.includes(name), registry)) : [];
|
|
588
|
+
const headerParams = headers ? Object.entries(headers.properties).map(([name, model]) => generateParameter(name, model, "header", headers.required.includes(name), registry)) : [];
|
|
589
|
+
const all = [
|
|
590
|
+
...pathParams,
|
|
591
|
+
...queryParams,
|
|
592
|
+
...headerParams
|
|
593
|
+
];
|
|
594
|
+
return all.length > 0 ? all : void 0;
|
|
595
|
+
}
|
|
596
|
+
function generateParameter(name, model, location, required, registry) {
|
|
452
597
|
return {
|
|
453
|
-
|
|
454
|
-
|
|
598
|
+
name,
|
|
599
|
+
in: location,
|
|
600
|
+
required,
|
|
601
|
+
schema: getSchema(model, registry)
|
|
455
602
|
};
|
|
456
603
|
}
|
|
457
|
-
function
|
|
458
|
-
return
|
|
459
|
-
|
|
460
|
-
...options
|
|
461
|
-
};
|
|
604
|
+
function generateRequestBody(route, registry) {
|
|
605
|
+
if (route.body == null || route.body.kind === "null") return void 0;
|
|
606
|
+
return { content: { [route.contentType ?? "application/json"]: generateMediaType(route.body, registry) } };
|
|
462
607
|
}
|
|
463
|
-
function
|
|
464
|
-
return {
|
|
465
|
-
kind: "json-content",
|
|
466
|
-
...options
|
|
467
|
-
};
|
|
608
|
+
function generateMediaType(model, registry) {
|
|
609
|
+
return { schema: getSchema(model, registry) };
|
|
468
610
|
}
|
|
469
|
-
function
|
|
470
|
-
return {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
};
|
|
611
|
+
function generateResponses(responses, registry) {
|
|
612
|
+
return Object.entries(responses).reduce((acc, [status, response]) => ({
|
|
613
|
+
...acc,
|
|
614
|
+
[status]: generateResponseObject(response, registry)
|
|
615
|
+
}), {});
|
|
474
616
|
}
|
|
475
|
-
function
|
|
617
|
+
function generateResponseObject(response, registry) {
|
|
476
618
|
return {
|
|
477
|
-
|
|
478
|
-
|
|
619
|
+
description: response.summary ?? "",
|
|
620
|
+
headers: response.kind !== "binary" && response.headers ? generateResponseHeaders(response.headers, registry) : void 0,
|
|
621
|
+
content: generateResponseContent(response, registry)
|
|
479
622
|
};
|
|
480
623
|
}
|
|
481
|
-
function
|
|
624
|
+
function generateResponseHeaders(headers, registry) {
|
|
625
|
+
return Object.entries(headers.properties).reduce((acc, [name, model]) => ({
|
|
626
|
+
...acc,
|
|
627
|
+
[name]: { schema: getSchema(model, registry) }
|
|
628
|
+
}), {});
|
|
629
|
+
}
|
|
630
|
+
function generateResponseContent(response, registry) {
|
|
631
|
+
switch (response.kind) {
|
|
632
|
+
case "json-response": {
|
|
633
|
+
if (response.body == null) return void 0;
|
|
634
|
+
const contentType = response.contentType ?? "application/json";
|
|
635
|
+
const mediaType = { schema: getSchema(response.body, registry) };
|
|
636
|
+
return { [contentType]: mediaType };
|
|
637
|
+
}
|
|
638
|
+
case "stream-response": {
|
|
639
|
+
if (response.body == null) return void 0;
|
|
640
|
+
const contentType = response.contentType ?? "application/x-ndjson";
|
|
641
|
+
const mediaType = {
|
|
642
|
+
schema: {
|
|
643
|
+
type: "string",
|
|
644
|
+
format: "binary"
|
|
645
|
+
},
|
|
646
|
+
itemSchema: getSchema(response.body, registry)
|
|
647
|
+
};
|
|
648
|
+
return { [contentType]: mediaType };
|
|
649
|
+
}
|
|
650
|
+
case "sse-response": {
|
|
651
|
+
const contentType = response.contentType ?? "text/event-stream";
|
|
652
|
+
const mediaType = {
|
|
653
|
+
schema: {
|
|
654
|
+
type: "string",
|
|
655
|
+
format: "binary"
|
|
656
|
+
},
|
|
657
|
+
itemSchema: response.body ? getSchema(response.body, registry) : { type: "string" }
|
|
658
|
+
};
|
|
659
|
+
return { [contentType]: mediaType };
|
|
660
|
+
}
|
|
661
|
+
case "binary": return { [response.contentType ?? "application/octet-stream"]: { schema: {
|
|
662
|
+
type: "string",
|
|
663
|
+
format: "binary"
|
|
664
|
+
} } };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function getSchema(model, registry) {
|
|
668
|
+
const ref = registry.getRef(model);
|
|
669
|
+
return ref ? { $ref: ref } : generateJsonSchema({
|
|
670
|
+
model,
|
|
671
|
+
registry
|
|
672
|
+
}).jsonSchema;
|
|
673
|
+
}
|
|
674
|
+
function buildSecuritySchemes(policy, deployments) {
|
|
675
|
+
const components = collectSecurityComponents(policy);
|
|
676
|
+
if (components.length === 0) return { schemes: void 0 };
|
|
677
|
+
const schemes = {};
|
|
678
|
+
for (const component of components) switch (component.kind) {
|
|
679
|
+
case "apikey":
|
|
680
|
+
schemes[component.id] = toApiKeyScheme(component);
|
|
681
|
+
break;
|
|
682
|
+
case "openIdConnect": {
|
|
683
|
+
const deployment = findDeployment(component, deployments);
|
|
684
|
+
if (!deployment || deployment.kind !== "openIdConnectDeployment") {
|
|
685
|
+
console.warn(`[generateOpenapi] Security scheme "${component.id}" (openIdConnect) has no matching deployment, skipping`);
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
schemes[component.id] = toOpenIdConnectScheme(component, deployment);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return { schemes: Object.keys(schemes).length > 0 ? schemes : void 0 };
|
|
693
|
+
}
|
|
694
|
+
function findDeployment(component, deployments) {
|
|
695
|
+
if (!deployments) return void 0;
|
|
696
|
+
for (const dep of Object.values(deployments)) if (dep.component.id === component.id) return dep;
|
|
697
|
+
}
|
|
698
|
+
function toApiKeyScheme(component) {
|
|
482
699
|
return {
|
|
483
|
-
|
|
484
|
-
|
|
700
|
+
type: "apiKey",
|
|
701
|
+
name: component.name,
|
|
702
|
+
in: "header",
|
|
703
|
+
description: component.description
|
|
485
704
|
};
|
|
486
705
|
}
|
|
487
|
-
function
|
|
488
|
-
const { kind = "server-sent-event-content", type = "text/event-stream", model } = options ?? {};
|
|
706
|
+
function toOpenIdConnectScheme(component, deployment) {
|
|
489
707
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
708
|
+
type: "openIdConnect",
|
|
709
|
+
openIdConnectUrl: deployment.issuer.endsWith("/") ? `${deployment.issuer}.well-known/openid-configuration` : `${deployment.issuer}/.well-known/openid-configuration`,
|
|
710
|
+
description: component.description
|
|
493
711
|
};
|
|
494
712
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
import org.jspecify.annotations.NullMarked;
|
|
504
|
-
|
|
505
|
-
@NullMarked
|
|
506
|
-
public record ${model.id}(
|
|
507
|
-
${Object.entries(model.properties).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
508
|
-
){
|
|
509
|
-
${Object.entries(model.properties).filter(([__dirname, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
|
|
510
|
-
}
|
|
511
|
-
`;
|
|
512
|
-
case "union": return `
|
|
513
|
-
package ${packageName};
|
|
514
|
-
|
|
515
|
-
import org.jspecify.annotations.NullMarked;
|
|
516
|
-
|
|
517
|
-
@NullMarked
|
|
518
|
-
public sealed interface ${model.id}{
|
|
519
|
-
${Object.entries(model.variants).map(([k, v]) => generateUnionVariantCode(packageName, model.id, k, v)).join("\n")}
|
|
520
|
-
}
|
|
521
|
-
`;
|
|
522
|
-
case "error": return `
|
|
523
|
-
package ${packageName};
|
|
524
|
-
|
|
525
|
-
import org.jspecify.annotations.NullMarked;
|
|
526
|
-
|
|
527
|
-
@NullMarked
|
|
528
|
-
public record ${model.id}(Context context){
|
|
529
|
-
${generateStaticPropertyCode(packageName, "code", literal({ value: model.code }))}
|
|
530
|
-
|
|
531
|
-
public ${model.id}(
|
|
532
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
533
|
-
){
|
|
534
|
-
this(new Context(${getPropertyNames(model.context)}));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
public record Context(
|
|
538
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generatePropertyCode(packageName, k, v))}
|
|
539
|
-
){
|
|
540
|
-
${Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateStaticPropertyCode(packageName, k, v)).join(";")}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
`;
|
|
544
|
-
case "enums": return `
|
|
545
|
-
|
|
546
|
-
package ${packageName};
|
|
547
|
-
|
|
548
|
-
import org.jspecify.annotations.NullMarked;
|
|
549
|
-
|
|
550
|
-
public enum ${model.id} {
|
|
551
|
-
|
|
552
|
-
${Object.entries(model.variants).map(([k, v]) => `${k}("${v}")`)};
|
|
553
|
-
|
|
554
|
-
private final String value;
|
|
555
|
-
|
|
556
|
-
public ${model.id}(String value){
|
|
557
|
-
this.value = value;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
561
|
-
public String getValue() {
|
|
562
|
-
return value;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
`;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
function getPropertyNames(properties) {
|
|
569
|
-
if (properties == null) return "";
|
|
570
|
-
return Object.keys(properties).join(",");
|
|
571
|
-
}
|
|
572
|
-
function generateUnionVariantCode(packageName, unionId, variantName, variant) {
|
|
573
|
-
switch (variant.kind) {
|
|
574
|
-
case "literal": return `
|
|
575
|
-
record ${variantName}() implements ${unionId} {
|
|
576
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
577
|
-
${generateStaticPropertyCode(packageName, "value", variant)}
|
|
578
|
-
}
|
|
579
|
-
`;
|
|
580
|
-
default: return `
|
|
581
|
-
record ${variantName}(
|
|
582
|
-
@com.fasterxml.jackson.annotation.JsonCreator
|
|
583
|
-
@com.fasterxml.jackson.annotation.JsonValue
|
|
584
|
-
${generateModelSignature$1(packageName, variant)} value
|
|
585
|
-
) implements ${unionId} {}
|
|
586
|
-
`;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function generatePropertyCode(packageName, name, property) {
|
|
590
|
-
return `${generateModelSignature$1(packageName, property)} ${name}`;
|
|
591
|
-
}
|
|
592
|
-
function generateStaticPropertyCode(packageName, name, property) {
|
|
593
|
-
return `public ${generateModelSignature$1(packageName, property)} ${name} = ${generateStaticValue$1(property.value)}`;
|
|
594
|
-
}
|
|
595
|
-
function generateStaticValue$1(value) {
|
|
596
|
-
switch (typeof value) {
|
|
597
|
-
case "string": return `"${value}"`;
|
|
598
|
-
case "boolean": return String(value);
|
|
599
|
-
case "number": return Number.isInteger(value) ? String(value) : value + "d";
|
|
600
|
-
default: throw Error("Unsupported literal value");
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
function generateModelSignature$1(packageName, model) {
|
|
604
|
-
switch (model.kind) {
|
|
605
|
-
case "string": return "String";
|
|
606
|
-
case "int32": return "int";
|
|
607
|
-
case "int64": return "long";
|
|
608
|
-
case "float32": return "float";
|
|
609
|
-
case "float64": return "double";
|
|
610
|
-
case "boolean": return "boolean";
|
|
611
|
-
case "date": return "java.time.LocalDate";
|
|
612
|
-
case "datetime": return "java.time.LocalDateTime";
|
|
613
|
-
case "duration": return "java.time.Duration";
|
|
614
|
-
case "array": return `Array<${generateModelSignature$1(packageName, model.base)}>`;
|
|
615
|
-
case "set": return `Set<${generateModelSignature$1(packageName, model.base)}>`;
|
|
616
|
-
case "map": return `Map<String, ${generateModelSignature$1(packageName, model.base)}>`;
|
|
617
|
-
case "optional": switch (model.base.kind) {
|
|
618
|
-
case "int32": return "@Nullable Integer";
|
|
619
|
-
case "int64": return "@Nullable Long";
|
|
620
|
-
case "float32": return "@Nullable Float";
|
|
621
|
-
case "float64": return "@Nullable Double";
|
|
622
|
-
case "boolean": return "@Nullable Boolean";
|
|
623
|
-
case "optional": return generateModelSignature$1(packageName, model.base);
|
|
624
|
-
default: return `@Nullable ${generateModelSignature$1(packageName, model.base)}`;
|
|
625
|
-
}
|
|
626
|
-
case "literal": switch (typeof model.value) {
|
|
627
|
-
case "string": return "String";
|
|
628
|
-
case "boolean": return "boolean";
|
|
629
|
-
case "number": return Number.isInteger(model.value) ? "int" : "double";
|
|
630
|
-
default: throw Error("Unsupported literal value");
|
|
713
|
+
function collectSecurityComponents(policy) {
|
|
714
|
+
const seen = /* @__PURE__ */ new Set();
|
|
715
|
+
const result = [];
|
|
716
|
+
for (const pathItem of Object.values(policy.paths)) {
|
|
717
|
+
if (!pathItem.pipeline) continue;
|
|
718
|
+
for (const apply of pathItem.pipeline) if (!seen.has(apply.component.id)) {
|
|
719
|
+
seen.add(apply.component.id);
|
|
720
|
+
result.push(apply.component);
|
|
631
721
|
}
|
|
632
|
-
default: return packageName + "." + model.id;
|
|
633
722
|
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
function applySecurityToPaths(paths, policy) {
|
|
726
|
+
if (!paths) return void 0;
|
|
727
|
+
const operationMethods = [
|
|
728
|
+
"get",
|
|
729
|
+
"put",
|
|
730
|
+
"post",
|
|
731
|
+
"delete",
|
|
732
|
+
"options",
|
|
733
|
+
"head",
|
|
734
|
+
"patch",
|
|
735
|
+
"trace"
|
|
736
|
+
];
|
|
737
|
+
return Object.entries(paths).reduce((acc, [pathKey, pathItem]) => {
|
|
738
|
+
const securedItem = operationMethods.reduce((methodAcc, method) => {
|
|
739
|
+
const op = pathItem[method];
|
|
740
|
+
if (!op) return methodAcc;
|
|
741
|
+
const requirements = resolveSecurityRequirements(policy, pathKey, method);
|
|
742
|
+
if (requirements.length === 0) return methodAcc;
|
|
743
|
+
return {
|
|
744
|
+
...methodAcc,
|
|
745
|
+
[method]: {
|
|
746
|
+
...op,
|
|
747
|
+
security: requirements
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}, pathItem);
|
|
751
|
+
return {
|
|
752
|
+
...acc,
|
|
753
|
+
[pathKey]: securedItem
|
|
754
|
+
};
|
|
755
|
+
}, {});
|
|
756
|
+
}
|
|
757
|
+
function resolveSecurityRequirements(policy, pathKey, method) {
|
|
758
|
+
const requirements = [];
|
|
759
|
+
for (const [pattern, pathItem] of Object.entries(policy.paths)) {
|
|
760
|
+
if (!matchesPath(pattern, pathKey)) continue;
|
|
761
|
+
const methods = pathItem.methods;
|
|
762
|
+
if (methods && methods.length > 0 && !methods.includes(method.toUpperCase())) continue;
|
|
763
|
+
if (!pathItem.pipeline) continue;
|
|
764
|
+
for (const apply of pathItem.pipeline) requirements.push({ [apply.component.id]: apply.scopes });
|
|
765
|
+
}
|
|
766
|
+
return requirements;
|
|
634
767
|
}
|
|
635
|
-
|
|
768
|
+
function matchesPath(pattern, path) {
|
|
636
769
|
try {
|
|
637
|
-
return
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
} catch (error) {
|
|
642
|
-
console.error(error);
|
|
643
|
-
return code;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
async function generateJavaCodes(options) {
|
|
647
|
-
const { package: packageName, srcDir, routes = [], models = [] } = options;
|
|
648
|
-
const codes = /* @__PURE__ */ new Map();
|
|
649
|
-
const namedModels = collectModelFromRoutes(routes);
|
|
650
|
-
for (const model of models) for (const [id, subModel] of collectModelDeep(model)) namedModels.set(id, subModel);
|
|
651
|
-
for (const [id, model] of namedModels) {
|
|
652
|
-
if (model.kind !== "union" && model.kind !== "record" && model.kind !== "enums" && model.kind !== "error") continue;
|
|
653
|
-
const path = resolve(srcDir, ...(packageName + "." + id).split("."), ".java");
|
|
654
|
-
const code = await formatJava(generateJavaClass({
|
|
655
|
-
package: packageName,
|
|
656
|
-
model
|
|
657
|
-
}));
|
|
658
|
-
codes.set(path, code);
|
|
659
|
-
}
|
|
660
|
-
return codes.entries().map(([path, code]) => ({
|
|
661
|
-
path,
|
|
662
|
-
code
|
|
663
|
-
})).toArray();
|
|
770
|
+
return new RegExp(pattern).test(path);
|
|
771
|
+
} catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
664
774
|
}
|
|
665
775
|
//#endregion
|
|
666
|
-
//#region src/
|
|
667
|
-
function
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
776
|
+
//#region src/codegen/collect.ts
|
|
777
|
+
function collectNamedModels(models, options) {
|
|
778
|
+
const identifier = options?.identifier ?? pascalCase;
|
|
779
|
+
const namespace = options?.namespace;
|
|
780
|
+
return models.reduce((acc, model) => {
|
|
781
|
+
if (model.kind === "record") return [...acc, toRecordDescriptor(model, identifier, namespace)];
|
|
782
|
+
if (model.kind === "enums") return [...acc, toEnumsDescriptor(model, identifier, namespace)];
|
|
783
|
+
if (model.kind === "union") return [...acc, {
|
|
784
|
+
kind: "union",
|
|
785
|
+
originalId: model.id,
|
|
786
|
+
identifier: identifier(model.id),
|
|
787
|
+
namespace,
|
|
788
|
+
title: model.title,
|
|
789
|
+
description: model.description,
|
|
790
|
+
deprecated: model.deprecated,
|
|
791
|
+
examples: model.examples,
|
|
792
|
+
variants: model.variants
|
|
793
|
+
}];
|
|
794
|
+
if (model.kind === "taggedUnion") return [...acc, {
|
|
795
|
+
kind: "taggedUnion",
|
|
796
|
+
originalId: model.id,
|
|
797
|
+
identifier: identifier(model.id),
|
|
798
|
+
namespace,
|
|
799
|
+
title: model.title,
|
|
800
|
+
description: model.description,
|
|
801
|
+
deprecated: model.deprecated,
|
|
802
|
+
examples: model.examples,
|
|
803
|
+
variants: model.variants,
|
|
804
|
+
variantKey: model.variantKey,
|
|
805
|
+
payloadKey: model.payloadKey
|
|
806
|
+
}];
|
|
807
|
+
return acc;
|
|
808
|
+
}, []);
|
|
809
|
+
}
|
|
810
|
+
function toRecordDescriptor(model, identifier, namespace) {
|
|
811
|
+
return {
|
|
812
|
+
kind: "record",
|
|
813
|
+
originalId: model.id,
|
|
814
|
+
identifier: identifier(model.id),
|
|
815
|
+
namespace,
|
|
816
|
+
title: model.title,
|
|
817
|
+
description: model.description,
|
|
818
|
+
deprecated: "deprecated" in model ? model.deprecated : void 0,
|
|
819
|
+
examples: model.examples,
|
|
820
|
+
fields: Object.entries(model.properties).map(([name, propModel]) => ({
|
|
821
|
+
name,
|
|
822
|
+
model: propModel,
|
|
823
|
+
required: model.required.includes(name),
|
|
824
|
+
title: propModel.title,
|
|
825
|
+
description: propModel.description,
|
|
826
|
+
deprecated: propModel.deprecated
|
|
827
|
+
}))
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function toEnumsDescriptor(model, identifier, namespace) {
|
|
831
|
+
return {
|
|
832
|
+
kind: "enums",
|
|
833
|
+
originalId: model.id,
|
|
834
|
+
identifier: identifier(model.id),
|
|
835
|
+
namespace,
|
|
836
|
+
title: model.title,
|
|
837
|
+
description: model.description,
|
|
838
|
+
values: model.variants
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function joinPath(basePath, routePath) {
|
|
842
|
+
if (!basePath) return routePath;
|
|
843
|
+
return `${basePath.endsWith("/") ? basePath.slice(0, -1) : basePath}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
|
|
844
|
+
}
|
|
845
|
+
function collectOperations(routers) {
|
|
846
|
+
return routers.flatMap((routerModel) => {
|
|
847
|
+
const basePath = routerModel.basePath ?? "";
|
|
848
|
+
return Object.entries(routerModel.routes).map(([id, route]) => {
|
|
849
|
+
const queries = {};
|
|
850
|
+
if (route.queries) for (const [name, propModel] of Object.entries(route.queries.properties)) queries[name] = {
|
|
851
|
+
model: propModel,
|
|
852
|
+
name,
|
|
853
|
+
required: route.queries.required.includes(name)
|
|
854
|
+
};
|
|
855
|
+
const headers = {};
|
|
856
|
+
if (route.headers) for (const [name, propModel] of Object.entries(route.headers.properties)) headers[name] = {
|
|
857
|
+
model: propModel,
|
|
858
|
+
name,
|
|
859
|
+
required: route.headers.required.includes(name)
|
|
860
|
+
};
|
|
861
|
+
const pathVariables = {};
|
|
862
|
+
if (route.variables) for (const [name, model] of Object.entries(route.variables)) pathVariables[name] = {
|
|
863
|
+
model,
|
|
864
|
+
name
|
|
865
|
+
};
|
|
866
|
+
const responses = {};
|
|
867
|
+
const responseKinds = {};
|
|
868
|
+
for (const [status, resp] of Object.entries(route.responses)) {
|
|
869
|
+
responses[Number(status)] = "body" in resp && resp.body != null ? resp.body : null;
|
|
870
|
+
responseKinds[Number(status)] = resp.kind;
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
id,
|
|
874
|
+
group: routerModel.name,
|
|
875
|
+
method: route.method,
|
|
876
|
+
path: joinPath(basePath, route.path),
|
|
877
|
+
summary: route.summary,
|
|
878
|
+
description: route.description,
|
|
879
|
+
tags: route.tags,
|
|
880
|
+
requestModel: route.body ?? null,
|
|
881
|
+
responses,
|
|
882
|
+
responseKinds,
|
|
883
|
+
pathVariables,
|
|
884
|
+
queries,
|
|
885
|
+
headers
|
|
886
|
+
};
|
|
687
887
|
});
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
function collectSchemaMap(ops) {
|
|
891
|
+
const all = collectAll(ops);
|
|
892
|
+
const map = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const m of all) {
|
|
894
|
+
if ("id" in m && map.has(m.id)) continue;
|
|
895
|
+
if (m.kind === "record") map.set(m.id, {
|
|
896
|
+
kind: "record",
|
|
897
|
+
fields: Object.entries(m.properties).map(([name, p]) => ({
|
|
898
|
+
name,
|
|
899
|
+
model: p,
|
|
900
|
+
required: m.required.includes(name)
|
|
901
|
+
}))
|
|
693
902
|
});
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
structFields.forEach((f) => {
|
|
698
|
-
code += ` ${f},\n`;
|
|
903
|
+
else if (m.kind === "enums") map.set(m.id, {
|
|
904
|
+
kind: "enums",
|
|
905
|
+
variants: m.variants
|
|
699
906
|
});
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
code += `impl ${structName} {\n`;
|
|
704
|
-
code += ` pub const CODE: &'static str = "${model.id}";\n`;
|
|
705
|
-
constFields.forEach((f) => {
|
|
706
|
-
code += ` pub ${f};\n`;
|
|
907
|
+
else if (m.kind === "union") map.set(m.id, {
|
|
908
|
+
kind: "union",
|
|
909
|
+
unionVariants: m.variants
|
|
707
910
|
});
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const enumName = toPascalCase(model.id || "Unknown");
|
|
714
|
-
let code = `use serde::Serialize;\n\n`;
|
|
715
|
-
code += `#[derive(Serialize)]\n#[serde(tag = "type", content = "value")]\npub enum ${enumName} {\n`;
|
|
716
|
-
Object.entries(model.variants).forEach(([k, v]) => {
|
|
717
|
-
const variantName = toPascalCase(k);
|
|
718
|
-
if (v.kind === "literal") code += ` ${variantName},\n`;
|
|
719
|
-
else {
|
|
720
|
-
const variantType = generateModelSignature(v);
|
|
721
|
-
code += ` ${variantName}(${variantType}),\n`;
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
code += `}\n`;
|
|
725
|
-
return code;
|
|
726
|
-
}
|
|
727
|
-
function generateErrorCode(model) {
|
|
728
|
-
const structName = toPascalCase(model.id || "Unknown");
|
|
729
|
-
const contextFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind !== "literal").map(([k, v]) => generateStructField(k, v));
|
|
730
|
-
const constFields = Object.entries(model.context ?? {}).filter(([_, v]) => v.kind === "literal").map(([k, v]) => generateConstField(k, v));
|
|
731
|
-
let code = `use serde::Serialize;\n\n`;
|
|
732
|
-
if (contextFields.length > 0 || constFields.length > 0) {
|
|
733
|
-
code += `#[derive(Serialize)]\npub struct ${structName} {\n`;
|
|
734
|
-
code += ` pub code: &'static str,\n`;
|
|
735
|
-
if (contextFields.length > 0) code += ` pub context: ${structName}Context,\n`;
|
|
736
|
-
code += `}\n\n`;
|
|
737
|
-
if (contextFields.length > 0) {
|
|
738
|
-
code += `#[derive(Serialize)]\npub struct ${structName}Context {\n`;
|
|
739
|
-
contextFields.forEach((f) => {
|
|
740
|
-
code += ` ${f};\n`;
|
|
741
|
-
});
|
|
742
|
-
code += `}\n\n`;
|
|
743
|
-
}
|
|
744
|
-
code += `impl ${structName} {\n`;
|
|
745
|
-
code += ` pub const CODE: &'static str = "${model.code}";\n`;
|
|
746
|
-
constFields.forEach((f) => {
|
|
747
|
-
code += ` pub ${f};\n`;
|
|
911
|
+
else if (m.kind === "taggedUnion") map.set(m.id, {
|
|
912
|
+
kind: "taggedUnion",
|
|
913
|
+
unionVariants: m.variants,
|
|
914
|
+
variantKey: m.variantKey,
|
|
915
|
+
payloadKey: m.payloadKey
|
|
748
916
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
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;
|
|
771
939
|
}
|
|
772
|
-
function
|
|
773
|
-
|
|
940
|
+
function resolveNamedRoot(m) {
|
|
941
|
+
switch (m.kind) {
|
|
942
|
+
case "record":
|
|
943
|
+
case "enums":
|
|
944
|
+
case "union":
|
|
945
|
+
case "taggedUnion": return m;
|
|
946
|
+
case "array":
|
|
947
|
+
case "set":
|
|
948
|
+
case "map": return resolveNamedRoot(m.base);
|
|
949
|
+
default: return null;
|
|
950
|
+
}
|
|
774
951
|
}
|
|
775
|
-
|
|
776
|
-
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/codegen/json-schema.ts
|
|
954
|
+
function mergeJsonSchemas(schemas) {
|
|
955
|
+
const entries = Object.entries(schemas);
|
|
956
|
+
const registry = entries.reduce((reg, [id, model]) => reg.add(id, model), createJsonSchemaRegistry());
|
|
957
|
+
return {
|
|
958
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
959
|
+
$defs: entries.reduce((acc, [id, model]) => {
|
|
960
|
+
const { jsonSchema } = generateJsonSchema({
|
|
961
|
+
model,
|
|
962
|
+
registry
|
|
963
|
+
});
|
|
964
|
+
return typeof jsonSchema === "object" && jsonSchema != null ? {
|
|
965
|
+
...acc,
|
|
966
|
+
[id]: jsonSchema
|
|
967
|
+
} : acc;
|
|
968
|
+
}, {})
|
|
969
|
+
};
|
|
777
970
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/codegen/hono-server.ts
|
|
973
|
+
function generateHonoServer(options) {
|
|
974
|
+
const { routers, identifier = pascalCase, namespace } = options;
|
|
975
|
+
const operations = collectOperations(routers);
|
|
976
|
+
const schemaMap = collectSchemaMap(operations);
|
|
977
|
+
const files = {};
|
|
978
|
+
files["models.ts"] = generateModels$1(schemaMap, identifier, namespace);
|
|
979
|
+
for (const operation of operations) files[`${camelCase(operation.id)}.ts`] = generateOpFile(operation, schemaMap, identifier, namespace);
|
|
980
|
+
files["index.ts"] = generateIndex(operations, identifier);
|
|
981
|
+
return files;
|
|
982
|
+
}
|
|
983
|
+
function generateModels$1(schemaMap, identifier, namespace) {
|
|
984
|
+
const lines = [];
|
|
985
|
+
lines.push(`import { z } from "zod"`);
|
|
986
|
+
lines.push("");
|
|
987
|
+
for (const [id, schemaInfo] of schemaMap) {
|
|
988
|
+
const schemaName = camelCase(id) + "Schema";
|
|
989
|
+
const tsName = identifier(id);
|
|
990
|
+
switch (schemaInfo.kind) {
|
|
991
|
+
case "record":
|
|
992
|
+
lines.push(`export const ${schemaName} = z.object({`);
|
|
993
|
+
for (const field of schemaInfo.fields) lines.push(` ${field.name}: ${toZod$1(field.model, schemaMap)}${field.required ? "" : ".optional()"},`);
|
|
994
|
+
lines.push(`})`);
|
|
995
|
+
lines.push("");
|
|
996
|
+
lines.push(`export interface ${tsName} {`);
|
|
997
|
+
for (const field of schemaInfo.fields) lines.push(` ${field.name}${field.required ? "" : "?"}: ${toTs$1(field.model, schemaMap, identifier, namespace)};`);
|
|
998
|
+
lines.push(`}`);
|
|
999
|
+
lines.push("");
|
|
1000
|
+
break;
|
|
1001
|
+
case "enums":
|
|
1002
|
+
lines.push(`export const ${schemaName} = z.enum(${JSON.stringify(Object.values(schemaInfo.variants))})`);
|
|
1003
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1004
|
+
lines.push("");
|
|
1005
|
+
break;
|
|
1006
|
+
case "union": {
|
|
1007
|
+
const unionItems = Object.entries(schemaInfo.unionVariants).map(([, v]) => toZod$1(v, schemaMap)).join(", ");
|
|
1008
|
+
lines.push(`export const ${schemaName} = z.union([${unionItems}])`);
|
|
1009
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1010
|
+
lines.push("");
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case "taggedUnion": {
|
|
1014
|
+
const variantKey = schemaInfo.variantKey;
|
|
1015
|
+
const payloadKey = schemaInfo.payloadKey;
|
|
1016
|
+
const unionItems = Object.entries(schemaInfo.unionVariants).map(([key, v]) => `z.object({ ${JSON.stringify(variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(payloadKey)}: ${toZod$1(v, schemaMap)} })`);
|
|
1017
|
+
lines.push(`export const ${schemaName} = z.discriminatedUnion(${JSON.stringify(variantKey)}, [${unionItems.join(", ")}])`);
|
|
1018
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1019
|
+
lines.push("");
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
784
1023
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1024
|
+
return lines.join("\n");
|
|
1025
|
+
}
|
|
1026
|
+
function generateOpFile(operation, schemaMap, identifier, namespace) {
|
|
1027
|
+
const lines = [];
|
|
1028
|
+
const OperationName = pascalCase(operation.id);
|
|
1029
|
+
const operationName = camelCase(operation.id);
|
|
1030
|
+
const hasBody = operation.requestModel != null && operation.requestModel.kind !== "null";
|
|
1031
|
+
const hasParams = Object.keys(operation.pathVariables).length > 0;
|
|
1032
|
+
const hasQuery = Object.keys(operation.queries).length > 0;
|
|
1033
|
+
const hasHeaders = Object.keys(operation.headers).length > 0;
|
|
1034
|
+
if (hasParams || hasQuery || hasHeaders) lines.push(`import { z } from "zod"`);
|
|
1035
|
+
const schemaImports = [];
|
|
1036
|
+
if (hasBody && "id" in operation.requestModel) {
|
|
1037
|
+
const root = resolveNamedRoot(operation.requestModel);
|
|
1038
|
+
if (root && schemaMap.has(root.id)) schemaImports.push(camelCase(root.id) + "Schema");
|
|
1039
|
+
}
|
|
1040
|
+
const typeImports = [];
|
|
1041
|
+
if (hasBody && "id" in operation.requestModel) {
|
|
1042
|
+
const root = resolveNamedRoot(operation.requestModel);
|
|
1043
|
+
if (root) {
|
|
1044
|
+
const typeName = identifier(root.id);
|
|
1045
|
+
if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
for (const responseModel of Object.values(operation.responses)) {
|
|
1049
|
+
if (responseModel == null) continue;
|
|
1050
|
+
const root = resolveNamedRoot(responseModel);
|
|
1051
|
+
if (root) {
|
|
1052
|
+
const typeName = identifier(root.id);
|
|
1053
|
+
if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const allImports = [...new Set([...schemaImports, ...typeImports])];
|
|
1057
|
+
lines.push(`import type { Context } from "hono"`);
|
|
1058
|
+
if (allImports.length > 0 || schemaImports.length > 0) {
|
|
1059
|
+
const parts = [];
|
|
1060
|
+
if (typeImports.length > 0) parts.push(typeImports.join(", "));
|
|
1061
|
+
if (schemaImports.length > 0) parts.push(schemaImports.join(", "));
|
|
1062
|
+
lines.push(`import { ${parts.join(", ")} } from "./models"`);
|
|
1063
|
+
}
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
if (hasParams) {
|
|
1066
|
+
const fields = Object.entries(operation.pathVariables).map(([key, value]) => ` ${key}: ${toZod$1(value.model, schemaMap)},`);
|
|
1067
|
+
lines.push(`const ${operationName}Params = z.object({`);
|
|
1068
|
+
lines.push(...fields);
|
|
1069
|
+
lines.push(`})`);
|
|
1070
|
+
lines.push("");
|
|
1071
|
+
}
|
|
1072
|
+
if (hasQuery) {
|
|
1073
|
+
const fields = Object.entries(operation.queries).map(([key, query]) => ` ${key}: ${toZod$1(query.model, schemaMap)}${query.required ? "" : ".optional()"},`);
|
|
1074
|
+
lines.push(`const ${operationName}Query = z.object({`);
|
|
1075
|
+
lines.push(...fields);
|
|
1076
|
+
lines.push(`})`);
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
}
|
|
1079
|
+
if (hasHeaders) {
|
|
1080
|
+
const fields = Object.entries(operation.headers).map(([key, header]) => ` "${key}": ${toZod$1(header.model, schemaMap)}${header.required ? "" : ".optional()"},`);
|
|
1081
|
+
lines.push(`const ${operationName}Headers = z.object({`);
|
|
1082
|
+
lines.push(...fields);
|
|
1083
|
+
lines.push(`})`);
|
|
1084
|
+
lines.push("");
|
|
1085
|
+
}
|
|
1086
|
+
const requestFields = [];
|
|
1087
|
+
if (hasParams) {
|
|
1088
|
+
const field = Object.entries(operation.pathVariables).map(([key, value]) => `${key}: ${toTs$1(value.model, schemaMap, identifier, namespace)}`).join("; ");
|
|
1089
|
+
requestFields.push(`params: { ${field} }`);
|
|
1090
|
+
}
|
|
1091
|
+
if (hasQuery) {
|
|
1092
|
+
const field = Object.entries(operation.queries).map(([key, query]) => `${key}${query.required ? "" : "?"}: ${toTs$1(query.model, schemaMap, identifier, namespace)}`).join("; ");
|
|
1093
|
+
requestFields.push(`query: { ${field} }`);
|
|
1094
|
+
}
|
|
1095
|
+
if (hasHeaders) {
|
|
1096
|
+
const field = Object.entries(operation.headers).map(([key, header]) => `"${key}"${header.required ? "" : "?"}: ${toTs$1(header.model, schemaMap, identifier, namespace)}`).join("; ");
|
|
1097
|
+
requestFields.push(`headers: { ${field} }`);
|
|
1098
|
+
}
|
|
1099
|
+
if (hasBody) requestFields.push(`body: ${toTs$1(operation.requestModel, schemaMap, identifier, namespace)}`);
|
|
1100
|
+
lines.push(`export interface ${OperationName}Request {`);
|
|
1101
|
+
for (const field of requestFields) lines.push(` ${field}`);
|
|
1102
|
+
lines.push(`}`);
|
|
1103
|
+
lines.push("");
|
|
1104
|
+
const responseEntries = Object.entries(operation.responses);
|
|
1105
|
+
const responseKind = (status) => operation.responseKinds[Number(status)] ?? "json-response";
|
|
1106
|
+
const responseBodyField = (kind, model) => {
|
|
1107
|
+
if (model == null) {
|
|
1108
|
+
if (kind === "binary") return "body: Blob";
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
if (kind === "stream-response" || kind === "sse-response") return `stream: ReadableStream<${toTs$1(model, schemaMap, identifier, namespace)}>`;
|
|
1112
|
+
if (kind === "binary") return "body: Blob";
|
|
1113
|
+
return `body: ${toTs$1(model, schemaMap, identifier, namespace)}`;
|
|
1114
|
+
};
|
|
1115
|
+
if (responseEntries.length === 1) {
|
|
1116
|
+
const [status, responseModel] = responseEntries[0];
|
|
1117
|
+
const bodyField = responseBodyField(responseKind(status), responseModel);
|
|
1118
|
+
if (bodyField != null) lines.push(`export type ${OperationName}Response = { status: ${status}; ${bodyField} }`);
|
|
1119
|
+
else lines.push(`export type ${OperationName}Response = { status: ${status} }`);
|
|
1120
|
+
} else {
|
|
1121
|
+
lines.push(`export type ${OperationName}Response =`);
|
|
1122
|
+
const parts = responseEntries.map(([status, responseModel]) => {
|
|
1123
|
+
const bodyField = responseBodyField(responseKind(status), responseModel);
|
|
1124
|
+
if (bodyField != null) return ` | { status: ${status}; ${bodyField} }`;
|
|
1125
|
+
return ` | { status: ${status} }`;
|
|
1126
|
+
});
|
|
1127
|
+
lines.push(parts.join("\n"));
|
|
1128
|
+
}
|
|
1129
|
+
lines.push("");
|
|
1130
|
+
if (requestFields.length > 0) lines.push(`export type ${OperationName}Handler = (req: ${OperationName}Request) => Promise<${OperationName}Response>`);
|
|
1131
|
+
else lines.push(`export type ${OperationName}Handler = () => Promise<${OperationName}Response>`);
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
lines.push(`export function ${operationName}(handler: ${OperationName}Handler) {`);
|
|
1134
|
+
lines.push(` return {`);
|
|
1135
|
+
lines.push(` method: "${operation.method.toUpperCase()}",`);
|
|
1136
|
+
lines.push(` path: "${toHonoPath(operation.path)}",`);
|
|
1137
|
+
lines.push(` async handler(context: Context): Promise<Response> {`);
|
|
1138
|
+
const requestArgs = [];
|
|
1139
|
+
if (hasParams) {
|
|
1140
|
+
lines.push(` const params = ${operationName}Params.parse(context.req.param())`);
|
|
1141
|
+
requestArgs.push("params");
|
|
1142
|
+
}
|
|
1143
|
+
if (hasQuery) {
|
|
1144
|
+
lines.push(` const query = ${operationName}Query.parse(context.req.query())`);
|
|
1145
|
+
requestArgs.push("query");
|
|
1146
|
+
}
|
|
1147
|
+
if (hasHeaders) {
|
|
1148
|
+
const headerParts = Object.keys(operation.headers).map((key) => ` "${key}": context.req.header("${key}"),`);
|
|
1149
|
+
lines.push(` const headers = ${operationName}Headers.parse({`);
|
|
1150
|
+
lines.push(...headerParts);
|
|
1151
|
+
lines.push(` })`);
|
|
1152
|
+
requestArgs.push("headers");
|
|
1153
|
+
}
|
|
1154
|
+
if (hasBody) {
|
|
1155
|
+
const schemaName = camelCase(operation.requestModel.id) + "Schema";
|
|
1156
|
+
lines.push(` const body = ${schemaName}.parse(await context.req.json())`);
|
|
1157
|
+
requestArgs.push("body");
|
|
1158
|
+
}
|
|
1159
|
+
if (requestArgs.length > 0) lines.push(` const result = await handler({ ${requestArgs.join(", ")} })`);
|
|
1160
|
+
else lines.push(` const result = await handler()`);
|
|
1161
|
+
lines.push(` switch (result.status) {`);
|
|
1162
|
+
for (const [status, responseModel] of Object.entries(operation.responses)) {
|
|
1163
|
+
const kind = operation.responseKinds[Number(status)] ?? "json-response";
|
|
1164
|
+
if (responseModel != null) if (kind === "json-response") lines.push(` case ${status}: return new Response(JSON.stringify(result.body), { status: ${status}, headers: { "Content-Type": "application/json" } })`);
|
|
1165
|
+
else if (kind === "binary") lines.push(` case ${status}: return new Response(result.body, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
|
|
1166
|
+
else lines.push(` case ${status}: return new Response(result.stream, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
|
|
1167
|
+
else if (kind === "binary") lines.push(` case ${status}: return new Response(result.body, { status: ${status}, headers: { "Content-Type": "${contentTypeForKind(kind)}" } })`);
|
|
1168
|
+
else lines.push(` case ${status}: return new Response(null, { status: ${status} })`);
|
|
1169
|
+
}
|
|
1170
|
+
lines.push(` default: return new Response(JSON.stringify({ message: \`Unexpected response status \${(result as { status: number }).status}\` }), { status: 500, headers: { "Content-Type": "application/json" } })`);
|
|
1171
|
+
lines.push(` }`);
|
|
1172
|
+
lines.push(` },`);
|
|
1173
|
+
lines.push(` }`);
|
|
1174
|
+
lines.push(`}`);
|
|
1175
|
+
return lines.join("\n");
|
|
1176
|
+
}
|
|
1177
|
+
function generateIndex(operations, _identifier) {
|
|
1178
|
+
const lines = [];
|
|
1179
|
+
lines.push(`import { Hono } from "hono"`);
|
|
1180
|
+
lines.push("");
|
|
1181
|
+
for (const operation of operations) {
|
|
1182
|
+
const operationName = camelCase(operation.id);
|
|
1183
|
+
const OperationName = pascalCase(operation.id);
|
|
1184
|
+
lines.push(`import { ${operationName}, type ${OperationName}Handler, type ${OperationName}Request, type ${OperationName}Response } from "./${operationName}"`);
|
|
1185
|
+
lines.push(`export type { ${OperationName}Handler, ${OperationName}Request, ${OperationName}Response }`);
|
|
1186
|
+
}
|
|
1187
|
+
lines.push("");
|
|
1188
|
+
const groups = groupBy$1(operations, (operation) => operation.group);
|
|
1189
|
+
lines.push(`export function mountRoutes(`);
|
|
1190
|
+
lines.push(` app: Hono,`);
|
|
1191
|
+
lines.push(` handlers: {`);
|
|
1192
|
+
for (const [group, groupOps] of Object.entries(groups)) {
|
|
1193
|
+
lines.push(` ${camelCase(group)}: {`);
|
|
1194
|
+
for (const operation of groupOps) {
|
|
1195
|
+
const operationName = camelCase(operation.id);
|
|
1196
|
+
lines.push(` ${operationName}: ${pascalCase(operation.id)}Handler,`);
|
|
806
1197
|
}
|
|
807
|
-
|
|
1198
|
+
lines.push(` },`);
|
|
1199
|
+
}
|
|
1200
|
+
lines.push(` },`);
|
|
1201
|
+
lines.push(`) {`);
|
|
1202
|
+
for (const [group, groupOps] of Object.entries(groups)) {
|
|
1203
|
+
lines.push(` // ── ${group} ──`);
|
|
1204
|
+
for (const operation of groupOps) {
|
|
1205
|
+
const operationName = camelCase(operation.id);
|
|
1206
|
+
lines.push(` const ${operationName}Def = ${operationName}(handlers.${camelCase(group)}.${operationName})`);
|
|
1207
|
+
lines.push(` app.on(${operationName}Def.method, ${operationName}Def.path, ${operationName}Def.handler)`);
|
|
1208
|
+
}
|
|
1209
|
+
lines.push("");
|
|
1210
|
+
}
|
|
1211
|
+
lines.push(`}`);
|
|
1212
|
+
lines.push("");
|
|
1213
|
+
return lines.join("\n");
|
|
1214
|
+
}
|
|
1215
|
+
function groupBy$1(items, keyFn) {
|
|
1216
|
+
const map = {};
|
|
1217
|
+
for (const item of items) {
|
|
1218
|
+
const key = keyFn(item);
|
|
1219
|
+
if (!map[key]) map[key] = [];
|
|
1220
|
+
map[key].push(item);
|
|
808
1221
|
}
|
|
1222
|
+
return map;
|
|
809
1223
|
}
|
|
810
|
-
function
|
|
811
|
-
return
|
|
1224
|
+
function toHonoPath(path) {
|
|
1225
|
+
return path.replace(/\{(\w+)\}/g, ":$1");
|
|
812
1226
|
}
|
|
813
|
-
function
|
|
814
|
-
|
|
1227
|
+
function contentTypeForKind(kind) {
|
|
1228
|
+
switch (kind) {
|
|
1229
|
+
case "binary": return "application/octet-stream";
|
|
1230
|
+
case "stream-response": return "application/x-ndjson";
|
|
1231
|
+
case "sse-response": return "text/event-stream";
|
|
1232
|
+
default: return "application/json";
|
|
1233
|
+
}
|
|
815
1234
|
}
|
|
816
|
-
function
|
|
817
|
-
|
|
1235
|
+
function toZod$1(model, schemaMap) {
|
|
1236
|
+
switch (model.kind) {
|
|
1237
|
+
case "int32": return "z.coerce.number().int()";
|
|
1238
|
+
case "float32":
|
|
1239
|
+
case "float64": return "z.coerce.number()";
|
|
1240
|
+
case "boolean": return "z.coerce.boolean()";
|
|
1241
|
+
case "string": return "z.string()";
|
|
1242
|
+
case "datetime": return "z.string().datetime()";
|
|
1243
|
+
case "date": return "z.string().date()";
|
|
1244
|
+
case "duration": return "z.string()";
|
|
1245
|
+
case "literal": return `z.literal(${JSON.stringify(model.value)})`;
|
|
1246
|
+
case "null": return "z.null()";
|
|
1247
|
+
case "array": return `${toZod$1(model.base, schemaMap)}.array()`;
|
|
1248
|
+
case "set": return `${toZod$1(model.base, schemaMap)}.array()`;
|
|
1249
|
+
case "map": return `z.record(z.string(), ${toZod$1(model.base, schemaMap)})`;
|
|
1250
|
+
case "enums": return `z.enum(${JSON.stringify(Object.values(model.variants))})`;
|
|
1251
|
+
case "record": return schemaMap.get(model.id)?.fields ? camelCase(model.id) + "Schema" : "z.unknown()";
|
|
1252
|
+
case "union":
|
|
1253
|
+
if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
|
|
1254
|
+
return `z.union([${Object.values(model.variants).map((v) => toZod$1(v, schemaMap)).join(", ")}])`;
|
|
1255
|
+
case "taggedUnion": {
|
|
1256
|
+
if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
|
|
1257
|
+
const unionItems = Object.entries(model.variants).map(([key, v]) => `z.object({ ${JSON.stringify(model.variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(model.payloadKey)}: ${toZod$1(v, schemaMap)} })`);
|
|
1258
|
+
return `z.discriminatedUnion(${JSON.stringify(model.variantKey)}, [${unionItems.join(", ")}])`;
|
|
1259
|
+
}
|
|
1260
|
+
default: return "z.unknown()";
|
|
1261
|
+
}
|
|
818
1262
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
model
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
return codes.entries().map(([path, code]) => ({
|
|
846
|
-
path,
|
|
847
|
-
code
|
|
848
|
-
})).toArray();
|
|
1263
|
+
function toTs$1(model, schemaMap, identifier, namespace) {
|
|
1264
|
+
switch (model.kind) {
|
|
1265
|
+
case "int32":
|
|
1266
|
+
case "float32":
|
|
1267
|
+
case "float64": return "number";
|
|
1268
|
+
case "boolean": return "boolean";
|
|
1269
|
+
case "string":
|
|
1270
|
+
case "datetime":
|
|
1271
|
+
case "date":
|
|
1272
|
+
case "duration": return "string";
|
|
1273
|
+
case "literal": return JSON.stringify(model.value);
|
|
1274
|
+
case "null": return "null";
|
|
1275
|
+
case "array":
|
|
1276
|
+
case "set": return `${toTs$1(model.base, schemaMap, identifier, namespace)}[]`;
|
|
1277
|
+
case "map": return `Record<string, ${toTs$1(model.base, schemaMap, identifier, namespace)}>`;
|
|
1278
|
+
case "enums":
|
|
1279
|
+
if (schemaMap.get(model.id)?.variants) return identifier(model.id);
|
|
1280
|
+
return Object.values(model.variants).map((v) => JSON.stringify(v)).join(" | ");
|
|
1281
|
+
case "record": return schemaMap.get(model.id)?.fields ? identifier(model.id) : "unknown";
|
|
1282
|
+
case "union":
|
|
1283
|
+
case "taggedUnion":
|
|
1284
|
+
if (schemaMap.get(model.id)?.unionVariants) return identifier(model.id);
|
|
1285
|
+
return Object.values(model.variants).map((v) => toTs$1(v, schemaMap, identifier, namespace)).join(" | ");
|
|
1286
|
+
default: return "unknown";
|
|
1287
|
+
}
|
|
849
1288
|
}
|
|
850
1289
|
//#endregion
|
|
851
|
-
//#region src/
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1290
|
+
//#region src/codegen/ts-client.ts
|
|
1291
|
+
function generateTsClient(options) {
|
|
1292
|
+
const { routers, identifier = pascalCase, namespace } = options;
|
|
1293
|
+
const operations = collectOperations(routers);
|
|
1294
|
+
const schemaMap = collectSchemaMap(operations);
|
|
1295
|
+
const files = {};
|
|
1296
|
+
files["models.ts"] = generateModels(schemaMap, identifier, namespace);
|
|
1297
|
+
for (const operation of operations) files[`${camelCase(operation.id)}.ts`] = generateClientFn(operation, schemaMap, identifier, namespace);
|
|
1298
|
+
files["index.ts"] = generateClientIndex(operations, identifier);
|
|
1299
|
+
return files;
|
|
1300
|
+
}
|
|
1301
|
+
function generateModels(schemaMap, identifier, namespace) {
|
|
1302
|
+
const lines = [];
|
|
1303
|
+
lines.push(`import { z } from "zod"`);
|
|
1304
|
+
lines.push("");
|
|
1305
|
+
for (const [id, schemaInfo] of schemaMap) {
|
|
1306
|
+
const schemaName = camelCase(id) + "Schema";
|
|
1307
|
+
const tsName = identifier(id);
|
|
1308
|
+
switch (schemaInfo.kind) {
|
|
1309
|
+
case "record":
|
|
1310
|
+
lines.push(`export const ${schemaName} = z.object({`);
|
|
1311
|
+
for (const f of schemaInfo.fields) lines.push(` ${f.name}: ${toZod(f.model, schemaMap)}${f.required ? "" : ".optional()"},`);
|
|
1312
|
+
lines.push(`})`);
|
|
1313
|
+
lines.push("");
|
|
1314
|
+
lines.push(`export interface ${tsName} {`);
|
|
1315
|
+
for (const f of schemaInfo.fields) lines.push(` ${f.name}${f.required ? "" : "?"}: ${toTs(f.model, schemaMap, identifier, namespace)};`);
|
|
1316
|
+
lines.push(`}`);
|
|
1317
|
+
lines.push("");
|
|
1318
|
+
break;
|
|
1319
|
+
case "enums":
|
|
1320
|
+
lines.push(`export const ${schemaName} = z.enum(${JSON.stringify(Object.values(schemaInfo.variants))})`);
|
|
1321
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1322
|
+
lines.push("");
|
|
1323
|
+
break;
|
|
1324
|
+
case "union": {
|
|
1325
|
+
const items = Object.entries(schemaInfo.unionVariants).map(([, v]) => toZod(v, schemaMap)).join(", ");
|
|
1326
|
+
lines.push(`export const ${schemaName} = z.union([${items}])`);
|
|
1327
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case "taggedUnion": {
|
|
1332
|
+
const vk = schemaInfo.variantKey;
|
|
1333
|
+
const pk = schemaInfo.payloadKey;
|
|
1334
|
+
const items = Object.entries(schemaInfo.unionVariants).map(([key, v]) => `z.object({ ${JSON.stringify(vk)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(pk)}: ${toZod(v, schemaMap)} })`);
|
|
1335
|
+
lines.push(`export const ${schemaName} = z.discriminatedUnion(${JSON.stringify(vk)}, [${items.join(", ")}])`);
|
|
1336
|
+
lines.push(`export type ${tsName} = z.infer<typeof ${schemaName}>`);
|
|
1337
|
+
lines.push("");
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
877
1340
|
}
|
|
878
|
-
return deserializeFailed(model, value);
|
|
879
1341
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1342
|
+
return lines.join("\n");
|
|
1343
|
+
}
|
|
1344
|
+
function generateClientFn(operation, schemaMap, identifier, namespace) {
|
|
1345
|
+
const lines = [];
|
|
1346
|
+
const functionName = camelCase(operation.id);
|
|
1347
|
+
const OperationName = pascalCase(operation.id);
|
|
1348
|
+
const hasBody = operation.requestModel != null && operation.requestModel.kind !== "null";
|
|
1349
|
+
const hasParams = Object.keys(operation.pathVariables).length > 0;
|
|
1350
|
+
const hasQuery = Object.keys(operation.queries).length > 0;
|
|
1351
|
+
const hasHeaders = Object.keys(operation.headers).length > 0;
|
|
1352
|
+
const typeImports = [];
|
|
1353
|
+
const addNamedRef = (model) => {
|
|
1354
|
+
const root = resolveNamedRoot(model);
|
|
1355
|
+
if (root) {
|
|
1356
|
+
const typeName = identifier(root.id);
|
|
1357
|
+
if (schemaMap.has(root.id) && !typeImports.includes(typeName)) typeImports.push(typeName);
|
|
885
1358
|
}
|
|
886
|
-
|
|
1359
|
+
};
|
|
1360
|
+
if (hasBody) addNamedRef(operation.requestModel);
|
|
1361
|
+
for (const responseModel of Object.values(operation.responses)) if (responseModel != null) addNamedRef(responseModel);
|
|
1362
|
+
const okStatus = Object.keys(operation.responses).find((s) => Number(s) >= 200 && Number(s) < 300);
|
|
1363
|
+
const okModel = okStatus ? operation.responses[Number(okStatus)] : null;
|
|
1364
|
+
const okKind = okStatus ? operation.responseKinds[Number(okStatus)] ?? "json-response" : "json-response";
|
|
1365
|
+
const isStreamLike = okKind === "stream-response" || okKind === "sse-response" || okKind === "binary";
|
|
1366
|
+
const okSchema = okModel ? resolveZodSchema(okModel, schemaMap) : null;
|
|
1367
|
+
const allModelImports = [...typeImports];
|
|
1368
|
+
if (okSchema && !isStreamLike) {
|
|
1369
|
+
const schemaRefs = collectSchemaRefs(okModel, schemaMap);
|
|
1370
|
+
for (const ref of schemaRefs) if (!allModelImports.includes(ref)) allModelImports.push(ref);
|
|
887
1371
|
}
|
|
888
|
-
if (
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (result.isSuccess) properties.set(key, result.value);
|
|
915
|
-
else return result;
|
|
916
|
-
}
|
|
917
|
-
return success(properties);
|
|
918
|
-
}
|
|
919
|
-
return deserializeFailed(model, value);
|
|
920
|
-
}
|
|
921
|
-
if (model.kind === "enums") {
|
|
922
|
-
if (typeof value === "string" && Object.values(model.variants).includes(value)) return success(value);
|
|
923
|
-
return deserializeFailed(model, value);
|
|
924
|
-
}
|
|
925
|
-
if (model.kind === "record") {
|
|
926
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
927
|
-
const properties = /* @__PURE__ */ new Map();
|
|
928
|
-
for (const key in model.properties) {
|
|
929
|
-
const result = deserializeInner(model.properties[key], value[key]);
|
|
930
|
-
if (result.isSuccess) properties.set(key, result.value);
|
|
931
|
-
else return deserializeFailed(model, value);
|
|
932
|
-
}
|
|
933
|
-
return success(Object.fromEntries(properties));
|
|
1372
|
+
if (allModelImports.length > 0) {
|
|
1373
|
+
lines.push(`import { ${allModelImports.join(", ")} } from "./models"`);
|
|
1374
|
+
lines.push("");
|
|
1375
|
+
}
|
|
1376
|
+
const requestFields = [];
|
|
1377
|
+
for (const [n, v] of Object.entries(operation.pathVariables)) requestFields.push(`${n}: ${toTs(v.model, schemaMap, identifier, namespace)}`);
|
|
1378
|
+
for (const [n, q] of Object.entries(operation.queries)) {
|
|
1379
|
+
const required = q.required ? "" : "?";
|
|
1380
|
+
requestFields.push(`${n}${required}: ${toTs(q.model, schemaMap, identifier, namespace)}`);
|
|
1381
|
+
}
|
|
1382
|
+
if (hasBody) requestFields.push(`body: ${toTs(operation.requestModel, schemaMap, identifier, namespace)}`);
|
|
1383
|
+
if (hasHeaders) {
|
|
1384
|
+
const field = Object.entries(operation.headers).map(([key, header]) => `"${key}"${header.required ? "" : "?"}: ${toTs(header.model, schemaMap, identifier, namespace)}`).join("; ");
|
|
1385
|
+
requestFields.push(`headers?: { ${field} } & Record<string, string>`);
|
|
1386
|
+
} else requestFields.push("headers?: Record<string, string>");
|
|
1387
|
+
requestFields.push("baseUrl?: string");
|
|
1388
|
+
lines.push(`export interface ${OperationName}Request {`);
|
|
1389
|
+
for (const field of requestFields) lines.push(` ${field}`);
|
|
1390
|
+
lines.push(`}`);
|
|
1391
|
+
lines.push("");
|
|
1392
|
+
const responseEntries = Object.entries(operation.responses);
|
|
1393
|
+
const responseKind = (status) => operation.responseKinds[Number(status)] ?? "json-response";
|
|
1394
|
+
const responseBodyField = (kind, responseModel) => {
|
|
1395
|
+
if (responseModel == null) {
|
|
1396
|
+
if (kind === "binary") return "body: Blob";
|
|
1397
|
+
return null;
|
|
934
1398
|
}
|
|
935
|
-
return
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1399
|
+
if (kind === "stream-response" || kind === "sse-response") return `stream: ReadableStream<${toTs(responseModel, schemaMap, identifier, namespace)}>`;
|
|
1400
|
+
if (kind === "binary") return "body: Blob";
|
|
1401
|
+
return `body: ${toTs(responseModel, schemaMap, identifier, namespace)}`;
|
|
1402
|
+
};
|
|
1403
|
+
if (responseEntries.length === 1) {
|
|
1404
|
+
const [status, responseModel] = responseEntries[0];
|
|
1405
|
+
const bodyField = responseBodyField(responseKind(status), responseModel);
|
|
1406
|
+
if (bodyField != null) lines.push(`export type ${OperationName}Response = { status: ${status}; ${bodyField} }`);
|
|
1407
|
+
else lines.push(`export type ${OperationName}Response = { status: ${status} }`);
|
|
1408
|
+
} else {
|
|
1409
|
+
lines.push(`export type ${OperationName}Response =`);
|
|
1410
|
+
for (const [status, responseModel] of responseEntries) {
|
|
1411
|
+
const bodyField = responseBodyField(responseKind(status), responseModel);
|
|
1412
|
+
if (bodyField != null) lines.push(` | { status: ${status}; ${bodyField} }`);
|
|
1413
|
+
else lines.push(` | { status: ${status} }`);
|
|
944
1414
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1415
|
+
}
|
|
1416
|
+
lines.push("");
|
|
1417
|
+
const reqParam = hasParams || hasBody ? `req: ${OperationName}Request` : `req?: ${OperationName}Request`;
|
|
1418
|
+
const retType = okModel && !isStreamLike ? toTs(okModel, schemaMap, identifier, namespace) : "Response";
|
|
1419
|
+
const pathExpr = operation.path.replace(/\{(\w+)\}/g, (_, name) => `\${encodeURIComponent(req.${name})}`);
|
|
1420
|
+
lines.push(`export async function ${functionName}(${reqParam}): Promise<${retType}> {`);
|
|
1421
|
+
lines.push(` const baseUrl = req?.baseUrl ?? ""`);
|
|
1422
|
+
if (hasQuery) {
|
|
1423
|
+
lines.push(` const parts: string[] = []`);
|
|
1424
|
+
for (const [n, q] of Object.entries(operation.queries)) if (q.required) lines.push(` parts.push("${n}=" + encodeURIComponent(req.${n}))`);
|
|
1425
|
+
else lines.push(` if (req?.${n} != null) parts.push("${n}=" + encodeURIComponent(req.${n}))`);
|
|
1426
|
+
lines.push(` const qs = parts.length > 0 ? "?" + parts.join("&") : ""`);
|
|
1427
|
+
lines.push(` const url = \`\${baseUrl}${pathExpr}\${qs}\``);
|
|
1428
|
+
} else lines.push(` const url = \`\${baseUrl}${pathExpr}\``);
|
|
1429
|
+
lines.push("");
|
|
1430
|
+
lines.push(` const res = await fetch(url, {`);
|
|
1431
|
+
lines.push(` method: "${operation.method}",`);
|
|
1432
|
+
if (hasBody) {
|
|
1433
|
+
lines.push(` headers: { "Content-Type": "application/json", ...req.headers },`);
|
|
1434
|
+
lines.push(` body: JSON.stringify(req.body),`);
|
|
1435
|
+
} else lines.push(` headers: req?.headers,`);
|
|
1436
|
+
lines.push(` })`);
|
|
1437
|
+
lines.push(` if (!res.ok) throw new Error(\`${operation.method} ${operation.path} failed: \${res.status}\`)`);
|
|
1438
|
+
if (okModel && !isStreamLike) if (okSchema) lines.push(` return ${okSchema}.parse(await res.json())`);
|
|
1439
|
+
else lines.push(` return res.json()`);
|
|
1440
|
+
else lines.push(` return res`);
|
|
1441
|
+
lines.push(`}`);
|
|
1442
|
+
return lines.join("\n");
|
|
1443
|
+
}
|
|
1444
|
+
function generateClientIndex(operations, _identifier) {
|
|
1445
|
+
const lines = [];
|
|
1446
|
+
const groups = groupBy(operations, (operation) => operation.group);
|
|
1447
|
+
for (const [group, groupOps] of Object.entries(groups)) {
|
|
1448
|
+
lines.push(`// ── ${group} ──`);
|
|
1449
|
+
for (const operation of groupOps) {
|
|
1450
|
+
const n = camelCase(operation.id);
|
|
1451
|
+
const OperationName = pascalCase(operation.id);
|
|
1452
|
+
lines.push(`export { ${n}, type ${OperationName}Request, type ${OperationName}Response } from "./${n}"`);
|
|
957
1453
|
}
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1454
|
+
lines.push("");
|
|
1455
|
+
}
|
|
1456
|
+
return lines.join("\n");
|
|
1457
|
+
}
|
|
1458
|
+
function groupBy(items, keyFn) {
|
|
1459
|
+
const map = {};
|
|
1460
|
+
for (const item of items) {
|
|
1461
|
+
const key = keyFn(item);
|
|
1462
|
+
if (!map[key]) map[key] = [];
|
|
1463
|
+
map[key].push(item);
|
|
1464
|
+
}
|
|
1465
|
+
return map;
|
|
1466
|
+
}
|
|
1467
|
+
function toTs(model, schemaMap, identifier, namespace) {
|
|
1468
|
+
switch (model.kind) {
|
|
1469
|
+
case "int32":
|
|
1470
|
+
case "float32":
|
|
1471
|
+
case "float64": return "number";
|
|
1472
|
+
case "boolean": return "boolean";
|
|
1473
|
+
case "string":
|
|
1474
|
+
case "datetime":
|
|
1475
|
+
case "date":
|
|
1476
|
+
case "duration": return "string";
|
|
1477
|
+
case "literal": return JSON.stringify(model.value);
|
|
1478
|
+
case "null": return "null";
|
|
1479
|
+
case "array":
|
|
1480
|
+
case "set": return `${toTs(model.base, schemaMap, identifier, namespace)}[]`;
|
|
1481
|
+
case "map": return `Record<string, ${toTs(model.base, schemaMap, identifier, namespace)}>`;
|
|
1482
|
+
case "enums":
|
|
1483
|
+
if (schemaMap.get(model.id)?.variants) return identifier(model.id);
|
|
1484
|
+
return Object.values(model.variants).map((v) => JSON.stringify(v)).join(" | ");
|
|
1485
|
+
case "record": return schemaMap.get(model.id)?.fields ? identifier(model.id) : "unknown";
|
|
1486
|
+
case "union":
|
|
1487
|
+
case "taggedUnion":
|
|
1488
|
+
if (schemaMap.get(model.id)?.unionVariants) return identifier(model.id);
|
|
1489
|
+
return Object.values(model.variants).map((v) => toTs(v, schemaMap, identifier, namespace)).join(" | ");
|
|
1490
|
+
default: return "unknown";
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function toZod(model, schemaMap) {
|
|
1494
|
+
switch (model.kind) {
|
|
1495
|
+
case "int32": return "z.coerce.number().int()";
|
|
1496
|
+
case "float32":
|
|
1497
|
+
case "float64": return "z.coerce.number()";
|
|
1498
|
+
case "boolean": return "z.coerce.boolean()";
|
|
1499
|
+
case "string": return "z.string()";
|
|
1500
|
+
case "datetime": return "z.string().datetime()";
|
|
1501
|
+
case "date": return "z.string().date()";
|
|
1502
|
+
case "duration": return "z.string()";
|
|
1503
|
+
case "literal": return `z.literal(${JSON.stringify(model.value)})`;
|
|
1504
|
+
case "null": return "z.null()";
|
|
1505
|
+
case "array": return `${toZod(model.base, schemaMap)}.array()`;
|
|
1506
|
+
case "set": return `${toZod(model.base, schemaMap)}.array()`;
|
|
1507
|
+
case "map": return `z.record(z.string(), ${toZod(model.base, schemaMap)})`;
|
|
1508
|
+
case "enums": return `z.enum(${JSON.stringify(Object.values(model.variants))})`;
|
|
1509
|
+
case "record": return schemaMap.get(model.id)?.fields ? camelCase(model.id) + "Schema" : "z.unknown()";
|
|
1510
|
+
case "union":
|
|
1511
|
+
if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
|
|
1512
|
+
return `z.union([${Object.values(model.variants).map((v) => toZod(v, schemaMap)).join(", ")}])`;
|
|
1513
|
+
case "taggedUnion": {
|
|
1514
|
+
if (schemaMap.get(model.id)?.unionVariants) return camelCase(model.id) + "Schema";
|
|
1515
|
+
const items = Object.entries(model.variants).map(([key, v]) => `z.object({ ${JSON.stringify(model.variantKey)}: z.literal(${JSON.stringify(key)}), ${JSON.stringify(model.payloadKey)}: ${toZod(v, schemaMap)} })`);
|
|
1516
|
+
return `z.discriminatedUnion(${JSON.stringify(model.variantKey)}, [${items.join(", ")}])`;
|
|
966
1517
|
}
|
|
967
|
-
return
|
|
968
|
-
code: model.code,
|
|
969
|
-
context: Object.fromEntries(properties)
|
|
970
|
-
});
|
|
1518
|
+
default: return "z.unknown()";
|
|
971
1519
|
}
|
|
972
|
-
return deserializeFailed(model, value);
|
|
973
1520
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1521
|
+
function resolveZodSchema(model, schemaMap) {
|
|
1522
|
+
switch (model.kind) {
|
|
1523
|
+
case "record":
|
|
1524
|
+
case "union":
|
|
1525
|
+
case "taggedUnion":
|
|
1526
|
+
case "enums": return schemaMap.get(model.id) ? camelCase(model.id) + "Schema" : null;
|
|
1527
|
+
case "array":
|
|
1528
|
+
case "set": {
|
|
1529
|
+
const inner = resolveZodSchema(model.base, schemaMap);
|
|
1530
|
+
return inner ? `${inner}.array()` : null;
|
|
981
1531
|
}
|
|
982
|
-
|
|
1532
|
+
case "map": {
|
|
1533
|
+
const inner = resolveZodSchema(model.base, schemaMap);
|
|
1534
|
+
return inner ? `z.record(z.string(), ${inner})` : null;
|
|
1535
|
+
}
|
|
1536
|
+
default: return null;
|
|
1537
|
+
}
|
|
983
1538
|
}
|
|
984
|
-
function
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
for (const key in model.context) properties.push(`"${key}": ${serialize(model.context[key], value[key])}`);
|
|
996
|
-
return `{"code":"${model.code}",context:{${properties}}}`;
|
|
997
|
-
}
|
|
998
|
-
if (model.kind === "record") {
|
|
999
|
-
const properties = [];
|
|
1000
|
-
for (const key in model.properties) properties.push(`"${key}": ${serialize(model.properties[key], value[key])}`);
|
|
1001
|
-
return `{${properties}}`;
|
|
1002
|
-
}
|
|
1003
|
-
if (model.kind === "union") {
|
|
1004
|
-
const variantKeys = Object.keys(model.variants);
|
|
1005
|
-
for (const key of variantKeys) if (key in value) return `{"${key}":${serialize(model.variants[key], value[key])}}`;
|
|
1006
|
-
}
|
|
1007
|
-
if (model.kind === "tagged-union") return JSON.stringify(value);
|
|
1008
|
-
return String(value);
|
|
1539
|
+
function collectSchemaRefs(model, schemaMap) {
|
|
1540
|
+
switch (model.kind) {
|
|
1541
|
+
case "record":
|
|
1542
|
+
case "union":
|
|
1543
|
+
case "taggedUnion":
|
|
1544
|
+
case "enums": return schemaMap.get(model.id) ? [camelCase(model.id) + "Schema"] : [];
|
|
1545
|
+
case "array":
|
|
1546
|
+
case "set": return collectSchemaRefs(model.base, schemaMap);
|
|
1547
|
+
case "map": return collectSchemaRefs(model.base, schemaMap);
|
|
1548
|
+
default: return [];
|
|
1549
|
+
}
|
|
1009
1550
|
}
|
|
1010
1551
|
//#endregion
|
|
1011
|
-
export { array, binary, boolean,
|
|
1552
|
+
export { apikey, array, binary, boolean, collectNamedModels, collectOperations, collectSchemaMap, createJsonSchemaRegistry, createOpenapiSchemaRegistry, date, datetime, deployOpenIdConnect, duration, enums, float32, float64, generateHonoServer, generateJsonSchema, generateOpenapi, generateTsClient, int32, json, jsonStream, literal, map, mergeJsonSchemas, nullLike, openIdConnect, record, resolveNamedRoot, route, set, sseStream, string, taggedUnion, union };
|
|
1012
1553
|
|
|
1013
1554
|
//# sourceMappingURL=index.mjs.map
|