@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.
@@ -0,0 +1,1795 @@
1
+ import { createGenerator, isDiscriminatedUnionSchema, isFileSchema, isHintPrefix, isIntegerSchema, isNamedStream, isObjectSchema, isStreamResponse, isSuccessStatus, localTypeName, mergeHeaderFields, parsePath, readDef, readDefType, readDeprecation, readDiscriminatedUnion, readDiscriminatorStringLiteral, readMetaDescription, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, routeStreams, sanitizeFieldName, sanitizeIdentifier, shortTypeName, soleStreamResponse, statusToCamelCase, streamMode, toCamelCase, toPascalCase, unwrapOptionalWrappers } from "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 sanitizeFieldName(key);
116
+ const camel = 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 = 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}${toPascalCase(key)}`);
126
+ const deprecation = 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: 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 (isFileSchema(schema)) return {
147
+ expression: "MultipartFile",
148
+ optional: false,
149
+ isFile: true
150
+ };
151
+ const id = readMetaId(schema);
152
+ const def = readDef(schema);
153
+ const discriminated = readDiscriminatedUnion(schema);
154
+ if (discriminated) {
155
+ const enumName = id ?? sanitizeIdentifier(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: readMetaDescription(schema)
164
+ });
165
+ const variants = [];
166
+ for (const option of discriminated.options) {
167
+ const variantId = readMetaId(option);
168
+ const literal = readDiscriminatorStringLiteral(option, discriminated.discriminator);
169
+ const variantResult = mapType(option, registry, variantId ?? `${enumName}${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: 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: readMetaDescription(schema)
212
+ });
213
+ const fields = objectFields(schema, registry, id);
214
+ registry.replace({
215
+ kind: "data-class",
216
+ name: id,
217
+ fields,
218
+ description: readMetaDescription(schema)
219
+ });
220
+ }
221
+ return {
222
+ expression: id,
223
+ optional: false
224
+ };
225
+ }
226
+ const { inner, optional } = 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 ? 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: 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 = sanitizeIdentifier(toPascalCase(hint));
287
+ if (!registry.has(className)) {
288
+ registry.add({
289
+ kind: "data-class",
290
+ name: className,
291
+ fields: [],
292
+ description: readMetaDescription(schema)
293
+ });
294
+ const fields = objectFields(schema, registry, className);
295
+ registry.replace({
296
+ kind: "data-class",
297
+ name: className,
298
+ fields,
299
+ description: readMetaDescription(schema)
300
+ });
301
+ }
302
+ return {
303
+ expression: className,
304
+ optional: false
305
+ };
306
+ }
307
+ case "enum": {
308
+ const enumName = id ?? sanitizeIdentifier(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: 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 = 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(readObjectShape(schema) ?? {}).length;
417
+ };
418
+ const objectShapeKeys = (schema, camelCase = false) => {
419
+ return Object.entries(readObjectShape(schema) ?? {}).map(([key, value]) => ({
420
+ name: propertyName(key, camelCase),
421
+ wireName: key,
422
+ isFile: 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 : toPascalCase(segment)).join("") : routeKey;
435
+ const methodName = methodNameOverride ?? fullJoinedName;
436
+ const baseHint = toPascalCase(fullJoinedName);
437
+ const pathParams = 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 = readMetaId(route.body) ?? `${baseHint}Input`;
445
+ const isMultipart = route.contentType === "multipart/form-data";
446
+ if (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 = 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 (isStreamResponse(responseValue)) {
492
+ const mode = streamMode(responseValue);
493
+ if (mode === "events" && isNamedStream(responseValue.stream)) stream = {
494
+ status,
495
+ mode,
496
+ events: Object.entries(responseValue.stream).map(([name, schema]) => ({
497
+ name,
498
+ caseName: toPascalCase(name),
499
+ type: mapType(schema, registry, `${baseHint}${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 = 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(resolveResponseBody(responseValue), registry, responseHint).expression;
520
+ if (isSuccessStatus(status)) {
521
+ const headersSchema = 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: 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" : statusToCamelCase(400),
538
+ status: 400,
539
+ type: "ValidationError"
540
+ });
541
+ }
542
+ successResponses.sort((left, right) => left.status - right.status);
543
+ const resultHeaderFields = 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 = createGenerator((options, _api) => {
580
+ const flatMethods = [];
581
+ const groupMap = /* @__PURE__ */ new Map();
582
+ return {
583
+ processRoute({ routeKey, route, deprecated, deprecationMessage }) {
584
+ if (routeStreams(route) && 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 : 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}${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 = 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 = 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 (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 = 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 = 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 = 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)}.${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 = 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 = 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 = 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 (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
+ export { generateKotlinClient as t };