@kizunajs/kotlin 2.0.0-beta.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/LICENSE.MD +9 -0
- package/README.md +19 -0
- package/dist/cli.cjs +66 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +67 -0
- package/dist/generator-BvNpSarY.mjs +1795 -0
- package/dist/generator-s2pn7vqP.cjs +1800 -0
- package/dist/index.cjs +22 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.mjs +20 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1800 @@
|
|
|
1
|
+
let kizunajs_generator = require("kizunajs/generator");
|
|
2
|
+
//#region src/emit.ts
|
|
3
|
+
var KotlinWriter = class {
|
|
4
|
+
lines = [];
|
|
5
|
+
depth = 0;
|
|
6
|
+
line(text = "") {
|
|
7
|
+
if (text === "") {
|
|
8
|
+
this.lines.push("");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
this.lines.push(" ".repeat(this.depth) + text);
|
|
12
|
+
}
|
|
13
|
+
block(opener, body) {
|
|
14
|
+
this.line(`${opener} {`);
|
|
15
|
+
this.depth += 1;
|
|
16
|
+
body();
|
|
17
|
+
this.depth -= 1;
|
|
18
|
+
this.line("}");
|
|
19
|
+
}
|
|
20
|
+
indent(body) {
|
|
21
|
+
this.depth += 1;
|
|
22
|
+
body();
|
|
23
|
+
this.depth -= 1;
|
|
24
|
+
}
|
|
25
|
+
docComment(text) {
|
|
26
|
+
if (!text) return;
|
|
27
|
+
const segments = text.split("\n");
|
|
28
|
+
if (segments.length === 1) {
|
|
29
|
+
this.line(`/** ${segments[0]} */`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.line("/**");
|
|
33
|
+
for (const segment of segments) this.line(` * ${segment}`);
|
|
34
|
+
this.line(" */");
|
|
35
|
+
}
|
|
36
|
+
blank() {
|
|
37
|
+
if (this.lines.length === 0) return;
|
|
38
|
+
if (this.lines[this.lines.length - 1] === "") return;
|
|
39
|
+
this.lines.push("");
|
|
40
|
+
}
|
|
41
|
+
appendToLastLine(suffix) {
|
|
42
|
+
if (this.lines.length > 0) this.lines[this.lines.length - 1] += suffix;
|
|
43
|
+
}
|
|
44
|
+
toString() {
|
|
45
|
+
return this.lines.join("\n").replace(/\n+$/, "") + "\n";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const stringLiteral = (value) => {
|
|
49
|
+
return "\"" + value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/zod-to-kotlin.ts
|
|
53
|
+
var TypeRegistry = class {
|
|
54
|
+
types = /* @__PURE__ */ new Map();
|
|
55
|
+
warningSet = /* @__PURE__ */ new Set();
|
|
56
|
+
explicitIds = /* @__PURE__ */ new Set();
|
|
57
|
+
sealedVariantPayloads = /* @__PURE__ */ new Set();
|
|
58
|
+
sealedVariantPaths = /* @__PURE__ */ new Map();
|
|
59
|
+
usesJsonElement = false;
|
|
60
|
+
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
61
|
+
this.camelCaseProperties = camelCaseProperties;
|
|
62
|
+
this.unknownEnumCase = unknownEnumCase;
|
|
63
|
+
}
|
|
64
|
+
has(name) {
|
|
65
|
+
return this.types.has(name);
|
|
66
|
+
}
|
|
67
|
+
get(name) {
|
|
68
|
+
return this.types.get(name);
|
|
69
|
+
}
|
|
70
|
+
add(type) {
|
|
71
|
+
if (this.types.has(type.name)) return;
|
|
72
|
+
this.types.set(type.name, type);
|
|
73
|
+
}
|
|
74
|
+
replace(type) {
|
|
75
|
+
this.types.set(type.name, type);
|
|
76
|
+
}
|
|
77
|
+
all() {
|
|
78
|
+
return Array.from(this.types.values());
|
|
79
|
+
}
|
|
80
|
+
markExplicitId(name) {
|
|
81
|
+
this.explicitIds.add(name);
|
|
82
|
+
}
|
|
83
|
+
isExplicitId(name) {
|
|
84
|
+
return this.explicitIds.has(name);
|
|
85
|
+
}
|
|
86
|
+
markSealedVariantPayload(name) {
|
|
87
|
+
this.sealedVariantPayloads.add(name);
|
|
88
|
+
}
|
|
89
|
+
isSealedVariantPayload(name) {
|
|
90
|
+
return this.sealedVariantPayloads.has(name);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An inlined sealed variant has no class of its own, so references to it go through the sealed
|
|
94
|
+
* interface: `UserSessionEvent.Logout`.
|
|
95
|
+
*/
|
|
96
|
+
markSealedVariantPath(name, path) {
|
|
97
|
+
this.sealedVariantPaths.set(name, path);
|
|
98
|
+
}
|
|
99
|
+
sealedVariantPath(name) {
|
|
100
|
+
return this.sealedVariantPaths.get(name);
|
|
101
|
+
}
|
|
102
|
+
warnJsonElement(hint, reason) {
|
|
103
|
+
this.usesJsonElement = true;
|
|
104
|
+
this.warningSet.add(`${hint}: ${reason}`);
|
|
105
|
+
}
|
|
106
|
+
warnings() {
|
|
107
|
+
return Array.from(this.warningSet);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The Kotlin property name for a wire key: verbatim, or camelCased under
|
|
112
|
+
* `camelCaseProperties`.
|
|
113
|
+
*/
|
|
114
|
+
const propertyName = (key, camelCase) => {
|
|
115
|
+
if (!camelCase) return (0, kizunajs_generator.sanitizeFieldName)(key);
|
|
116
|
+
const camel = (0, kizunajs_generator.toCamelCase)(key);
|
|
117
|
+
if (!camel) return "field";
|
|
118
|
+
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
119
|
+
};
|
|
120
|
+
const objectFields = (schema, registry, hint) => {
|
|
121
|
+
const shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {};
|
|
122
|
+
const fields = [];
|
|
123
|
+
const seen = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
125
|
+
const result = mapType(value, registry, `${hint}${(0, kizunajs_generator.toPascalCase)(key)}`);
|
|
126
|
+
const deprecation = (0, kizunajs_generator.readDeprecation)(value);
|
|
127
|
+
const kotlinName = propertyName(key, registry.camelCaseProperties);
|
|
128
|
+
const previousWireName = seen.get(kotlinName);
|
|
129
|
+
if (previousWireName !== void 0 && previousWireName !== key) throw new Error(`@kizunajs/kotlin: field key collision in ${hint}, ${stringify(previousWireName)} and ${stringify(key)} both sanitize to ${stringify(kotlinName)}.`);
|
|
130
|
+
seen.set(kotlinName, key);
|
|
131
|
+
fields.push({
|
|
132
|
+
name: kotlinName,
|
|
133
|
+
wireName: key,
|
|
134
|
+
type: result.expression,
|
|
135
|
+
optional: result.optional,
|
|
136
|
+
description: (0, kizunajs_generator.readMetaDescription)(value),
|
|
137
|
+
isFile: result.isFile,
|
|
138
|
+
deprecated: deprecation !== void 0,
|
|
139
|
+
deprecationMessage: deprecation?.message
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return fields;
|
|
143
|
+
};
|
|
144
|
+
const stringify = (value) => JSON.stringify(value);
|
|
145
|
+
const mapType = (schema, registry, hint) => {
|
|
146
|
+
if ((0, kizunajs_generator.isFileSchema)(schema)) return {
|
|
147
|
+
expression: "MultipartFile",
|
|
148
|
+
optional: false,
|
|
149
|
+
isFile: true
|
|
150
|
+
};
|
|
151
|
+
const id = (0, kizunajs_generator.readMetaId)(schema);
|
|
152
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
153
|
+
const discriminated = (0, kizunajs_generator.readDiscriminatedUnion)(schema);
|
|
154
|
+
if (discriminated) {
|
|
155
|
+
const enumName = id ?? (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.toPascalCase)(hint));
|
|
156
|
+
if (id) registry.markExplicitId(enumName);
|
|
157
|
+
if (!registry.has(enumName)) {
|
|
158
|
+
registry.add({
|
|
159
|
+
kind: "sealed-class",
|
|
160
|
+
name: enumName,
|
|
161
|
+
discriminator: discriminated.discriminator,
|
|
162
|
+
variants: [],
|
|
163
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
164
|
+
});
|
|
165
|
+
const variants = [];
|
|
166
|
+
for (const option of discriminated.options) {
|
|
167
|
+
const variantId = (0, kizunajs_generator.readMetaId)(option);
|
|
168
|
+
const literal = (0, kizunajs_generator.readDiscriminatorStringLiteral)(option, discriminated.discriminator);
|
|
169
|
+
const variantResult = mapType(option, registry, variantId ?? `${enumName}${(0, kizunajs_generator.toPascalCase)(literal ?? "Variant")}`);
|
|
170
|
+
if (literal !== void 0) variants.push({
|
|
171
|
+
caseName: sanitizeCaseName(literal),
|
|
172
|
+
literal,
|
|
173
|
+
payloadType: variantResult.expression,
|
|
174
|
+
nested: variantId === void 0
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
for (const variant of variants) {
|
|
178
|
+
if (variant.nested) {
|
|
179
|
+
registry.markSealedVariantPayload(variant.payloadType);
|
|
180
|
+
registry.markSealedVariantPath(variant.payloadType, `${enumName}.${variant.caseName}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const payload = registry.get(variant.payloadType);
|
|
184
|
+
if (payload?.kind === "data-class") registry.replace({
|
|
185
|
+
...payload,
|
|
186
|
+
sealedParent: enumName,
|
|
187
|
+
serialName: variant.literal,
|
|
188
|
+
discriminatorWireName: discriminated.discriminator
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
registry.replace({
|
|
192
|
+
kind: "sealed-class",
|
|
193
|
+
name: enumName,
|
|
194
|
+
discriminator: discriminated.discriminator,
|
|
195
|
+
variants,
|
|
196
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
expression: enumName,
|
|
201
|
+
optional: false
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (id && def.type === "object") {
|
|
205
|
+
registry.markExplicitId(id);
|
|
206
|
+
if (!registry.has(id)) {
|
|
207
|
+
registry.add({
|
|
208
|
+
kind: "data-class",
|
|
209
|
+
name: id,
|
|
210
|
+
fields: [],
|
|
211
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
212
|
+
});
|
|
213
|
+
const fields = objectFields(schema, registry, id);
|
|
214
|
+
registry.replace({
|
|
215
|
+
kind: "data-class",
|
|
216
|
+
name: id,
|
|
217
|
+
fields,
|
|
218
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
expression: id,
|
|
223
|
+
optional: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const { inner, optional } = (0, kizunajs_generator.unwrapOptionalWrappers)(schema);
|
|
227
|
+
if (optional) {
|
|
228
|
+
const innerResult = mapType(inner, registry, hint);
|
|
229
|
+
return {
|
|
230
|
+
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
231
|
+
optional: true,
|
|
232
|
+
isFile: innerResult.isFile
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
switch (def.type) {
|
|
236
|
+
case "void":
|
|
237
|
+
case "never": return {
|
|
238
|
+
expression: "Unit",
|
|
239
|
+
optional: false
|
|
240
|
+
};
|
|
241
|
+
case "pipe": {
|
|
242
|
+
const outType = def.out ? (0, kizunajs_generator.readDefType)(def.out) : void 0;
|
|
243
|
+
if (def.out && outType !== "transform") return mapType(def.out, registry, hint);
|
|
244
|
+
if (def.in) return mapType(def.in, registry, hint);
|
|
245
|
+
registry.warnJsonElement(hint, "pipe schema missing resolvable type");
|
|
246
|
+
return {
|
|
247
|
+
expression: "JsonElement",
|
|
248
|
+
optional: false
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case "string": return {
|
|
252
|
+
expression: def.format === "datetime" || def.checks?.some((check) => check.format === "datetime") ? "Instant" : "String",
|
|
253
|
+
optional: false
|
|
254
|
+
};
|
|
255
|
+
case "number": return {
|
|
256
|
+
expression: (0, kizunajs_generator.isIntegerSchema)(schema) ? "Int" : "Double",
|
|
257
|
+
optional: false
|
|
258
|
+
};
|
|
259
|
+
case "bigint": return {
|
|
260
|
+
expression: "Long",
|
|
261
|
+
optional: false
|
|
262
|
+
};
|
|
263
|
+
case "boolean": return {
|
|
264
|
+
expression: "Boolean",
|
|
265
|
+
optional: false
|
|
266
|
+
};
|
|
267
|
+
case "date": return {
|
|
268
|
+
expression: "Instant",
|
|
269
|
+
optional: false
|
|
270
|
+
};
|
|
271
|
+
case "array": {
|
|
272
|
+
const element = def.element;
|
|
273
|
+
if (!element) {
|
|
274
|
+
registry.warnJsonElement(hint, "array element schema missing");
|
|
275
|
+
return {
|
|
276
|
+
expression: "List<JsonElement>",
|
|
277
|
+
optional: false
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
expression: `List<${mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "")}>`,
|
|
282
|
+
optional: false
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case "object": {
|
|
286
|
+
const className = (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.toPascalCase)(hint));
|
|
287
|
+
if (!registry.has(className)) {
|
|
288
|
+
registry.add({
|
|
289
|
+
kind: "data-class",
|
|
290
|
+
name: className,
|
|
291
|
+
fields: [],
|
|
292
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
293
|
+
});
|
|
294
|
+
const fields = objectFields(schema, registry, className);
|
|
295
|
+
registry.replace({
|
|
296
|
+
kind: "data-class",
|
|
297
|
+
name: className,
|
|
298
|
+
fields,
|
|
299
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
expression: className,
|
|
304
|
+
optional: false
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
case "enum": {
|
|
308
|
+
const enumName = id ?? (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.toPascalCase)(hint));
|
|
309
|
+
if (id) registry.markExplicitId(enumName);
|
|
310
|
+
const entries = def.entries ?? {};
|
|
311
|
+
const cases = Object.values(entries);
|
|
312
|
+
if (!registry.has(enumName)) registry.add({
|
|
313
|
+
kind: "enum-class",
|
|
314
|
+
name: enumName,
|
|
315
|
+
cases,
|
|
316
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema),
|
|
317
|
+
unknownCase: registry.unknownEnumCase
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
expression: enumName,
|
|
321
|
+
optional: false
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
case "literal": {
|
|
325
|
+
const values = def.values ?? [];
|
|
326
|
+
if (values.every((value) => typeof value === "string")) return {
|
|
327
|
+
expression: "String",
|
|
328
|
+
optional: false
|
|
329
|
+
};
|
|
330
|
+
if (values.every((value) => typeof value === "number")) return {
|
|
331
|
+
expression: "Double",
|
|
332
|
+
optional: false
|
|
333
|
+
};
|
|
334
|
+
if (values.every((value) => typeof value === "boolean")) return {
|
|
335
|
+
expression: "Boolean",
|
|
336
|
+
optional: false
|
|
337
|
+
};
|
|
338
|
+
registry.warnJsonElement(hint, "literal of mixed types");
|
|
339
|
+
return {
|
|
340
|
+
expression: "JsonElement",
|
|
341
|
+
optional: false
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case "union": {
|
|
345
|
+
const options = def.options ?? [];
|
|
346
|
+
if (options.every((option) => {
|
|
347
|
+
const optionDef = (0, kizunajs_generator.readDef)(option);
|
|
348
|
+
return optionDef.type === "literal" && (optionDef.values ?? []).every((value) => typeof value === "string");
|
|
349
|
+
})) return {
|
|
350
|
+
expression: "String",
|
|
351
|
+
optional: false
|
|
352
|
+
};
|
|
353
|
+
const resolved = options.map((option) => mapType(option, registry, hint)).filter((result) => result !== void 0 && result.expression !== "JsonElement");
|
|
354
|
+
if (resolved.length === 0) {
|
|
355
|
+
registry.warnJsonElement(hint, "non-discriminated union");
|
|
356
|
+
return {
|
|
357
|
+
expression: "JsonElement",
|
|
358
|
+
optional: false
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const expressions = resolved.map((result) => result.expression);
|
|
362
|
+
const unique = [...new Set(expressions)];
|
|
363
|
+
if (unique.length === 1) return resolved[0];
|
|
364
|
+
const listPattern = /^List<(.+)>$/;
|
|
365
|
+
const baseTypes = unique.map((expression) => {
|
|
366
|
+
const match = listPattern.exec(expression);
|
|
367
|
+
return match ? match[1] : expression;
|
|
368
|
+
});
|
|
369
|
+
if (new Set(baseTypes).size === 1) return {
|
|
370
|
+
expression: `List<${baseTypes[0]}>`,
|
|
371
|
+
optional: false
|
|
372
|
+
};
|
|
373
|
+
registry.warnJsonElement(hint, "non-discriminated union");
|
|
374
|
+
return {
|
|
375
|
+
expression: "JsonElement",
|
|
376
|
+
optional: false
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case "record": {
|
|
380
|
+
const valueType = def.valueType;
|
|
381
|
+
if (valueType) return {
|
|
382
|
+
expression: `Map<String, ${mapType(valueType, registry, `${hint}Value`).expression.replace(/\?$/, "")}>`,
|
|
383
|
+
optional: false
|
|
384
|
+
};
|
|
385
|
+
registry.warnJsonElement(hint, "record without value schema");
|
|
386
|
+
return {
|
|
387
|
+
expression: "Map<String, JsonElement>",
|
|
388
|
+
optional: false
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
case "any":
|
|
392
|
+
case "unknown":
|
|
393
|
+
registry.warnJsonElement(hint, `${def.type} schema`);
|
|
394
|
+
return {
|
|
395
|
+
expression: "JsonElement",
|
|
396
|
+
optional: false
|
|
397
|
+
};
|
|
398
|
+
default:
|
|
399
|
+
registry.warnJsonElement(hint, `unhandled schema type: ${def.type ?? "unknown"}`);
|
|
400
|
+
return {
|
|
401
|
+
expression: "JsonElement",
|
|
402
|
+
optional: false
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const sanitizeCaseName = (literal) => {
|
|
407
|
+
const cleaned = literal.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
408
|
+
if (!cleaned) return "value";
|
|
409
|
+
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
410
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
411
|
+
};
|
|
412
|
+
const collectObjectFields = (schema, registry, hint) => {
|
|
413
|
+
return objectFields(schema, registry, hint);
|
|
414
|
+
};
|
|
415
|
+
const objectFieldCount = (schema) => {
|
|
416
|
+
return Object.keys((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
|
|
417
|
+
};
|
|
418
|
+
const objectShapeKeys = (schema, camelCase = false) => {
|
|
419
|
+
return Object.entries((0, kizunajs_generator.readObjectShape)(schema) ?? {}).map(([key, value]) => ({
|
|
420
|
+
name: propertyName(key, camelCase),
|
|
421
|
+
wireName: key,
|
|
422
|
+
isFile: (0, kizunajs_generator.isFileSchema)(value)
|
|
423
|
+
}));
|
|
424
|
+
};
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/generator.ts
|
|
427
|
+
const BODY_FLATTEN_MAX_FIELDS = 6;
|
|
428
|
+
const METHODS_REQUIRING_BODY = new Set([
|
|
429
|
+
"POST",
|
|
430
|
+
"PUT",
|
|
431
|
+
"PATCH"
|
|
432
|
+
]);
|
|
433
|
+
const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMessage, methodNameOverride) => {
|
|
434
|
+
const fullJoinedName = routeKey.includes(".") ? routeKey.split(".").map((segment, index) => index === 0 ? segment : (0, kizunajs_generator.toPascalCase)(segment)).join("") : routeKey;
|
|
435
|
+
const methodName = methodNameOverride ?? fullJoinedName;
|
|
436
|
+
const baseHint = (0, kizunajs_generator.toPascalCase)(fullJoinedName);
|
|
437
|
+
const pathParams = (0, kizunajs_generator.parsePath)(route.path).paramNames;
|
|
438
|
+
const declaredPathParams = route.pathParams ? collectObjectFields(route.pathParams, registry, `${baseHint}Params`) : [];
|
|
439
|
+
const queryFields = route.query ? collectObjectFields(route.query, registry, `${baseHint}Query`) : [];
|
|
440
|
+
const headerFields = route.headers ? collectObjectFields(route.headers, registry, `${baseHint}Headers`) : [];
|
|
441
|
+
const bodyDefType = route.body?._def?.type ?? route.body?.def?.type;
|
|
442
|
+
let bodyDescriptor;
|
|
443
|
+
if (route.body && bodyDefType !== "void") {
|
|
444
|
+
const bodyHint = (0, kizunajs_generator.readMetaId)(route.body) ?? `${baseHint}Input`;
|
|
445
|
+
const isMultipart = route.contentType === "multipart/form-data";
|
|
446
|
+
if ((0, kizunajs_generator.isDiscriminatedUnionSchema)(route.body)) bodyDescriptor = {
|
|
447
|
+
kind: "union",
|
|
448
|
+
structName: mapType(route.body, registry, bodyHint).expression,
|
|
449
|
+
flattened: [],
|
|
450
|
+
multipartFields: []
|
|
451
|
+
};
|
|
452
|
+
else if (isMultipart) {
|
|
453
|
+
mapType(route.body, registry, bodyHint);
|
|
454
|
+
bodyDescriptor = {
|
|
455
|
+
kind: "multipart",
|
|
456
|
+
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
457
|
+
multipartFields: objectShapeKeys(route.body, registry.camelCaseProperties)
|
|
458
|
+
};
|
|
459
|
+
} else {
|
|
460
|
+
const isObject = (0, kizunajs_generator.isObjectSchema)(route.body);
|
|
461
|
+
const fieldCount = isObject ? objectFieldCount(route.body) : 0;
|
|
462
|
+
if (isObject && fieldCount === 0) bodyDescriptor = {
|
|
463
|
+
kind: "json-empty",
|
|
464
|
+
flattened: [],
|
|
465
|
+
multipartFields: []
|
|
466
|
+
};
|
|
467
|
+
else {
|
|
468
|
+
const useStruct = !isObject || fieldCount > BODY_FLATTEN_MAX_FIELDS;
|
|
469
|
+
const structName = mapType(route.body, registry, bodyHint).expression;
|
|
470
|
+
if (useStruct) bodyDescriptor = {
|
|
471
|
+
kind: "json-struct",
|
|
472
|
+
structName,
|
|
473
|
+
flattened: [],
|
|
474
|
+
multipartFields: []
|
|
475
|
+
};
|
|
476
|
+
else bodyDescriptor = {
|
|
477
|
+
kind: "json-flat",
|
|
478
|
+
structName,
|
|
479
|
+
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
480
|
+
multipartFields: []
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const successResponses = [];
|
|
486
|
+
const errorCases = [];
|
|
487
|
+
let stream;
|
|
488
|
+
for (const [statusKey, responseValue] of Object.entries(route.responses)) {
|
|
489
|
+
const status = Number(statusKey);
|
|
490
|
+
const responseHint = `${baseHint}Response${status === 200 ? "" : status}`;
|
|
491
|
+
if ((0, kizunajs_generator.isStreamResponse)(responseValue)) {
|
|
492
|
+
const mode = (0, kizunajs_generator.streamMode)(responseValue);
|
|
493
|
+
if (mode === "events" && (0, kizunajs_generator.isNamedStream)(responseValue.stream)) stream = {
|
|
494
|
+
status,
|
|
495
|
+
mode,
|
|
496
|
+
events: Object.entries(responseValue.stream).map(([name, schema]) => ({
|
|
497
|
+
name,
|
|
498
|
+
caseName: (0, kizunajs_generator.toPascalCase)(name),
|
|
499
|
+
type: mapType(schema, registry, `${baseHint}${(0, kizunajs_generator.toPascalCase)(name)}`).expression
|
|
500
|
+
}))
|
|
501
|
+
};
|
|
502
|
+
else if (mode === "events") stream = {
|
|
503
|
+
status,
|
|
504
|
+
mode,
|
|
505
|
+
messageType: mapType(responseValue.stream, registry, `${baseHint}Message`).expression
|
|
506
|
+
};
|
|
507
|
+
else stream = {
|
|
508
|
+
status,
|
|
509
|
+
mode
|
|
510
|
+
};
|
|
511
|
+
const headersSchema = (0, kizunajs_generator.resolveResponseHeaders)(responseValue);
|
|
512
|
+
successResponses.push({
|
|
513
|
+
status,
|
|
514
|
+
type: stream.messageType ?? "Event",
|
|
515
|
+
responseHeaders: headersSchema ? collectObjectFields(headersSchema, registry, `${baseHint}ResponseHeaders`) : []
|
|
516
|
+
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const typeExpression = mapType((0, kizunajs_generator.resolveResponseBody)(responseValue), registry, responseHint).expression;
|
|
520
|
+
if ((0, kizunajs_generator.isSuccessStatus)(status)) {
|
|
521
|
+
const headersSchema = (0, kizunajs_generator.resolveResponseHeaders)(responseValue);
|
|
522
|
+
const perStatusHeaderFields = headersSchema ? collectObjectFields(headersSchema, registry, `${baseHint}ResponseHeaders`) : [];
|
|
523
|
+
successResponses.push({
|
|
524
|
+
status,
|
|
525
|
+
type: typeExpression,
|
|
526
|
+
responseHeaders: perStatusHeaderFields
|
|
527
|
+
});
|
|
528
|
+
} else errorCases.push({
|
|
529
|
+
caseName: (0, kizunajs_generator.statusToCamelCase)(status),
|
|
530
|
+
status,
|
|
531
|
+
type: typeExpression
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (route.body || route.query) {
|
|
535
|
+
const has400 = errorCases.some((candidate) => candidate.status === 400);
|
|
536
|
+
errorCases.push({
|
|
537
|
+
caseName: has400 ? "validationError" : (0, kizunajs_generator.statusToCamelCase)(400),
|
|
538
|
+
status: 400,
|
|
539
|
+
type: "ValidationError"
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
successResponses.sort((left, right) => left.status - right.status);
|
|
543
|
+
const resultHeaderFields = (0, kizunajs_generator.mergeHeaderFields)(successResponses.map((entry) => entry.responseHeaders));
|
|
544
|
+
let successReturnType = "Unit";
|
|
545
|
+
let successSumClassName;
|
|
546
|
+
if (route.method !== "HEAD") {
|
|
547
|
+
const onlySuccess = successResponses[0];
|
|
548
|
+
if (successResponses.length === 1 && onlySuccess) successReturnType = onlySuccess.type;
|
|
549
|
+
else if (successResponses.length > 1) {
|
|
550
|
+
successSumClassName = "Success";
|
|
551
|
+
successReturnType = successSumClassName;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const resultWrapperName = successReturnType !== "Unit" ? "Result" : void 0;
|
|
555
|
+
return {
|
|
556
|
+
name: methodName,
|
|
557
|
+
operationName: baseHint,
|
|
558
|
+
summary: route.summary,
|
|
559
|
+
description: route.description,
|
|
560
|
+
deprecated,
|
|
561
|
+
deprecationMessage,
|
|
562
|
+
pathParams,
|
|
563
|
+
declaredPathParams,
|
|
564
|
+
pathTemplate: route.path,
|
|
565
|
+
method: route.method,
|
|
566
|
+
body: bodyDescriptor,
|
|
567
|
+
query: queryFields,
|
|
568
|
+
headers: headerFields,
|
|
569
|
+
resultHeaderFields,
|
|
570
|
+
resultWrapperName,
|
|
571
|
+
successResponses,
|
|
572
|
+
successReturnType,
|
|
573
|
+
successSumClassName,
|
|
574
|
+
failureClassName: "Failure",
|
|
575
|
+
errorCases,
|
|
576
|
+
stream
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
const kotlinGenerator = (0, kizunajs_generator.createGenerator)((options, _api) => {
|
|
580
|
+
const flatMethods = [];
|
|
581
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
582
|
+
return {
|
|
583
|
+
processRoute({ routeKey, route, deprecated, deprecationMessage }) {
|
|
584
|
+
if ((0, kizunajs_generator.routeStreams)(route) && (0, kizunajs_generator.soleStreamResponse)(route) === void 0) {
|
|
585
|
+
console.warn(`[kizuna/kotlin] Skipping route "${routeKey}": a streamed status beside another 2xx status is not generated.`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const dotIndex = routeKey.indexOf(".");
|
|
589
|
+
if (dotIndex !== -1) {
|
|
590
|
+
const groupKey = routeKey.slice(0, dotIndex);
|
|
591
|
+
const leafName = routeKey.slice(dotIndex + 1).split(".").map((segment, index) => index === 0 ? segment : (0, kizunajs_generator.toPascalCase)(segment)).join("");
|
|
592
|
+
const methods = groupMap.get(groupKey) ?? [];
|
|
593
|
+
if (methods.length === 0) groupMap.set(groupKey, methods);
|
|
594
|
+
methods.push(buildRouteMethod(routeKey, route, options.registry, deprecated, deprecationMessage, leafName));
|
|
595
|
+
} else flatMethods.push(buildRouteMethod(routeKey, route, options.registry, deprecated, deprecationMessage));
|
|
596
|
+
},
|
|
597
|
+
finalize() {
|
|
598
|
+
const groups = [];
|
|
599
|
+
for (const [groupKey, methods] of groupMap) groups.push({
|
|
600
|
+
groupKey,
|
|
601
|
+
className: `${options.namespaceName}${(0, kizunajs_generator.toPascalCase)(groupKey)}Client`,
|
|
602
|
+
propertyName: groupKey,
|
|
603
|
+
methods
|
|
604
|
+
});
|
|
605
|
+
return {
|
|
606
|
+
flatMethods,
|
|
607
|
+
groups
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
const KOTLIN_KEYWORDS = new Set([
|
|
613
|
+
"as",
|
|
614
|
+
"break",
|
|
615
|
+
"class",
|
|
616
|
+
"continue",
|
|
617
|
+
"do",
|
|
618
|
+
"else",
|
|
619
|
+
"false",
|
|
620
|
+
"for",
|
|
621
|
+
"fun",
|
|
622
|
+
"if",
|
|
623
|
+
"in",
|
|
624
|
+
"interface",
|
|
625
|
+
"is",
|
|
626
|
+
"null",
|
|
627
|
+
"object",
|
|
628
|
+
"package",
|
|
629
|
+
"return",
|
|
630
|
+
"super",
|
|
631
|
+
"this",
|
|
632
|
+
"throw",
|
|
633
|
+
"true",
|
|
634
|
+
"try",
|
|
635
|
+
"typealias",
|
|
636
|
+
"typeof",
|
|
637
|
+
"val",
|
|
638
|
+
"var",
|
|
639
|
+
"when",
|
|
640
|
+
"while"
|
|
641
|
+
]);
|
|
642
|
+
const escapeKeyword = (name) => {
|
|
643
|
+
return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
644
|
+
};
|
|
645
|
+
const optionalize = (type, optional) => {
|
|
646
|
+
if (!optional) return type;
|
|
647
|
+
return type.endsWith("?") ? type : `${type}?`;
|
|
648
|
+
};
|
|
649
|
+
const KOTLIN_PRIMITIVE_TYPES = new Set([
|
|
650
|
+
"String",
|
|
651
|
+
"Int",
|
|
652
|
+
"Double",
|
|
653
|
+
"Long",
|
|
654
|
+
"Boolean",
|
|
655
|
+
"Instant",
|
|
656
|
+
"Unit"
|
|
657
|
+
]);
|
|
658
|
+
const resolveType = (typeName, currentOperation, context, scope = "client") => {
|
|
659
|
+
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
660
|
+
const optional = typeName.endsWith("?");
|
|
661
|
+
const base = optional ? typeName.slice(0, -1) : typeName;
|
|
662
|
+
if (KOTLIN_PRIMITIVE_TYPES.has(base)) return typeName;
|
|
663
|
+
if (base.startsWith("List<") && base.endsWith(">")) {
|
|
664
|
+
const resolved = resolveType(base.slice(5, -1), currentOperation, context, scope);
|
|
665
|
+
return optional ? `List<${resolved}>?` : `List<${resolved}>`;
|
|
666
|
+
}
|
|
667
|
+
if (base.startsWith("Map<String, ") && base.endsWith(">")) {
|
|
668
|
+
const resolved = resolveType(base.slice(12, -1), currentOperation, context, scope);
|
|
669
|
+
return optional ? `Map<String, ${resolved}>?` : `Map<String, ${resolved}>`;
|
|
670
|
+
}
|
|
671
|
+
if (base === "MultipartFile" || base === "ValidationError" || base === "ValidationIssue") {
|
|
672
|
+
const qualified = `${clientName}.${base}`;
|
|
673
|
+
return optional ? `${qualified}?` : qualified;
|
|
674
|
+
}
|
|
675
|
+
if (base === "JsonElement") return optional ? `JsonElement?` : `JsonElement`;
|
|
676
|
+
if (fileLevelTypeNames.has(base)) return optional ? `${base}?` : base;
|
|
677
|
+
const owningStruct = context.ownedTypeMap.get(base);
|
|
678
|
+
if (owningStruct !== void 0) {
|
|
679
|
+
const short = (0, kizunajs_generator.shortTypeName)(base, owningStruct);
|
|
680
|
+
const ownerResolved = resolveType(owningStruct, currentOperation, context, scope).replace(/\?$/, "");
|
|
681
|
+
return optional ? `${ownerResolved}.${short}?` : `${ownerResolved}.${short}`;
|
|
682
|
+
}
|
|
683
|
+
const owningOp = operationTypeMap.get(base);
|
|
684
|
+
if (owningOp === void 0) return optional ? `${namespaceName}.${base}?` : `${namespaceName}.${base}`;
|
|
685
|
+
const short = (0, kizunajs_generator.localTypeName)(base, owningOp);
|
|
686
|
+
if (owningOp === currentOperation && scope === "operation-object") return optional ? `${short}?` : short;
|
|
687
|
+
return optional ? `${clientName}.${owningOp}.${short}?` : `${clientName}.${owningOp}.${short}`;
|
|
688
|
+
};
|
|
689
|
+
const buildOperationTypeMap = (allMethods, registry) => {
|
|
690
|
+
const operationNames = new Set(allMethods.map((method) => method.operationName));
|
|
691
|
+
const operationTypeMap = /* @__PURE__ */ new Map();
|
|
692
|
+
for (const type of registry.all()) {
|
|
693
|
+
let bestMatch;
|
|
694
|
+
for (const operationName of operationNames) if ((0, kizunajs_generator.isHintPrefix)(type.name, operationName)) {
|
|
695
|
+
if (!bestMatch || operationName.length > bestMatch.length) bestMatch = operationName;
|
|
696
|
+
}
|
|
697
|
+
if (bestMatch !== void 0) operationTypeMap.set(type.name, bestMatch);
|
|
698
|
+
}
|
|
699
|
+
return operationTypeMap;
|
|
700
|
+
};
|
|
701
|
+
const localizeType = (type, operationName, context) => {
|
|
702
|
+
const shortName = (0, kizunajs_generator.localTypeName)(type.name, operationName);
|
|
703
|
+
if (type.kind === "data-class") return {
|
|
704
|
+
...type,
|
|
705
|
+
name: shortName,
|
|
706
|
+
fields: type.fields.map((field) => ({
|
|
707
|
+
...field,
|
|
708
|
+
type: resolveType(field.type, operationName, context, "operation-object")
|
|
709
|
+
}))
|
|
710
|
+
};
|
|
711
|
+
if (type.kind === "enum-class") return {
|
|
712
|
+
...type,
|
|
713
|
+
name: shortName
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...type,
|
|
717
|
+
name: shortName,
|
|
718
|
+
variants: type.variants.map((variant) => ({
|
|
719
|
+
...variant,
|
|
720
|
+
payloadType: resolveType(variant.payloadType, operationName, context, "operation-object")
|
|
721
|
+
}))
|
|
722
|
+
};
|
|
723
|
+
};
|
|
724
|
+
const deprecatedAnnotation = (message) => {
|
|
725
|
+
if (message === void 0 || message === "") return "@Deprecated(\"Deprecated\")";
|
|
726
|
+
return `@Deprecated(${stringLiteral(message)})`;
|
|
727
|
+
};
|
|
728
|
+
/**
|
|
729
|
+
* Converts an enum value into a valid Kotlin enum constant name.
|
|
730
|
+
*
|
|
731
|
+
* Uppercases and snake-cases the value, prefixing `_` when it starts with a
|
|
732
|
+
* digit (Kotlin identifiers cannot begin with one). The original value is
|
|
733
|
+
* preserved separately as the `@SerialName` wire value.
|
|
734
|
+
*/
|
|
735
|
+
const enumConstantName = (value) => {
|
|
736
|
+
const upper = value.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
737
|
+
return /^[0-9]/.test(upper) ? `_${upper}` : upper;
|
|
738
|
+
};
|
|
739
|
+
const emitEnumClass = (writer, name, cases, unknownCase, description) => {
|
|
740
|
+
writer.blank();
|
|
741
|
+
writer.docComment(description);
|
|
742
|
+
if (!unknownCase) {
|
|
743
|
+
writer.line("@Serializable");
|
|
744
|
+
writer.block(`enum class ${name}(override val wireValue: String) : KizunaQueryValue`, () => {
|
|
745
|
+
for (let index = 0; index < cases.length; index += 1) {
|
|
746
|
+
const caseName = cases[index];
|
|
747
|
+
const enumConstant = enumConstantName(caseName);
|
|
748
|
+
const separator = index < cases.length - 1 ? "," : "";
|
|
749
|
+
writer.line(`@SerialName(${stringLiteral(caseName)}) ${enumConstant}(${stringLiteral(caseName)})${separator}`);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
writer.line(`@Serializable(with = ${name}.Serializer::class)`);
|
|
755
|
+
writer.block(`sealed interface ${name} : KizunaQueryValue`, () => {
|
|
756
|
+
for (const caseName of cases) writer.block(`data object ${enumConstantName(caseName)} : ${name}`, () => {
|
|
757
|
+
writer.line(`override val wireValue: String = ${stringLiteral(caseName)}`);
|
|
758
|
+
});
|
|
759
|
+
writer.line(`data class Unknown(override val wireValue: String) : ${name}`);
|
|
760
|
+
writer.blank();
|
|
761
|
+
writer.block("companion object", () => {
|
|
762
|
+
writer.block(`fun fromWireValue(wireValue: String): ${name} = when (wireValue)`, () => {
|
|
763
|
+
for (const caseName of cases) writer.line(`${stringLiteral(caseName)} -> ${enumConstantName(caseName)}`);
|
|
764
|
+
writer.line("else -> Unknown(wireValue)");
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
writer.blank();
|
|
768
|
+
writer.block(`object Serializer : KSerializer<${name}>`, () => {
|
|
769
|
+
writer.line(`override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(${stringLiteral(name)}, PrimitiveKind.STRING)`);
|
|
770
|
+
writer.line(`override fun deserialize(decoder: Decoder): ${name} = ${name}.fromWireValue(decoder.decodeString())`);
|
|
771
|
+
writer.block(`override fun serialize(encoder: Encoder, value: ${name})`, () => {
|
|
772
|
+
writer.line("encoder.encodeString(value.wireValue)");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName) => {
|
|
778
|
+
const lookupName = registryName ?? type.name;
|
|
779
|
+
const baseFields = type.discriminatorWireName ? type.fields.filter((field) => field.wireName !== type.discriminatorWireName) : type.fields;
|
|
780
|
+
const hasFile = baseFields.some((field) => field.isFile);
|
|
781
|
+
const resolveOwnedType = (raw) => {
|
|
782
|
+
const optional = raw.endsWith("?");
|
|
783
|
+
const stripped = optional ? raw.slice(0, -1) : raw;
|
|
784
|
+
const isList = stripped.startsWith("List<") && stripped.endsWith(">");
|
|
785
|
+
const inner = isList ? stripped.slice(5, -1) : stripped;
|
|
786
|
+
if (ownedTypeMap.get(inner) !== lookupName) return raw;
|
|
787
|
+
const short = (0, kizunajs_generator.shortTypeName)(inner, lookupName);
|
|
788
|
+
const resolved = isList ? `List<${short}>` : short;
|
|
789
|
+
return optional ? `${resolved}?` : resolved;
|
|
790
|
+
};
|
|
791
|
+
const adjustedFields = baseFields.map((field) => ({
|
|
792
|
+
...field,
|
|
793
|
+
type: resolveOwnedType(field.type)
|
|
794
|
+
}));
|
|
795
|
+
const needsSerialName = !hasFile && baseFields.some((field) => field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name));
|
|
796
|
+
if (type.serialName !== void 0) writer.line(`@SerialName(${stringLiteral(type.serialName)})`);
|
|
797
|
+
if (!hasFile) writer.line("@Serializable");
|
|
798
|
+
const params = adjustedFields.map((field) => {
|
|
799
|
+
const typeExpression = optionalize(field.type, field.optional);
|
|
800
|
+
const defaultPart = field.optional ? " = null" : "";
|
|
801
|
+
const serialNameAnnotation = needsSerialName && (field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name)) ? `@SerialName(${stringLiteral(field.wireName)}) ` : "";
|
|
802
|
+
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
803
|
+
});
|
|
804
|
+
const hasOwnedTypes = Array.from(ownedTypeMap.entries()).some(([, owner]) => owner === lookupName);
|
|
805
|
+
if (params.length === 0) writer.line(`data class ${type.name}(private val unused: Unit = Unit)`);
|
|
806
|
+
else if (params.length === 1) writer.line(`data class ${type.name}(${params[0]})`);
|
|
807
|
+
else {
|
|
808
|
+
writer.line(`data class ${type.name}(`);
|
|
809
|
+
writer.indent(() => {
|
|
810
|
+
for (let index = 0; index < params.length; index += 1) {
|
|
811
|
+
const separator = index < params.length - 1 ? "," : "";
|
|
812
|
+
writer.line(`${params[index]}${separator}`);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
writer.line(")");
|
|
816
|
+
}
|
|
817
|
+
if (type.sealedParent !== void 0) writer.appendToLastLine(` : ${type.sealedParent}`);
|
|
818
|
+
if (hasOwnedTypes) {
|
|
819
|
+
writer.appendToLastLine(" {");
|
|
820
|
+
writer.indent(() => {
|
|
821
|
+
emitOwnedTypes(writer, lookupName, ownedTypeMap, ownedTypeLookup);
|
|
822
|
+
});
|
|
823
|
+
writer.line("}");
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const emitOwnedTypes = (writer, ownerName, ownedTypeMap, ownedTypeLookup, registry) => {
|
|
827
|
+
for (const [ownedName, owningType] of ownedTypeMap) {
|
|
828
|
+
if (owningType !== ownerName) continue;
|
|
829
|
+
const ownedType = ownedTypeLookup.get(ownedName);
|
|
830
|
+
if (!ownedType) continue;
|
|
831
|
+
const shortName = (0, kizunajs_generator.shortTypeName)(ownedName, ownerName);
|
|
832
|
+
emitType(writer, {
|
|
833
|
+
...ownedType,
|
|
834
|
+
name: shortName
|
|
835
|
+
}, ownedTypeMap, ownedTypeLookup, ownedName, registry);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
const relativeOwnedPath = (typeName, ownedTypeMap, enclosingPath, registry) => {
|
|
839
|
+
const path = ownedTypePath(typeName, ownedTypeMap, registry);
|
|
840
|
+
const prefix = `${enclosingPath}.`;
|
|
841
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) : void 0;
|
|
842
|
+
};
|
|
843
|
+
const ownedTypePath = (typeName, ownedTypeMap, registry) => {
|
|
844
|
+
const sealedPath = registry?.sealedVariantPath(typeName);
|
|
845
|
+
if (sealedPath !== void 0) return sealedPath;
|
|
846
|
+
const owningClass = ownedTypeMap.get(typeName);
|
|
847
|
+
if (owningClass === void 0) return typeName;
|
|
848
|
+
return `${ownedTypePath(owningClass, ownedTypeMap, registry)}.${(0, kizunajs_generator.shortTypeName)(typeName, owningClass)}`;
|
|
849
|
+
};
|
|
850
|
+
const emitSealedClass = (writer, type, registry, ownedTypeMap, ownedTypeLookup) => {
|
|
851
|
+
writer.line(`@OptIn(ExperimentalSerializationApi::class)`);
|
|
852
|
+
writer.line(`@JsonClassDiscriminator(${stringLiteral(type.discriminator)})`);
|
|
853
|
+
writer.line("@Serializable");
|
|
854
|
+
const nestedVariants = type.variants.filter((variant) => variant.nested);
|
|
855
|
+
if (nestedVariants.length === 0) {
|
|
856
|
+
writer.line(`sealed interface ${type.name}`);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const payloadFor = (payloadName) => {
|
|
860
|
+
const found = registry.all().find((candidate) => candidate.name === payloadName) ?? registry.all().find((candidate) => candidate.name.endsWith(payloadName));
|
|
861
|
+
return found && found.kind === "data-class" ? found : void 0;
|
|
862
|
+
};
|
|
863
|
+
const wrapped = nestedVariants.map((variant) => ({
|
|
864
|
+
variant,
|
|
865
|
+
payload: payloadFor(variant.payloadType),
|
|
866
|
+
inlined: (() => {
|
|
867
|
+
const candidate = registry.all().find((entry) => entry.name === variant.payloadType);
|
|
868
|
+
return !!candidate && candidate.kind === "data-class" && candidate.fields.length > 0;
|
|
869
|
+
})()
|
|
870
|
+
}));
|
|
871
|
+
const sharedFields = wrapped.length > 0 && wrapped.every(({ payload, inlined }) => payload !== void 0 && !inlined) ? wrapped[0].payload.fields.filter((field) => wrapped.every(({ payload }) => payload.fields.some((other) => other.name === field.name && other.type === field.type && other.optional === field.optional))) : [];
|
|
872
|
+
writer.block(`sealed interface ${type.name}`, () => {
|
|
873
|
+
for (const field of sharedFields) {
|
|
874
|
+
const typeExpression = optionalize(ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
875
|
+
writer.line(`val ${escapeKeyword(field.name)}: ${typeExpression}`);
|
|
876
|
+
}
|
|
877
|
+
if (sharedFields.length > 0) writer.blank();
|
|
878
|
+
for (const variant of nestedVariants) {
|
|
879
|
+
const payloadType = registry.all().find((candidate) => candidate.name === variant.payloadType);
|
|
880
|
+
writer.line(`@SerialName(${stringLiteral(variant.literal)})`);
|
|
881
|
+
writer.line("@Serializable");
|
|
882
|
+
const ownsNestedTypes = Array.from(ownedTypeMap.values()).includes(variant.payloadType);
|
|
883
|
+
const emitVariantBody = () => {
|
|
884
|
+
emitOwnedTypes(writer, variant.payloadType, ownedTypeMap, ownedTypeLookup, registry);
|
|
885
|
+
};
|
|
886
|
+
if (payloadType && payloadType.kind === "data-class" && payloadType.fields.length > 0) {
|
|
887
|
+
const fields = payloadType.fields.filter((field) => field.wireName !== type.discriminator);
|
|
888
|
+
if (fields.length === 0) writer.line(`data object ${variant.caseName} : ${type.name}`);
|
|
889
|
+
else {
|
|
890
|
+
const params = fields.map((field) => {
|
|
891
|
+
const variantPath = registry.sealedVariantPath(variant.payloadType) ?? variant.payloadType;
|
|
892
|
+
const typeExpression = optionalize(relativeOwnedPath(field.type, ownedTypeMap, variantPath, registry) ?? ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
893
|
+
const defaultPart = field.optional ? " = null" : "";
|
|
894
|
+
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
895
|
+
});
|
|
896
|
+
if (params.length === 1) writer.line(`data class ${variant.caseName}(${params[0]}) : ${type.name}`);
|
|
897
|
+
else {
|
|
898
|
+
writer.line(`data class ${variant.caseName}(`);
|
|
899
|
+
writer.indent(() => {
|
|
900
|
+
for (let index = 0; index < params.length; index += 1) {
|
|
901
|
+
const separator = index < params.length - 1 ? "," : "";
|
|
902
|
+
writer.line(`${params[index]}${separator}`);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
writer.line(`) : ${type.name}`);
|
|
906
|
+
}
|
|
907
|
+
if (ownsNestedTypes) {
|
|
908
|
+
writer.appendToLastLine(" {");
|
|
909
|
+
writer.indent(emitVariantBody);
|
|
910
|
+
writer.line("}");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} else if (sharedFields.length > 0) writer.block(`data class ${variant.caseName}(val value: ${ownedTypePath(variant.payloadType, ownedTypeMap, registry)}) : ${type.name}`, () => {
|
|
914
|
+
for (const field of sharedFields) {
|
|
915
|
+
const typeExpression = optionalize(ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
916
|
+
writer.line(`override val ${escapeKeyword(field.name)}: ${typeExpression} get() = value.${escapeKeyword(field.name)}`);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
else writer.line(`data class ${variant.caseName}(val value: ${ownedTypePath(variant.payloadType, ownedTypeMap, registry)}) : ${type.name}`);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
};
|
|
923
|
+
const emitType = (writer, type, ownedTypeMap = /* @__PURE__ */ new Map(), ownedTypeLookup = /* @__PURE__ */ new Map(), registryName, registry) => {
|
|
924
|
+
writer.blank();
|
|
925
|
+
writer.docComment(type.description);
|
|
926
|
+
if (type.kind === "data-class") emitDataClass(writer, type, ownedTypeMap, ownedTypeLookup, registryName);
|
|
927
|
+
else if (type.kind === "enum-class") emitEnumClass(writer, type.name, type.cases, type.unknownCase, type.description);
|
|
928
|
+
else if (registry) emitSealedClass(writer, type, registry, ownedTypeMap, ownedTypeLookup);
|
|
929
|
+
};
|
|
930
|
+
const emitTypes = (writer, types, ownedTypeMap = /* @__PURE__ */ new Map(), ownedTypeLookup = /* @__PURE__ */ new Map(), registry) => {
|
|
931
|
+
for (const type of types) emitType(writer, type, ownedTypeMap, ownedTypeLookup, void 0, registry);
|
|
932
|
+
};
|
|
933
|
+
const emitConstructorClass = (writer, declaration, params) => {
|
|
934
|
+
if (params.length === 1) writer.line(`${declaration}(${params[0]})`);
|
|
935
|
+
else {
|
|
936
|
+
writer.line(`${declaration}(`);
|
|
937
|
+
writer.indent(() => {
|
|
938
|
+
for (let index = 0; index < params.length; index += 1) {
|
|
939
|
+
const separator = index < params.length - 1 ? "," : "";
|
|
940
|
+
writer.line(`${params[index]}${separator}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
writer.line(")");
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const emitByteArrayEquality = (writer, typeName, fields) => {
|
|
947
|
+
const equalsTerms = fields.map((field) => {
|
|
948
|
+
const name = escapeKeyword(field.name);
|
|
949
|
+
return field.isByteArray ? `${name}.contentEquals(other.${name})` : `${name} == other.${name}`;
|
|
950
|
+
});
|
|
951
|
+
writer.line(`override fun equals(other: Any?) = other is ${typeName} && ${equalsTerms.join(" && ")}`);
|
|
952
|
+
const hashExpression = fields.reduce((accumulator, field, index) => {
|
|
953
|
+
const name = escapeKeyword(field.name);
|
|
954
|
+
const term = field.isByteArray ? `${name}.contentHashCode()` : `${name}.hashCode()`;
|
|
955
|
+
if (index === 0) return term;
|
|
956
|
+
return `31 * ${index === 1 ? accumulator : `(${accumulator})`} + ${term}`;
|
|
957
|
+
}, "");
|
|
958
|
+
writer.line(`override fun hashCode() = ${hashExpression}`);
|
|
959
|
+
};
|
|
960
|
+
const isVoidSuccessMethod = (method) => method.successReturnType === "Unit";
|
|
961
|
+
/**
|
|
962
|
+
* Emit the per-operation result types for the throw-on-error model:
|
|
963
|
+
*
|
|
964
|
+
* - `Result`: returned on success, exposing `body` (and `headers`). Omitted for
|
|
965
|
+
* void-success routes, which return `Unit`.
|
|
966
|
+
* - `Success`: a sealed sum of the success statuses, used as `Result.body` when a
|
|
967
|
+
* route has more than one success status.
|
|
968
|
+
* - `Failure`: a sealed `Exception` thrown for declared error statuses, decode
|
|
969
|
+
* failures, and any unexpected status.
|
|
970
|
+
*/
|
|
971
|
+
const emitOperationResultTypes = (writer, method, context) => {
|
|
972
|
+
const hasHeaders = method.resultHeaderFields.length > 0;
|
|
973
|
+
const isMultiSuccess = method.successResponses.length > 1;
|
|
974
|
+
const isVoidSuccess = isVoidSuccessMethod(method);
|
|
975
|
+
if (isMultiSuccess) {
|
|
976
|
+
writer.blank();
|
|
977
|
+
writer.block("sealed interface Success", () => {
|
|
978
|
+
for (const successResponse of method.successResponses) {
|
|
979
|
+
const resolved = resolveType(successResponse.type, method.operationName, context, "operation-object");
|
|
980
|
+
writer.line(`data class Status${successResponse.status}(val body: ${resolved}) : Success`);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const streamEvents = method.stream?.events;
|
|
985
|
+
if (streamEvents) {
|
|
986
|
+
const eventNames = new Set(streamEvents.map((event) => event.name));
|
|
987
|
+
if (eventNames.has("tool_call") && eventNames.has("tool_result")) {
|
|
988
|
+
writer.blank();
|
|
989
|
+
writer.line("/** One tool call, with its result once it answered. */");
|
|
990
|
+
writer.line("data class ToolCallRecord(");
|
|
991
|
+
writer.indent(() => {
|
|
992
|
+
writer.line("val id: String,");
|
|
993
|
+
writer.line("val name: String,");
|
|
994
|
+
writer.line("val state: State = State.Running,");
|
|
995
|
+
writer.line("/** The call as it arrived, to read its input. */");
|
|
996
|
+
writer.line("val call: ToolCall? = null,");
|
|
997
|
+
writer.line("/** The result once it answered, to read its output. */");
|
|
998
|
+
writer.line("val result: ToolResult? = null,");
|
|
999
|
+
writer.line("/** What the tool reported when it failed. */");
|
|
1000
|
+
writer.line("val message: String? = null,");
|
|
1001
|
+
});
|
|
1002
|
+
writer.line(") {");
|
|
1003
|
+
writer.indent(() => {
|
|
1004
|
+
writer.line("/** How far along the call is. */");
|
|
1005
|
+
writer.line("enum class State { Running, Done, Failed }");
|
|
1006
|
+
});
|
|
1007
|
+
writer.line("}");
|
|
1008
|
+
writer.blank();
|
|
1009
|
+
writer.line("/** Fold a stream's events into one row per tool call, in the order the calls arrived. */");
|
|
1010
|
+
writer.block("fun readToolCalls(events: List<Event>): List<ToolCallRecord>", () => {
|
|
1011
|
+
writer.line("val calls = LinkedHashMap<String, ToolCallRecord>()");
|
|
1012
|
+
writer.block("for (event in events)", () => {
|
|
1013
|
+
writer.line("when (event) {");
|
|
1014
|
+
writer.indent(() => {
|
|
1015
|
+
writer.line("is Event.ToolCall -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(call = event.data)");
|
|
1016
|
+
writer.line("is Event.ToolResult -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(state = ToolCallRecord.State.Done, result = event.data)");
|
|
1017
|
+
if (eventNames.has("tool_error")) writer.line("is Event.ToolError -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name.wireValue)).copy(state = ToolCallRecord.State.Failed, message = event.data.message)");
|
|
1018
|
+
writer.line("else -> Unit");
|
|
1019
|
+
});
|
|
1020
|
+
writer.line("}");
|
|
1021
|
+
});
|
|
1022
|
+
writer.line("return calls.values.toList()");
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
writer.blank();
|
|
1026
|
+
writer.block("sealed interface Event", () => {
|
|
1027
|
+
for (const event of streamEvents) {
|
|
1028
|
+
const payloadType = resolveType(event.type, method.operationName, context);
|
|
1029
|
+
writer.line(`data class ${event.caseName}(val data: ${payloadType}) : Event`);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
if (!isVoidSuccess) {
|
|
1034
|
+
const bodyType = method.stream ? `Flow<${streamElementType(method, context, "operation-object")}>` : isMultiSuccess ? "Success" : resolveType(method.successReturnType, method.operationName, context, "operation-object");
|
|
1035
|
+
writer.blank();
|
|
1036
|
+
const params = [`val body: ${bodyType}`];
|
|
1037
|
+
if (hasHeaders) params.push("val headers: Headers");
|
|
1038
|
+
emitConstructorClass(writer, "data class Result", params);
|
|
1039
|
+
if (hasHeaders) {
|
|
1040
|
+
const headerParams = method.resultHeaderFields.map((field) => {
|
|
1041
|
+
const rawType = resolveType(field.type, method.operationName, context, "operation-object");
|
|
1042
|
+
return `val ${escapeKeyword(field.name)}: ${optionalize(rawType, field.optional)}`;
|
|
1043
|
+
});
|
|
1044
|
+
writer.appendToLastLine(" {");
|
|
1045
|
+
writer.indent(() => {
|
|
1046
|
+
writer.blank();
|
|
1047
|
+
emitConstructorClass(writer, "data class Headers", headerParams);
|
|
1048
|
+
});
|
|
1049
|
+
writer.line("}");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
writer.blank();
|
|
1053
|
+
writer.block("sealed class Failure(message: String? = null) : Exception(message)", () => {
|
|
1054
|
+
for (const errorCase of method.errorCases) {
|
|
1055
|
+
const caseName = (0, kizunajs_generator.toPascalCase)(errorCase.caseName);
|
|
1056
|
+
if (errorCase.type === "Unit") writer.line(`data object ${caseName} : Failure()`);
|
|
1057
|
+
else {
|
|
1058
|
+
const resolved = resolveType(errorCase.type, method.operationName, context, "operation-object");
|
|
1059
|
+
writer.line(`data class ${caseName}(val body: ${resolved}) : Failure()`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
writer.line("class Unexpected(val statusCode: Int, val data: ByteArray) : Failure(\"Unexpected status $statusCode\")");
|
|
1063
|
+
writer.line("class Decoding(override val cause: Throwable, val statusCode: Int, val data: ByteArray) : Failure(cause.message)");
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
const streamElementType = (method, context, scope) => {
|
|
1067
|
+
const stream = method.stream;
|
|
1068
|
+
if (stream.mode === "text") return "String";
|
|
1069
|
+
if (stream.mode === "binary") return "ByteArray";
|
|
1070
|
+
if (stream.events) return scope === "operation-object" ? "Event" : `${context.clientName}.${method.operationName}.Event`;
|
|
1071
|
+
return resolveType(stream.messageType, method.operationName, context, scope);
|
|
1072
|
+
};
|
|
1073
|
+
const groupTypeRef = (operationName, className, context) => `${context.clientName}.${operationName}.${className}`;
|
|
1074
|
+
const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
|
|
1075
|
+
const groupClassField = (field) => {
|
|
1076
|
+
const typeExpression = optionalize(field.type, field.optional);
|
|
1077
|
+
const defaultPart = field.optional ? " = null" : "";
|
|
1078
|
+
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1079
|
+
};
|
|
1080
|
+
const emitGroupClass = (writer, className, fields, method, context) => {
|
|
1081
|
+
const resolved = fields.map((field) => ({
|
|
1082
|
+
...field,
|
|
1083
|
+
type: resolveType(field.type, method.operationName, context, "operation-object")
|
|
1084
|
+
}));
|
|
1085
|
+
writer.blank();
|
|
1086
|
+
emitConstructorClass(writer, `data class ${className}`, resolved.map((field) => groupClassField(field)));
|
|
1087
|
+
};
|
|
1088
|
+
const emitBodyGroupClass = (writer, method, context) => {
|
|
1089
|
+
const body = method.body;
|
|
1090
|
+
if (!body || body.kind === "json-empty") return;
|
|
1091
|
+
const operationName = method.operationName;
|
|
1092
|
+
if (body.kind === "json-struct" || body.kind === "union") {
|
|
1093
|
+
const payloadType = resolveType(body.structName, operationName, context, "operation-object");
|
|
1094
|
+
writer.blank();
|
|
1095
|
+
emitConstructorClass(writer, "data class Body", [`val payload: ${payloadType}`]);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const resolved = body.flattened.map((field) => ({
|
|
1099
|
+
...field,
|
|
1100
|
+
type: field.type === "MultipartFile" ? `${context.clientName}.MultipartFile` : resolveType(field.type, operationName, context, "operation-object")
|
|
1101
|
+
}));
|
|
1102
|
+
writer.blank();
|
|
1103
|
+
emitConstructorClass(writer, "data class Body", resolved.map((field) => groupClassField(field)));
|
|
1104
|
+
};
|
|
1105
|
+
const emitRequestGroupClasses = (writer, method, context) => {
|
|
1106
|
+
if (method.pathParams.length > 0) emitGroupClass(writer, "Params", pathParamFields(method), method, context);
|
|
1107
|
+
if (method.query.length > 0) emitGroupClass(writer, "Query", method.query, method, context);
|
|
1108
|
+
if (method.headers.length > 0) emitGroupClass(writer, "Headers", method.headers, method, context);
|
|
1109
|
+
emitBodyGroupClass(writer, method, context);
|
|
1110
|
+
emitArgsScaffolding(writer, method, context);
|
|
1111
|
+
};
|
|
1112
|
+
const pathParamFields = (method) => method.pathParams.map((name) => {
|
|
1113
|
+
const declared = method.declaredPathParams.find((field) => field.wireName === name);
|
|
1114
|
+
return declared ? {
|
|
1115
|
+
...declared,
|
|
1116
|
+
optional: false
|
|
1117
|
+
} : {
|
|
1118
|
+
name,
|
|
1119
|
+
wireName: name,
|
|
1120
|
+
type: "String",
|
|
1121
|
+
optional: false
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1124
|
+
const hasBodyGroup = (method) => method.body !== void 0 && method.body.kind !== "json-empty";
|
|
1125
|
+
const methodChannels = (method) => {
|
|
1126
|
+
const channels = [];
|
|
1127
|
+
if (method.pathParams.length > 0) channels.push({
|
|
1128
|
+
varName: "params",
|
|
1129
|
+
className: "Params",
|
|
1130
|
+
required: true,
|
|
1131
|
+
fields: pathParamFields(method)
|
|
1132
|
+
});
|
|
1133
|
+
if (method.query.length > 0) channels.push({
|
|
1134
|
+
varName: "query",
|
|
1135
|
+
className: "Query",
|
|
1136
|
+
required: method.query.some((field) => !field.optional),
|
|
1137
|
+
fields: method.query
|
|
1138
|
+
});
|
|
1139
|
+
if (method.headers.length > 0) channels.push({
|
|
1140
|
+
varName: "headers",
|
|
1141
|
+
className: "Headers",
|
|
1142
|
+
required: method.headers.some((field) => !field.optional),
|
|
1143
|
+
fields: method.headers
|
|
1144
|
+
});
|
|
1145
|
+
if (hasBodyGroup(method)) {
|
|
1146
|
+
const body = method.body;
|
|
1147
|
+
const fields = body.kind === "json-struct" || body.kind === "union" ? [{
|
|
1148
|
+
name: "payload",
|
|
1149
|
+
wireName: "payload",
|
|
1150
|
+
type: body.structName,
|
|
1151
|
+
optional: false
|
|
1152
|
+
}] : body.flattened;
|
|
1153
|
+
channels.push({
|
|
1154
|
+
varName: "body",
|
|
1155
|
+
className: "Body",
|
|
1156
|
+
required: true,
|
|
1157
|
+
fields
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return channels;
|
|
1161
|
+
};
|
|
1162
|
+
const resolveChannelFieldType = (field, method, context) => field.type === "MultipartFile" ? `${context.clientName}.MultipartFile` : resolveType(field.type, method.operationName, context, "operation-object");
|
|
1163
|
+
/**
|
|
1164
|
+
* Emit the call-shape scaffolding for a method's request channels:
|
|
1165
|
+
*
|
|
1166
|
+
* - `Args`: the sealed result of the request-builder lambda, holding one value per channel
|
|
1167
|
+
* (nullable when the channel is all-optional).
|
|
1168
|
+
* - `Scope`: the lambda receiver exposing a factory function per channel
|
|
1169
|
+
* (`params(id = "1")`), so call sites need no type names and completion lists the channels.
|
|
1170
|
+
* - `After<Channel>`: typestate steps returned by each factory; later channels chain off them
|
|
1171
|
+
* (`params(...).headers(...)`). Only steps whose remaining channels are all optional implement
|
|
1172
|
+
* `Args`, so omitting a required channel is a compile error, not a runtime one.
|
|
1173
|
+
*/
|
|
1174
|
+
const emitArgsScaffolding = (writer, method, context) => {
|
|
1175
|
+
const channels = methodChannels(method).map((channel) => ({
|
|
1176
|
+
...channel,
|
|
1177
|
+
fields: channel.fields.map((field) => ({
|
|
1178
|
+
...field,
|
|
1179
|
+
type: resolveChannelFieldType(field, method, context)
|
|
1180
|
+
}))
|
|
1181
|
+
}));
|
|
1182
|
+
if (channels.length === 0) return;
|
|
1183
|
+
const isTerminal = (lastDecidedIndex) => channels.slice(lastDecidedIndex + 1).every((channel) => !channel.required);
|
|
1184
|
+
const transitionsFrom = (lastDecidedIndex) => {
|
|
1185
|
+
const targets = [];
|
|
1186
|
+
for (let target = lastDecidedIndex + 1; target < channels.length; target += 1) if (channels.slice(lastDecidedIndex + 1, target).every((channel) => !channel.required)) targets.push(target);
|
|
1187
|
+
return targets;
|
|
1188
|
+
};
|
|
1189
|
+
const factorySignature = (channel) => channel.fields.map((field) => {
|
|
1190
|
+
const typeExpression = optionalize(field.type, field.optional);
|
|
1191
|
+
const defaultPart = field.optional ? " = null" : "";
|
|
1192
|
+
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1193
|
+
}).join(", ");
|
|
1194
|
+
const factoryConstruction = (channel) => {
|
|
1195
|
+
const args = channel.fields.map((field) => `${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`).join(", ");
|
|
1196
|
+
return `${channel.className}(${args})`;
|
|
1197
|
+
};
|
|
1198
|
+
const emitTransition = (fromIndex, toIndex) => {
|
|
1199
|
+
const target = channels[toIndex];
|
|
1200
|
+
const constructorArgs = [];
|
|
1201
|
+
for (let index = 0; index <= toIndex; index += 1) {
|
|
1202
|
+
const channel = channels[index];
|
|
1203
|
+
if (index < toIndex) constructorArgs.push(`${channel.varName} = ${index <= fromIndex ? channel.varName : "null"}`);
|
|
1204
|
+
else constructorArgs.push(`${channel.varName} = ${factoryConstruction(channel)}`);
|
|
1205
|
+
}
|
|
1206
|
+
writer.line(`fun ${escapeKeyword(target.varName)}(${factorySignature(target)}): After${target.className} = After${target.className}(${constructorArgs.join(", ")})`);
|
|
1207
|
+
};
|
|
1208
|
+
writer.blank();
|
|
1209
|
+
writer.block("sealed interface Args", () => {
|
|
1210
|
+
for (const channel of channels) writer.line(`val ${channel.varName}: ${channel.className}${channel.required ? "" : "?"}`);
|
|
1211
|
+
});
|
|
1212
|
+
writer.blank();
|
|
1213
|
+
writer.block("object Scope", () => {
|
|
1214
|
+
for (const target of transitionsFrom(-1)) emitTransition(-1, target);
|
|
1215
|
+
});
|
|
1216
|
+
for (let stateIndex = 0; stateIndex < channels.length; stateIndex += 1) {
|
|
1217
|
+
const stateChannel = channels[stateIndex];
|
|
1218
|
+
const terminal = isTerminal(stateIndex);
|
|
1219
|
+
const constructorParams = [];
|
|
1220
|
+
for (let index = 0; index <= stateIndex; index += 1) {
|
|
1221
|
+
const channel = channels[index];
|
|
1222
|
+
const modifier = terminal ? "override val" : "internal val";
|
|
1223
|
+
constructorParams.push(`${modifier} ${channel.varName}: ${channel.className}${channel.required ? "" : "?"}`);
|
|
1224
|
+
}
|
|
1225
|
+
const declaration = `class After${stateChannel.className} internal constructor(${constructorParams.join(", ")})${terminal ? " : Args" : ""}`;
|
|
1226
|
+
const undecidedOverrides = terminal ? channels.slice(stateIndex + 1) : [];
|
|
1227
|
+
const transitions = transitionsFrom(stateIndex);
|
|
1228
|
+
writer.blank();
|
|
1229
|
+
if (undecidedOverrides.length === 0 && transitions.length === 0) writer.line(declaration);
|
|
1230
|
+
else writer.block(declaration, () => {
|
|
1231
|
+
for (const channel of undecidedOverrides) writer.line(`override val ${channel.varName}: ${channel.className}? get() = null`);
|
|
1232
|
+
for (const target of transitions) emitTransition(stateIndex, target);
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
const buildMethodParameters = (method, context) => {
|
|
1237
|
+
const channels = methodChannels(method);
|
|
1238
|
+
if (channels.length === 0) return [];
|
|
1239
|
+
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1240
|
+
return [`build: ${operationRef}.Scope.() -> ${operationRef}.Args${channels.every((channel) => !channel.required) ? ` = { ${escapeKeyword(channels[0].varName)}() }` : ""}`];
|
|
1241
|
+
};
|
|
1242
|
+
const emitBodyEncoding = (writer, method, context) => {
|
|
1243
|
+
if (!method.body) return;
|
|
1244
|
+
const body = method.body;
|
|
1245
|
+
if (body.kind === "multipart") {
|
|
1246
|
+
writer.line("val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)");
|
|
1247
|
+
for (const field of body.multipartFields) {
|
|
1248
|
+
const accessor = groupMemberAccessor("body", field);
|
|
1249
|
+
const isFlattenedFile = body.flattened.find((flatField) => flatField.name === field.name)?.isFile === true;
|
|
1250
|
+
if (field.isFile || isFlattenedFile) writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.filename, ${accessor}.data.toRequestBody(${accessor}.mimeType.toMediaType()))`);
|
|
1251
|
+
else writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.toString())`);
|
|
1252
|
+
}
|
|
1253
|
+
writer.line("requestBody = multipartBuilder.build()");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (body.kind === "json-empty") {
|
|
1257
|
+
writer.line("requestBody = \"{}\".toRequestBody(\"application/json\".toMediaType())");
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (body.kind === "json-struct" || body.kind === "union") {
|
|
1261
|
+
writer.line("requestBody = json.encodeToString(body.payload).toRequestBody(\"application/json\".toMediaType())");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (body.kind === "json-flat" && body.structName) {
|
|
1265
|
+
const resolved = resolveType(body.structName, method.operationName, context);
|
|
1266
|
+
const args = body.flattened.map((field) => `${field.name} = ${groupMemberAccessor("body", field)}`).join(", ");
|
|
1267
|
+
writer.line(`val payload = ${resolved}(${args})`);
|
|
1268
|
+
writer.line("requestBody = json.encodeToString(payload).toRequestBody(\"application/json\".toMediaType())");
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const emitMethodBody = (writer, method, context) => {
|
|
1272
|
+
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1273
|
+
const failureRef = `${operationRef}.Failure`;
|
|
1274
|
+
const hasBody = method.body !== void 0;
|
|
1275
|
+
const isMultiSuccess = method.successResponses.length > 1;
|
|
1276
|
+
const isVoidSuccess = isVoidSuccessMethod(method);
|
|
1277
|
+
const hasHeaders = method.resultHeaderFields.length > 0;
|
|
1278
|
+
const channels = methodChannels(method);
|
|
1279
|
+
if (channels.length > 0) {
|
|
1280
|
+
writer.line(`val args = ${operationRef}.Scope.build()`);
|
|
1281
|
+
for (const channel of channels) {
|
|
1282
|
+
const groupRef = groupTypeRef(method.operationName, channel.className, context);
|
|
1283
|
+
if (channel.required) writer.line(`val ${channel.varName} = args.${channel.varName}`);
|
|
1284
|
+
else writer.line(`val ${channel.varName} = args.${channel.varName} ?: ${groupRef}()`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
writer.line(`${method.pathParams.length > 0 ? "var" : "val"} path = ${stringLiteral(method.pathTemplate)}`);
|
|
1288
|
+
for (const field of pathParamFields(method)) {
|
|
1289
|
+
const accessor = groupMemberAccessor("params", field);
|
|
1290
|
+
writer.line(`path = path.replace(${stringLiteral(`:${field.name}`)}, Kizuna.encodePathSegment(${accessor}))`);
|
|
1291
|
+
}
|
|
1292
|
+
writer.line("val urlBuilder = Kizuna.resolveUrl(baseUrl, path)");
|
|
1293
|
+
if (method.query.length > 0) for (const field of method.query) {
|
|
1294
|
+
const accessor = groupMemberAccessor("query", field);
|
|
1295
|
+
const appendBlock = (sourceExpression) => {
|
|
1296
|
+
writer.block(`for (stringValue in Kizuna.stringifyQueryValue(${sourceExpression}))`, () => {
|
|
1297
|
+
writer.line(`urlBuilder.addQueryParameter(${stringLiteral(field.wireName)}, stringValue)`);
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
if (field.optional) writer.block(`if (${accessor} != null)`, () => {
|
|
1301
|
+
appendBlock(accessor);
|
|
1302
|
+
});
|
|
1303
|
+
else appendBlock(accessor);
|
|
1304
|
+
}
|
|
1305
|
+
if (hasBody) {
|
|
1306
|
+
writer.line("val requestBody: RequestBody");
|
|
1307
|
+
emitBodyEncoding(writer, method, context);
|
|
1308
|
+
}
|
|
1309
|
+
const builderReassigned = context.requestContextFields.length > 0 || method.headers.length > 0;
|
|
1310
|
+
writer.line(`${builderReassigned ? "var" : "val"} requestBuilder = Request.Builder()`);
|
|
1311
|
+
writer.line(" .url(urlBuilder.build())");
|
|
1312
|
+
if (hasBody) writer.line(` .method(${stringLiteral(method.method)}, requestBody)`);
|
|
1313
|
+
else if (METHODS_REQUIRING_BODY.has(method.method)) writer.line(` .method(${stringLiteral(method.method)}, ByteArray(0).toRequestBody(null))`);
|
|
1314
|
+
else writer.line(` .method(${stringLiteral(method.method)}, null)`);
|
|
1315
|
+
if (context.requestContextFields.length > 0) writer.line("for ((name, value) in requestContextHeaders) requestBuilder = requestBuilder.header(name, value)");
|
|
1316
|
+
for (const field of method.headers) {
|
|
1317
|
+
const accessor = groupMemberAccessor("headers", field);
|
|
1318
|
+
const setHeader = (sourceExpression) => {
|
|
1319
|
+
writer.line(`requestBuilder = requestBuilder.header(${stringLiteral(field.wireName)}, Kizuna.stringifyQueryValue(${sourceExpression}).joinToString(", "))`);
|
|
1320
|
+
};
|
|
1321
|
+
if (field.optional) writer.block(`if (${accessor} != null)`, () => {
|
|
1322
|
+
setHeader(accessor);
|
|
1323
|
+
});
|
|
1324
|
+
else setHeader(accessor);
|
|
1325
|
+
}
|
|
1326
|
+
writer.line("requestInterceptor?.invoke(requestBuilder)");
|
|
1327
|
+
writer.line("val httpResponse = Kizuna.execute(client, requestBuilder.build())");
|
|
1328
|
+
if (method.stream) {
|
|
1329
|
+
emitStreamMethodTail(writer, method, context);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
writer.line(`${isVoidSuccess ? "" : "return "}httpResponse.use {`);
|
|
1333
|
+
writer.indent(() => {
|
|
1334
|
+
writer.line("responseInterceptor?.invoke(requestBuilder.build(), httpResponse)");
|
|
1335
|
+
writer.line("val data = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { httpResponse.body?.bytes() ?: ByteArray(0) }");
|
|
1336
|
+
writer.line("when (val statusCode = httpResponse.code) {");
|
|
1337
|
+
writer.indent(() => {
|
|
1338
|
+
for (const successResponse of method.successResponses) {
|
|
1339
|
+
if (isVoidSuccess) {
|
|
1340
|
+
writer.line(`${successResponse.status} -> {}`);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
writer.line(`${successResponse.status} -> {`);
|
|
1344
|
+
writer.indent(() => {
|
|
1345
|
+
const resolved = resolveType(successResponse.type, method.operationName, context);
|
|
1346
|
+
writer.block("try", () => {
|
|
1347
|
+
writer.line(`val payload = json.decodeFromString<${resolved}>(data.decodeToString())`);
|
|
1348
|
+
const bodyExpr = isMultiSuccess ? `${operationRef}.Success.Status${successResponse.status}(payload)` : "payload";
|
|
1349
|
+
if (hasHeaders) {
|
|
1350
|
+
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
1351
|
+
const headersArgs = method.resultHeaderFields.map((field) => `${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`).join(", ");
|
|
1352
|
+
writer.line(`return@use ${operationRef}.Result(body = ${bodyExpr}, headers = ${operationRef}.Result.Headers(${headersArgs}))`);
|
|
1353
|
+
} else writer.line(`return@use ${operationRef}.Result(body = ${bodyExpr})`);
|
|
1354
|
+
});
|
|
1355
|
+
writer.line(`catch (error: Exception) { throw ${failureRef}.Decoding(error, statusCode, data) }`);
|
|
1356
|
+
});
|
|
1357
|
+
writer.line("}");
|
|
1358
|
+
}
|
|
1359
|
+
emitErrorBranches(writer, method, context);
|
|
1360
|
+
});
|
|
1361
|
+
writer.line("}");
|
|
1362
|
+
});
|
|
1363
|
+
writer.line("}");
|
|
1364
|
+
};
|
|
1365
|
+
const emitErrorBranches = (writer, method, context) => {
|
|
1366
|
+
const failureRef = `${`${context.clientName}.${method.operationName}`}.Failure`;
|
|
1367
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1368
|
+
for (const errorCase of method.errorCases) {
|
|
1369
|
+
const existing = grouped.get(errorCase.status);
|
|
1370
|
+
if (existing) existing.push(errorCase);
|
|
1371
|
+
else grouped.set(errorCase.status, [errorCase]);
|
|
1372
|
+
}
|
|
1373
|
+
for (const [status, cases] of grouped) {
|
|
1374
|
+
writer.line(`${status} -> {`);
|
|
1375
|
+
writer.indent(() => {
|
|
1376
|
+
if (cases.length === 1) {
|
|
1377
|
+
const errorCase = cases[0];
|
|
1378
|
+
const caseName = (0, kizunajs_generator.toPascalCase)(errorCase.caseName);
|
|
1379
|
+
if (errorCase.type === "Unit") writer.line(`throw ${failureRef}.${caseName}`);
|
|
1380
|
+
else {
|
|
1381
|
+
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1382
|
+
writer.line("val payload = try {");
|
|
1383
|
+
writer.indent(() => {
|
|
1384
|
+
writer.line(`json.decodeFromString<${resolved}>(data.decodeToString())`);
|
|
1385
|
+
});
|
|
1386
|
+
writer.line(`} catch (error: Exception) { throw ${failureRef}.Decoding(error, statusCode, data) }`);
|
|
1387
|
+
writer.line(`throw ${failureRef}.${caseName}(body = payload)`);
|
|
1388
|
+
}
|
|
1389
|
+
} else {
|
|
1390
|
+
for (const errorCase of cases) {
|
|
1391
|
+
const caseName = (0, kizunajs_generator.toPascalCase)(errorCase.caseName);
|
|
1392
|
+
if (errorCase.type === "Unit") writer.line(`throw ${failureRef}.${caseName}`);
|
|
1393
|
+
else {
|
|
1394
|
+
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1395
|
+
writer.line(`val ${errorCase.caseName} = try {`);
|
|
1396
|
+
writer.indent(() => {
|
|
1397
|
+
writer.line(`json.decodeFromString<${resolved}>(data.decodeToString())`);
|
|
1398
|
+
});
|
|
1399
|
+
writer.line("} catch (_: Exception) { null }");
|
|
1400
|
+
writer.line(`if (${errorCase.caseName} != null) throw ${failureRef}.${caseName}(body = ${errorCase.caseName})`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
writer.line(`throw ${failureRef}.Unexpected(statusCode = statusCode, data = data)`);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
writer.line("}");
|
|
1407
|
+
}
|
|
1408
|
+
writer.line(`else -> throw ${failureRef}.Unexpected(statusCode = statusCode, data = data)`);
|
|
1409
|
+
};
|
|
1410
|
+
const emitStreamMethodTail = (writer, method, context) => {
|
|
1411
|
+
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1412
|
+
const stream = method.stream;
|
|
1413
|
+
const successResponse = method.successResponses.find((candidate) => candidate.status === stream.status);
|
|
1414
|
+
const hasHeaders = method.resultHeaderFields.length > 0;
|
|
1415
|
+
writer.line("responseInterceptor?.invoke(requestBuilder.build(), httpResponse)");
|
|
1416
|
+
writer.block(`if (httpResponse.code == ${stream.status})`, () => {
|
|
1417
|
+
if (stream.mode === "text") writer.line("val body = Kizuna.lines(httpResponse)");
|
|
1418
|
+
else if (stream.mode === "binary") writer.line("val body = Kizuna.chunks(httpResponse)");
|
|
1419
|
+
else if (stream.events) {
|
|
1420
|
+
const events = stream.events;
|
|
1421
|
+
const elementType = streamElementType(method, context, "client");
|
|
1422
|
+
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse) { event ->`, () => {
|
|
1423
|
+
writer.block("when (event.event)", () => {
|
|
1424
|
+
for (const event of events) {
|
|
1425
|
+
const payloadType = resolveType(event.type, method.operationName, context);
|
|
1426
|
+
writer.line(`${stringLiteral(event.name)} -> ${elementType}.${event.caseName}(json.decodeFromString<${payloadType}>(event.data))`);
|
|
1427
|
+
}
|
|
1428
|
+
writer.line("else -> null");
|
|
1429
|
+
});
|
|
1430
|
+
});
|
|
1431
|
+
} else {
|
|
1432
|
+
const elementType = streamElementType(method, context, "client");
|
|
1433
|
+
writer.line(`val body = Kizuna.events<${elementType}>(httpResponse) { event -> json.decodeFromString<${elementType}>(event.data) }`);
|
|
1434
|
+
}
|
|
1435
|
+
if (hasHeaders) {
|
|
1436
|
+
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
1437
|
+
const headersArgs = method.resultHeaderFields.map((field) => `${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`).join(", ");
|
|
1438
|
+
writer.line(`return ${operationRef}.Result(body = body, headers = ${operationRef}.Result.Headers(${headersArgs}))`);
|
|
1439
|
+
} else writer.line(`return ${operationRef}.Result(body = body)`);
|
|
1440
|
+
});
|
|
1441
|
+
writer.line("return httpResponse.use {");
|
|
1442
|
+
writer.indent(() => {
|
|
1443
|
+
writer.line("val data = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { httpResponse.body?.bytes() ?: ByteArray(0) }");
|
|
1444
|
+
writer.line("when (val statusCode = httpResponse.code) {");
|
|
1445
|
+
writer.indent(() => {
|
|
1446
|
+
emitErrorBranches(writer, method, context);
|
|
1447
|
+
});
|
|
1448
|
+
writer.line("}");
|
|
1449
|
+
});
|
|
1450
|
+
writer.line("}");
|
|
1451
|
+
};
|
|
1452
|
+
const resultRef = (method, context) => `${context.clientName}.${method.operationName}.Result`;
|
|
1453
|
+
const buildMethodSignature = (method, context) => {
|
|
1454
|
+
const params = buildMethodParameters(method, context);
|
|
1455
|
+
const returnType = isVoidSuccessMethod(method) ? "" : `: ${resultRef(method, context)}`;
|
|
1456
|
+
return `suspend fun ${escapeKeyword(method.name)}(${params.join(", ")})${returnType}`;
|
|
1457
|
+
};
|
|
1458
|
+
const emitMethodDoc = (writer, method) => {
|
|
1459
|
+
writer.docComment(method.summary ?? method.description);
|
|
1460
|
+
};
|
|
1461
|
+
const emitMethod = (writer, method, context) => {
|
|
1462
|
+
emitMethodDoc(writer, method);
|
|
1463
|
+
if (method.deprecated) writer.line(deprecatedAnnotation(method.deprecationMessage));
|
|
1464
|
+
writer.line(`@Throws(${context.clientName}.${method.operationName}.Failure::class)`);
|
|
1465
|
+
writer.block(buildMethodSignature(method, context), () => {
|
|
1466
|
+
emitMethodBody(writer, method, context);
|
|
1467
|
+
});
|
|
1468
|
+
};
|
|
1469
|
+
const emitSubClientMethod = (writer, method, context) => {
|
|
1470
|
+
emitMethodDoc(writer, method);
|
|
1471
|
+
if (method.deprecated) writer.line(deprecatedAnnotation(method.deprecationMessage));
|
|
1472
|
+
writer.line(`@Throws(${context.clientName}.${method.operationName}.Failure::class)`);
|
|
1473
|
+
writer.block(buildMethodSignature(method, context), () => {
|
|
1474
|
+
emitMethodBody(writer, method, context);
|
|
1475
|
+
});
|
|
1476
|
+
};
|
|
1477
|
+
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1478
|
+
writer.blank();
|
|
1479
|
+
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?)`, () => {
|
|
1480
|
+
for (const method of group.methods) {
|
|
1481
|
+
writer.blank();
|
|
1482
|
+
emitSubClientMethod(writer, method, context);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
};
|
|
1486
|
+
const closure = (writer, opener, body) => {
|
|
1487
|
+
writer.line(opener);
|
|
1488
|
+
writer.indent(body);
|
|
1489
|
+
writer.line("}");
|
|
1490
|
+
};
|
|
1491
|
+
const emitKizunaStreaming = (writer) => {
|
|
1492
|
+
writer.blank();
|
|
1493
|
+
writer.line("data class ServerSentEvent(val event: String?, val data: String, val id: String?, val retry: Int?)");
|
|
1494
|
+
writer.blank();
|
|
1495
|
+
writer.block("private suspend fun readLine(source: okio.BufferedSource): String?", () => {
|
|
1496
|
+
writer.line("return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { source.readUtf8Line() }");
|
|
1497
|
+
});
|
|
1498
|
+
writer.blank();
|
|
1499
|
+
writer.block("fun lines(response: Response): Flow<String> = flow", () => {
|
|
1500
|
+
closure(writer, "response.use { open ->", () => {
|
|
1501
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1502
|
+
writer.block("while (true)", () => {
|
|
1503
|
+
writer.line("val line = readLine(source) ?: break");
|
|
1504
|
+
writer.line("emit(line)");
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
writer.blank();
|
|
1509
|
+
writer.block("fun chunks(response: Response): Flow<ByteArray> = flow", () => {
|
|
1510
|
+
closure(writer, "response.use { open ->", () => {
|
|
1511
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1512
|
+
writer.line("val buffer = ByteArray(16_384)");
|
|
1513
|
+
writer.block("while (true)", () => {
|
|
1514
|
+
writer.line("val read = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { source.read(buffer) }");
|
|
1515
|
+
writer.line("if (read == -1) break");
|
|
1516
|
+
writer.line("emit(buffer.copyOf(read))");
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
writer.blank();
|
|
1521
|
+
writer.block("fun serverSentEvents(response: Response): Flow<ServerSentEvent> = flow", () => {
|
|
1522
|
+
closure(writer, "response.use { open ->", () => {
|
|
1523
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1524
|
+
writer.line("var eventType = \"\"");
|
|
1525
|
+
writer.line("val dataLines = mutableListOf<String>()");
|
|
1526
|
+
writer.line("var lastEventId = \"\"");
|
|
1527
|
+
writer.line("var retry: Int? = null");
|
|
1528
|
+
writer.block("while (true)", () => {
|
|
1529
|
+
writer.line("val line = readLine(source) ?: break");
|
|
1530
|
+
writer.block("if (line.isEmpty())", () => {
|
|
1531
|
+
writer.block("if (dataLines.isNotEmpty())", () => {
|
|
1532
|
+
writer.line("emit(ServerSentEvent(eventType.ifEmpty { null }, dataLines.joinToString(\"\\n\"), lastEventId.ifEmpty { null }, retry))");
|
|
1533
|
+
writer.line("dataLines.clear()");
|
|
1534
|
+
writer.line("retry = null");
|
|
1535
|
+
});
|
|
1536
|
+
writer.line("eventType = \"\"");
|
|
1537
|
+
writer.line("continue");
|
|
1538
|
+
});
|
|
1539
|
+
writer.line("if (line.startsWith(\":\")) continue");
|
|
1540
|
+
writer.line("val separator = line.indexOf(':')");
|
|
1541
|
+
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1542
|
+
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1543
|
+
writer.block("when (field)", () => {
|
|
1544
|
+
writer.line("\"event\" -> eventType = value");
|
|
1545
|
+
writer.line("\"data\" -> dataLines.add(value)");
|
|
1546
|
+
writer.line("\"id\" -> if (!value.contains('\\u0000')) lastEventId = value");
|
|
1547
|
+
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { it.isDigit() }) retry = value.toIntOrNull()");
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
1552
|
+
writer.blank();
|
|
1553
|
+
writer.line("fun <Event : Any> events(response: Response, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response).mapNotNull(decode)");
|
|
1554
|
+
};
|
|
1555
|
+
const emitKizunaObject = (writer, options) => {
|
|
1556
|
+
writer.blank();
|
|
1557
|
+
writer.block("private object Kizuna", () => {
|
|
1558
|
+
if (options.streaming) emitKizunaStreaming(writer);
|
|
1559
|
+
writer.block("fun resolveUrl(baseUrl: String, path: String): HttpUrl.Builder", () => {
|
|
1560
|
+
writer.line("val base = baseUrl.toHttpUrl()");
|
|
1561
|
+
writer.line("val trimmedPath = path.trimStart('/')");
|
|
1562
|
+
writer.line("val basePath = base.encodedPath.trimEnd('/')");
|
|
1563
|
+
writer.line("val fullPath = if (basePath.isEmpty()) \"/$trimmedPath\" else \"$basePath/$trimmedPath\"");
|
|
1564
|
+
writer.line("return base.newBuilder().encodedPath(fullPath)");
|
|
1565
|
+
});
|
|
1566
|
+
writer.blank();
|
|
1567
|
+
writer.block("fun encodePathSegment(value: Any): String", () => {
|
|
1568
|
+
writer.line("val text = stringifyQueryValue(value).firstOrNull() ?: \"\"");
|
|
1569
|
+
writer.line("return java.net.URLEncoder.encode(text, \"UTF-8\").replace(\"+\", \"%20\")");
|
|
1570
|
+
});
|
|
1571
|
+
writer.blank();
|
|
1572
|
+
writer.block("fun stringifyQueryValue(value: Any): List<String>", () => {
|
|
1573
|
+
writer.line("return when (value) {");
|
|
1574
|
+
writer.indent(() => {
|
|
1575
|
+
writer.line("is Instant -> listOf(value.toString())");
|
|
1576
|
+
writer.line("is List<*> -> value.filterNotNull().flatMap { stringifyQueryValue(it) }");
|
|
1577
|
+
writer.line("is KizunaQueryValue -> listOf(value.wireValue)");
|
|
1578
|
+
writer.line("is Enum<*> -> listOf(value.name)");
|
|
1579
|
+
writer.line("else -> listOf(value.toString())");
|
|
1580
|
+
});
|
|
1581
|
+
writer.line("}");
|
|
1582
|
+
});
|
|
1583
|
+
writer.blank();
|
|
1584
|
+
writer.block("suspend fun execute(client: OkHttpClient, request: Request): Response", () => {
|
|
1585
|
+
writer.line("return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->");
|
|
1586
|
+
writer.indent(() => {
|
|
1587
|
+
writer.line("val call = client.newCall(request)");
|
|
1588
|
+
writer.block("continuation.invokeOnCancellation", () => {
|
|
1589
|
+
writer.line("call.cancel()");
|
|
1590
|
+
});
|
|
1591
|
+
writer.block("call.enqueue(object : Callback", () => {
|
|
1592
|
+
writer.block("override fun onFailure(call: Call, e: java.io.IOException)", () => {
|
|
1593
|
+
writer.line("continuation.resumeWith(Result.failure(e))");
|
|
1594
|
+
});
|
|
1595
|
+
writer.block("override fun onResponse(call: Call, response: Response)", () => {
|
|
1596
|
+
writer.line("continuation.resumeWith(Result.success(response))");
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
writer.line(")");
|
|
1600
|
+
});
|
|
1601
|
+
writer.line("}");
|
|
1602
|
+
});
|
|
1603
|
+
});
|
|
1604
|
+
};
|
|
1605
|
+
const emitClient = (writer, config, partition, context, typesByOperation, registry) => {
|
|
1606
|
+
const { clientName } = config;
|
|
1607
|
+
const { flatMethods, groups } = partition;
|
|
1608
|
+
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1609
|
+
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1610
|
+
const contextFields = context.requestContextFields;
|
|
1611
|
+
const contextRequired = contextFields.some((field) => !field.optional);
|
|
1612
|
+
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1613
|
+
writer.blank();
|
|
1614
|
+
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null)`, () => {
|
|
1615
|
+
if (contextFields.length > 0) {
|
|
1616
|
+
writer.blank();
|
|
1617
|
+
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
1618
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${field.optional ? " = null" : ""}`));
|
|
1619
|
+
writer.blank();
|
|
1620
|
+
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1621
|
+
for (const field of contextFields) if (field.optional) writer.line(`if (requestContext.${escapeKeyword(field.name)} != null) put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)}!!)`);
|
|
1622
|
+
else writer.line(`put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)})`);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
if (usesMultipart) {
|
|
1626
|
+
writer.blank();
|
|
1627
|
+
emitConstructorClass(writer, "data class MultipartFile", [
|
|
1628
|
+
"val data: ByteArray",
|
|
1629
|
+
"val filename: String",
|
|
1630
|
+
"val mimeType: String = \"application/octet-stream\""
|
|
1631
|
+
]);
|
|
1632
|
+
writer.appendToLastLine(" {");
|
|
1633
|
+
writer.indent(() => {
|
|
1634
|
+
emitByteArrayEquality(writer, "MultipartFile", [
|
|
1635
|
+
{
|
|
1636
|
+
name: "data",
|
|
1637
|
+
isByteArray: true
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
name: "filename",
|
|
1641
|
+
isByteArray: false
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "mimeType",
|
|
1645
|
+
isByteArray: false
|
|
1646
|
+
}
|
|
1647
|
+
]);
|
|
1648
|
+
});
|
|
1649
|
+
writer.line("}");
|
|
1650
|
+
}
|
|
1651
|
+
if (allMethods.some((method) => method.errorCases.some((candidate) => candidate.type === "ValidationError"))) {
|
|
1652
|
+
writer.blank();
|
|
1653
|
+
writer.line("@Serializable");
|
|
1654
|
+
emitConstructorClass(writer, "data class ValidationError", [
|
|
1655
|
+
"val type: String",
|
|
1656
|
+
"val title: String",
|
|
1657
|
+
"val status: Int",
|
|
1658
|
+
"val detail: String",
|
|
1659
|
+
"val errors: List<ValidationIssue>"
|
|
1660
|
+
]);
|
|
1661
|
+
writer.blank();
|
|
1662
|
+
writer.line("@Serializable");
|
|
1663
|
+
emitConstructorClass(writer, "data class ValidationIssue", [
|
|
1664
|
+
"val code: String",
|
|
1665
|
+
"val path: List<String>",
|
|
1666
|
+
"val message: String"
|
|
1667
|
+
]);
|
|
1668
|
+
}
|
|
1669
|
+
for (const method of allMethods) {
|
|
1670
|
+
const localTypes = (typesByOperation.get(method.operationName) ?? []).map((type) => localizeType(type, method.operationName, context));
|
|
1671
|
+
writer.blank();
|
|
1672
|
+
writer.block(`object ${method.operationName}`, () => {
|
|
1673
|
+
emitTypes(writer, localTypes, void 0, void 0, registry);
|
|
1674
|
+
emitRequestGroupClasses(writer, method, context);
|
|
1675
|
+
emitOperationResultTypes(writer, method, context);
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
if (groups.length > 0) for (const group of groups) {
|
|
1679
|
+
writer.blank();
|
|
1680
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor)`);
|
|
1681
|
+
}
|
|
1682
|
+
for (const method of flatMethods) {
|
|
1683
|
+
writer.blank();
|
|
1684
|
+
emitMethod(writer, method, context);
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
};
|
|
1688
|
+
/**
|
|
1689
|
+
* Generate a Kotlin API client from a Kizuna api.
|
|
1690
|
+
*
|
|
1691
|
+
* @param api - What `defineConfig` assembled.
|
|
1692
|
+
* @param config - Override the generated names:
|
|
1693
|
+
* - `namespaceName`: the object wrapping shared types.
|
|
1694
|
+
* - `packageName`: optional package declaration for the generated file.
|
|
1695
|
+
*/
|
|
1696
|
+
const generateKotlinClient = (api, config) => {
|
|
1697
|
+
const { namespaceName, packageName, camelCaseProperties = false, unknownEnumCase = false } = config;
|
|
1698
|
+
const registry = new TypeRegistry(camelCaseProperties, unknownEnumCase);
|
|
1699
|
+
const partition = kotlinGenerator(api, {
|
|
1700
|
+
namespaceName,
|
|
1701
|
+
registry
|
|
1702
|
+
});
|
|
1703
|
+
const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
|
|
1704
|
+
const operationTypeMap = buildOperationTypeMap(allMethods, registry);
|
|
1705
|
+
const clientName = `${namespaceName}Client`;
|
|
1706
|
+
const writer = new KotlinWriter();
|
|
1707
|
+
writer.line("// Generated by @kizunajs/kotlin. Do not edit by hand.");
|
|
1708
|
+
writer.blank();
|
|
1709
|
+
const suppressions = [
|
|
1710
|
+
...camelCaseProperties ? [] : [
|
|
1711
|
+
"PropertyName",
|
|
1712
|
+
"LocalVariableName",
|
|
1713
|
+
"ConstructorParameterNaming"
|
|
1714
|
+
],
|
|
1715
|
+
"SpellCheckingInspection",
|
|
1716
|
+
"unused",
|
|
1717
|
+
"RedundantVisibilityModifier",
|
|
1718
|
+
"RedundantUnitReturnType"
|
|
1719
|
+
];
|
|
1720
|
+
writer.line(`@file:Suppress(${suppressions.map((name) => `"${name}"`).join(", ")})`);
|
|
1721
|
+
writer.blank();
|
|
1722
|
+
if (packageName) {
|
|
1723
|
+
writer.line(`package ${packageName}`);
|
|
1724
|
+
writer.blank();
|
|
1725
|
+
}
|
|
1726
|
+
writer.line("import kotlinx.serialization.*");
|
|
1727
|
+
writer.line("import kotlinx.serialization.json.*");
|
|
1728
|
+
if (registry.all().some((type) => type.kind === "enum-class" && type.unknownCase)) {
|
|
1729
|
+
writer.line("import kotlinx.serialization.descriptors.*");
|
|
1730
|
+
writer.line("import kotlinx.serialization.encoding.*");
|
|
1731
|
+
}
|
|
1732
|
+
writer.line("import kotlinx.datetime.Instant");
|
|
1733
|
+
const usesStreaming = allMethods.some((method) => method.stream !== void 0);
|
|
1734
|
+
if (usesStreaming) writer.line("import kotlinx.coroutines.flow.*");
|
|
1735
|
+
writer.line("import okhttp3.*");
|
|
1736
|
+
writer.line("import okhttp3.MediaType.Companion.toMediaType");
|
|
1737
|
+
writer.line("import okhttp3.RequestBody.Companion.toRequestBody");
|
|
1738
|
+
writer.line("import okhttp3.HttpUrl.Companion.toHttpUrl");
|
|
1739
|
+
writer.blank();
|
|
1740
|
+
if (registry.all().some((type) => type.kind === "enum-class")) {
|
|
1741
|
+
writer.block("interface KizunaQueryValue", () => {
|
|
1742
|
+
writer.line("val wireValue: String");
|
|
1743
|
+
});
|
|
1744
|
+
writer.blank();
|
|
1745
|
+
}
|
|
1746
|
+
const sharedTypes = [];
|
|
1747
|
+
const typesByOperation = /* @__PURE__ */ new Map();
|
|
1748
|
+
for (const type of registry.all()) {
|
|
1749
|
+
const owningOp = operationTypeMap.get(type.name);
|
|
1750
|
+
if (owningOp !== void 0) {
|
|
1751
|
+
const bucket = typesByOperation.get(owningOp) ?? [];
|
|
1752
|
+
if (bucket.length === 0) typesByOperation.set(owningOp, bucket);
|
|
1753
|
+
bucket.push(type);
|
|
1754
|
+
} else sharedTypes.push(type);
|
|
1755
|
+
}
|
|
1756
|
+
const fileLevelTypeNames = /* @__PURE__ */ new Set();
|
|
1757
|
+
const allClassNames = sharedTypes.filter((type) => type.kind === "data-class").map((type) => type.name);
|
|
1758
|
+
const ownedTypeMap = /* @__PURE__ */ new Map();
|
|
1759
|
+
for (const type of sharedTypes) {
|
|
1760
|
+
if (registry.isExplicitId(type.name)) continue;
|
|
1761
|
+
if (registry.isSealedVariantPayload(type.name)) continue;
|
|
1762
|
+
let bestMatch;
|
|
1763
|
+
for (const className of allClassNames) {
|
|
1764
|
+
if (className === type.name) continue;
|
|
1765
|
+
if ((0, kizunajs_generator.isHintPrefix)(type.name, className) && (!bestMatch || className.length > bestMatch.length)) bestMatch = className;
|
|
1766
|
+
}
|
|
1767
|
+
if (bestMatch !== void 0) ownedTypeMap.set(type.name, bestMatch);
|
|
1768
|
+
}
|
|
1769
|
+
const ownedTypeLookup = new Map(sharedTypes.filter((type) => ownedTypeMap.has(type.name)).map((type) => [type.name, type]));
|
|
1770
|
+
const topLevelSharedTypes = sharedTypes.filter((type) => !ownedTypeMap.has(type.name) && !registry.isSealedVariantPayload(type.name));
|
|
1771
|
+
const requestContextFields = [];
|
|
1772
|
+
for (const declaration of Object.values(api.requestContext ?? {})) {
|
|
1773
|
+
const headersSchema = declaration.headers;
|
|
1774
|
+
if (!headersSchema) continue;
|
|
1775
|
+
requestContextFields.push(...collectObjectFields(headersSchema, registry, "RequestContext"));
|
|
1776
|
+
}
|
|
1777
|
+
const context = {
|
|
1778
|
+
namespaceName,
|
|
1779
|
+
clientName,
|
|
1780
|
+
operationTypeMap,
|
|
1781
|
+
fileLevelTypeNames,
|
|
1782
|
+
ownedTypeMap,
|
|
1783
|
+
requestContextFields
|
|
1784
|
+
};
|
|
1785
|
+
writer.block(`object ${namespaceName}`, () => {
|
|
1786
|
+
emitTypes(writer, topLevelSharedTypes, ownedTypeMap, ownedTypeLookup, registry);
|
|
1787
|
+
});
|
|
1788
|
+
emitClient(writer, { clientName }, partition, context, typesByOperation, registry);
|
|
1789
|
+
for (const group of partition.groups) emitSubClientClass(writer, group, clientName, context);
|
|
1790
|
+
emitKizunaObject(writer, { streaming: usesStreaming });
|
|
1791
|
+
for (const warning of registry.warnings()) process.stderr.write(`[kizuna/kotlin] JsonElement fallback at ${warning}\n`);
|
|
1792
|
+
return writer.toString();
|
|
1793
|
+
};
|
|
1794
|
+
//#endregion
|
|
1795
|
+
Object.defineProperty(exports, "generateKotlinClient", {
|
|
1796
|
+
enumerable: true,
|
|
1797
|
+
get: function() {
|
|
1798
|
+
return generateKotlinClient;
|
|
1799
|
+
}
|
|
1800
|
+
});
|