@kizunajs/swift 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,2063 @@
1
+ import { createGenerator, isBinarySchema, isDiscriminatedUnionSchema, isFileSchema, isHintPrefix, isIntegerSchema, isJsonMediaType, isNamedStream, isObjectSchema, isStreamResponse, isSuccessStatus, isVoidSchema, localTypeName, mergeHeaderFields, parsePath, readDef, readDefType, readDeprecation, readDiscriminatedUnion, readDiscriminatorStringLiteral, readMetaDescription, readMetaId, readObjectShape, resolveResponseBody, resolveResponseContentType, resolveResponseHeaders, routeStreams, sanitizeFieldName, sanitizeIdentifier, shortTypeName, soleStreamResponse, statusToCamelCase, streamMode, toCamelCase, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
2
+ //#region src/emit.ts
3
+ var SwiftWriter = 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
+ for (const segment of text.split("\n")) this.line(`/// ${segment}`);
28
+ }
29
+ blank() {
30
+ if (this.lines.length === 0) return;
31
+ if (this.lines[this.lines.length - 1] === "") return;
32
+ this.lines.push("");
33
+ }
34
+ toString() {
35
+ return this.lines.join("\n").replace(/\n+$/, "") + "\n";
36
+ }
37
+ };
38
+ const stringLiteral = (value) => {
39
+ return "\"" + value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
40
+ };
41
+ //#endregion
42
+ //#region src/zod-to-swift.ts
43
+ var TypeRegistry = class {
44
+ types = /* @__PURE__ */ new Map();
45
+ warningSet = /* @__PURE__ */ new Set();
46
+ explicitIds = /* @__PURE__ */ new Set();
47
+ unionVariantOwners = /* @__PURE__ */ new Map();
48
+ usesAnyCodable = false;
49
+ constructor(camelCaseProperties = false, unknownEnumCase = false) {
50
+ this.camelCaseProperties = camelCaseProperties;
51
+ this.unknownEnumCase = unknownEnumCase;
52
+ }
53
+ has(name) {
54
+ return this.types.has(name);
55
+ }
56
+ get(name) {
57
+ return this.types.get(name);
58
+ }
59
+ add(type) {
60
+ if (this.types.has(type.name)) return;
61
+ this.types.set(type.name, type);
62
+ }
63
+ replace(type) {
64
+ this.types.set(type.name, type);
65
+ }
66
+ all() {
67
+ return Array.from(this.types.values());
68
+ }
69
+ markExplicitId(name) {
70
+ this.explicitIds.add(name);
71
+ }
72
+ isExplicitId(name) {
73
+ return this.explicitIds.has(name);
74
+ }
75
+ /**
76
+ * Records that `payloadName` was synthesized as a variant of `unionName`. The real parent, as
77
+ * opposed to the name-prefix guess the ownership heuristic would otherwise make.
78
+ */
79
+ markUnionVariant(payloadName, unionName) {
80
+ this.unionVariantOwners.set(payloadName, unionName);
81
+ }
82
+ unionVariantOwner(payloadName) {
83
+ return this.unionVariantOwners.get(payloadName);
84
+ }
85
+ warnAnyCodable(hint, reason) {
86
+ this.usesAnyCodable = true;
87
+ this.warningSet.add(`${hint}: ${reason}`);
88
+ }
89
+ warnings() {
90
+ return Array.from(this.warningSet);
91
+ }
92
+ };
93
+ /**
94
+ * The Swift property name for a wire key: verbatim, or camelCased under
95
+ * `camelCaseProperties`.
96
+ */
97
+ const propertyName = (key, camelCase) => {
98
+ if (!camelCase) return sanitizeFieldName(key);
99
+ const camel = toCamelCase(key);
100
+ if (!camel) return "field";
101
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
102
+ };
103
+ const objectFields = (schema, registry, hint) => {
104
+ const shape = readObjectShape(schema) ?? {};
105
+ const fields = [];
106
+ const seen = /* @__PURE__ */ new Map();
107
+ for (const [key, value] of Object.entries(shape)) {
108
+ const result = mapType(value, registry, `${hint}${toPascalCase(key)}`);
109
+ const deprecation = readDeprecation(value);
110
+ const swiftName = propertyName(key, registry.camelCaseProperties);
111
+ const previousWireName = seen.get(swiftName);
112
+ if (previousWireName !== void 0 && previousWireName !== key) throw new Error(`@kizunajs/swift: field key collision in ${hint}, ${stringify(previousWireName)} and ${stringify(key)} both sanitize to ${stringify(swiftName)}.`);
113
+ seen.set(swiftName, key);
114
+ fields.push({
115
+ name: swiftName,
116
+ wireName: key,
117
+ type: result.expression,
118
+ optional: result.optional,
119
+ description: readMetaDescription(value),
120
+ isFile: result.isFile,
121
+ deprecated: deprecation !== void 0,
122
+ deprecationMessage: deprecation?.message
123
+ });
124
+ }
125
+ return fields;
126
+ };
127
+ const stringify = (value) => JSON.stringify(value);
128
+ const mapType = (schema, registry, hint) => {
129
+ if (isFileSchema(schema)) return {
130
+ expression: "MultipartFile",
131
+ optional: false,
132
+ isFile: true
133
+ };
134
+ if (isBinarySchema(schema)) return {
135
+ expression: "Foundation.Data",
136
+ optional: false
137
+ };
138
+ const id = readMetaId(schema);
139
+ const def = readDef(schema);
140
+ const discriminated = readDiscriminatedUnion(schema);
141
+ if (discriminated) {
142
+ const enumName = id ?? sanitizeIdentifier(toPascalCase(hint));
143
+ if (id) registry.markExplicitId(enumName);
144
+ if (!registry.has(enumName)) {
145
+ registry.add({
146
+ kind: "discriminated-enum",
147
+ name: enumName,
148
+ discriminator: discriminated.discriminator,
149
+ variants: [],
150
+ description: readMetaDescription(schema)
151
+ });
152
+ const variants = [];
153
+ for (const option of discriminated.options) {
154
+ const variantId = readMetaId(option);
155
+ const literal = readDiscriminatorStringLiteral(option, discriminated.discriminator);
156
+ const variantResult = mapType(option, registry, variantId ?? `${enumName}${toPascalCase(literal ?? "Variant")}`);
157
+ if (literal !== void 0) {
158
+ variants.push({
159
+ caseName: sanitizeCaseName(literal),
160
+ literal,
161
+ payloadType: variantResult.expression,
162
+ payloadRegistryName: variantResult.expression
163
+ });
164
+ if (variantId === void 0) registry.markUnionVariant(variantResult.expression, enumName);
165
+ }
166
+ }
167
+ registry.replace({
168
+ kind: "discriminated-enum",
169
+ name: enumName,
170
+ discriminator: discriminated.discriminator,
171
+ variants,
172
+ description: readMetaDescription(schema)
173
+ });
174
+ }
175
+ return {
176
+ expression: enumName,
177
+ optional: false
178
+ };
179
+ }
180
+ if (id && def.type === "object") {
181
+ registry.markExplicitId(id);
182
+ if (!registry.has(id)) {
183
+ registry.add({
184
+ kind: "struct",
185
+ name: id,
186
+ fields: [],
187
+ description: readMetaDescription(schema)
188
+ });
189
+ const fields = objectFields(schema, registry, id);
190
+ registry.replace({
191
+ kind: "struct",
192
+ name: id,
193
+ fields,
194
+ description: readMetaDescription(schema)
195
+ });
196
+ }
197
+ return {
198
+ expression: id,
199
+ optional: false
200
+ };
201
+ }
202
+ const { inner, optional } = unwrapOptionalWrappers(schema);
203
+ if (optional) {
204
+ const innerResult = mapType(inner, registry, hint);
205
+ return {
206
+ expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
207
+ optional: true,
208
+ isFile: innerResult.isFile
209
+ };
210
+ }
211
+ switch (def.type) {
212
+ case "void":
213
+ case "never": return {
214
+ expression: "Void",
215
+ optional: false
216
+ };
217
+ case "pipe": {
218
+ const outType = def.out ? readDefType(def.out) : void 0;
219
+ if (def.out && outType !== "transform") return mapType(def.out, registry, hint);
220
+ if (def.in) return mapType(def.in, registry, hint);
221
+ registry.warnAnyCodable(hint, "pipe schema missing resolvable type");
222
+ return {
223
+ expression: "AnyCodable",
224
+ optional: false
225
+ };
226
+ }
227
+ case "string": return {
228
+ expression: def.format === "datetime" || def.checks?.some((check) => check.format === "datetime") ? "Date" : "String",
229
+ optional: false
230
+ };
231
+ case "number": return {
232
+ expression: isIntegerSchema(schema) ? "Int" : "Double",
233
+ optional: false
234
+ };
235
+ case "bigint": return {
236
+ expression: "Int64",
237
+ optional: false
238
+ };
239
+ case "boolean": return {
240
+ expression: "Bool",
241
+ optional: false
242
+ };
243
+ case "date": return {
244
+ expression: "Date",
245
+ optional: false
246
+ };
247
+ case "array": {
248
+ const element = def.element;
249
+ if (!element) {
250
+ registry.warnAnyCodable(hint, "array element schema missing");
251
+ return {
252
+ expression: "[AnyCodable]",
253
+ optional: false
254
+ };
255
+ }
256
+ return {
257
+ expression: `[${mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "")}]`,
258
+ optional: false
259
+ };
260
+ }
261
+ case "object": {
262
+ const structName = sanitizeIdentifier(toPascalCase(hint));
263
+ if (!registry.has(structName)) {
264
+ registry.add({
265
+ kind: "struct",
266
+ name: structName,
267
+ fields: [],
268
+ description: readMetaDescription(schema)
269
+ });
270
+ const fields = objectFields(schema, registry, structName);
271
+ registry.replace({
272
+ kind: "struct",
273
+ name: structName,
274
+ fields,
275
+ description: readMetaDescription(schema)
276
+ });
277
+ }
278
+ return {
279
+ expression: structName,
280
+ optional: false
281
+ };
282
+ }
283
+ case "enum": {
284
+ const enumName = id ?? sanitizeIdentifier(toPascalCase(hint));
285
+ if (id) registry.markExplicitId(enumName);
286
+ const entries = def.entries ?? {};
287
+ const cases = Object.values(entries);
288
+ if (!registry.has(enumName)) registry.add({
289
+ kind: "enum",
290
+ name: enumName,
291
+ cases,
292
+ description: readMetaDescription(schema),
293
+ unknownCase: registry.unknownEnumCase
294
+ });
295
+ return {
296
+ expression: enumName,
297
+ optional: false
298
+ };
299
+ }
300
+ case "literal": {
301
+ const values = def.values ?? [];
302
+ if (values.every((value) => typeof value === "string")) return {
303
+ expression: "String",
304
+ optional: false
305
+ };
306
+ if (values.every((value) => typeof value === "number")) return {
307
+ expression: "Double",
308
+ optional: false
309
+ };
310
+ if (values.every((value) => typeof value === "boolean")) return {
311
+ expression: "Bool",
312
+ optional: false
313
+ };
314
+ registry.warnAnyCodable(hint, "literal of mixed types");
315
+ return {
316
+ expression: "AnyCodable",
317
+ optional: false
318
+ };
319
+ }
320
+ case "union": {
321
+ const options = def.options ?? [];
322
+ if (options.every((option) => {
323
+ const optionDef = readDef(option);
324
+ return optionDef.type === "literal" && (optionDef.values ?? []).every((value) => typeof value === "string");
325
+ })) return {
326
+ expression: "String",
327
+ optional: false
328
+ };
329
+ const resolved = options.map((option) => mapType(option, registry, hint)).filter((result) => result !== void 0 && result.expression !== "AnyCodable");
330
+ if (resolved.length === 0) {
331
+ registry.warnAnyCodable(hint, "non-discriminated union");
332
+ return {
333
+ expression: "AnyCodable",
334
+ optional: false
335
+ };
336
+ }
337
+ const expressions = resolved.map((result) => result.expression);
338
+ const unique = [...new Set(expressions)];
339
+ if (unique.length === 1) return resolved[0];
340
+ const arrayPattern = /^\[(.+)\]$/;
341
+ const baseTypes = unique.map((expression) => {
342
+ const match = arrayPattern.exec(expression);
343
+ return match ? match[1] : expression;
344
+ });
345
+ if (new Set(baseTypes).size === 1) return {
346
+ expression: `[${baseTypes[0]}]`,
347
+ optional: false
348
+ };
349
+ registry.warnAnyCodable(hint, "non-discriminated union");
350
+ return {
351
+ expression: "AnyCodable",
352
+ optional: false
353
+ };
354
+ }
355
+ case "record": {
356
+ const valueType = def.valueType;
357
+ if (valueType) return {
358
+ expression: `[String: ${mapType(valueType, registry, `${hint}Value`).expression.replace(/\?$/, "")}]`,
359
+ optional: false
360
+ };
361
+ registry.warnAnyCodable(hint, "record without value schema");
362
+ return {
363
+ expression: "[String: AnyCodable]",
364
+ optional: false
365
+ };
366
+ }
367
+ case "any":
368
+ case "unknown":
369
+ registry.warnAnyCodable(hint, `${def.type} schema`);
370
+ return {
371
+ expression: "AnyCodable",
372
+ optional: false
373
+ };
374
+ default:
375
+ registry.warnAnyCodable(hint, `unhandled schema type: ${def.type ?? "unknown"}`);
376
+ return {
377
+ expression: "AnyCodable",
378
+ optional: false
379
+ };
380
+ }
381
+ };
382
+ const sanitizeCaseName = (literal) => {
383
+ const cleaned = literal.replace(/[^a-zA-Z0-9_]/g, "_");
384
+ if (!cleaned) return "value";
385
+ if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
386
+ return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
387
+ };
388
+ const collectObjectFields = (schema, registry, hint) => {
389
+ return objectFields(schema, registry, hint);
390
+ };
391
+ const objectFieldCount = (schema) => {
392
+ return Object.keys(readObjectShape(schema) ?? {}).length;
393
+ };
394
+ const objectShapeKeys = (schema, camelCase = false) => {
395
+ return Object.entries(readObjectShape(schema) ?? {}).map(([key, value]) => ({
396
+ name: propertyName(key, camelCase),
397
+ wireName: key,
398
+ isFile: isFileSchema(value)
399
+ }));
400
+ };
401
+ //#endregion
402
+ //#region src/generator.ts
403
+ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMessage, methodNameOverride) => {
404
+ const fullJoinedName = routeKey.includes(".") ? routeKey.split(".").map((segment, index) => index === 0 ? segment : toPascalCase(segment)).join("") : routeKey;
405
+ const methodName = methodNameOverride ?? fullJoinedName;
406
+ const baseHint = toPascalCase(fullJoinedName);
407
+ const pathParams = parsePath(route.path).paramNames;
408
+ const declaredPathParams = route.pathParams ? collectObjectFields(route.pathParams, registry, `${baseHint}Params`) : [];
409
+ const queryFields = route.query ? collectObjectFields(route.query, registry, `${baseHint}Query`) : [];
410
+ const headerFields = route.headers ? collectObjectFields(route.headers, registry, `${baseHint}Headers`) : [];
411
+ let bodyDescriptor;
412
+ if (route.body && !isVoidSchema(route.body)) {
413
+ const bodyHint = readMetaId(route.body) ?? `${baseHint}Input`;
414
+ const isMultipart = route.contentType === "multipart/form-data";
415
+ if (isDiscriminatedUnionSchema(route.body)) bodyDescriptor = {
416
+ kind: "union",
417
+ structName: mapType(route.body, registry, bodyHint).expression,
418
+ flattened: [],
419
+ multipartFields: []
420
+ };
421
+ else if (isMultipart) {
422
+ mapType(route.body, registry, bodyHint);
423
+ bodyDescriptor = {
424
+ kind: "multipart",
425
+ flattened: collectObjectFields(route.body, registry, bodyHint),
426
+ multipartFields: objectShapeKeys(route.body, registry.camelCaseProperties)
427
+ };
428
+ } else {
429
+ const isObject = isObjectSchema(route.body);
430
+ const fieldCount = isObject ? objectFieldCount(route.body) : 0;
431
+ if (isObject && fieldCount === 0) bodyDescriptor = {
432
+ kind: "json-empty",
433
+ flattened: [],
434
+ multipartFields: []
435
+ };
436
+ else {
437
+ const structName = mapType(route.body, registry, bodyHint).expression;
438
+ if (!isObject) bodyDescriptor = {
439
+ kind: "json-struct",
440
+ structName,
441
+ flattened: [],
442
+ multipartFields: []
443
+ };
444
+ else bodyDescriptor = {
445
+ kind: "json-flat",
446
+ structName,
447
+ flattened: collectObjectFields(route.body, registry, bodyHint),
448
+ multipartFields: []
449
+ };
450
+ }
451
+ }
452
+ }
453
+ const successResponses = [];
454
+ const errorCases = [];
455
+ let stream;
456
+ for (const [statusKey, responseValue] of Object.entries(route.responses)) {
457
+ const status = Number(statusKey);
458
+ const responseHint = `${baseHint}Response${status === 200 ? "" : status}`;
459
+ if (isStreamResponse(responseValue)) {
460
+ const mode = streamMode(responseValue);
461
+ if (mode === "events" && isNamedStream(responseValue.stream)) stream = {
462
+ status,
463
+ mode,
464
+ events: Object.entries(responseValue.stream).map(([name, schema]) => ({
465
+ name,
466
+ caseName: sanitizeEnumCaseName(name),
467
+ type: mapType(schema, registry, `${baseHint}${toPascalCase(name)}`).expression
468
+ }))
469
+ };
470
+ else if (mode === "events") stream = {
471
+ status,
472
+ mode,
473
+ messageType: mapType(responseValue.stream, registry, `${baseHint}Message`).expression
474
+ };
475
+ else stream = {
476
+ status,
477
+ mode
478
+ };
479
+ const headersSchema = resolveResponseHeaders(responseValue);
480
+ successResponses.push({
481
+ status,
482
+ type: stream.messageType ?? "Event",
483
+ responseHeaders: headersSchema ? collectObjectFields(headersSchema, registry, `${baseHint}ResponseHeaders`) : [],
484
+ isRaw: false,
485
+ isBinary: false
486
+ });
487
+ continue;
488
+ }
489
+ const bodySchema = resolveResponseBody(responseValue);
490
+ const responseContentType = resolveResponseContentType(responseValue);
491
+ const isBinary = isBinarySchema(bodySchema);
492
+ const isRaw = isBinary || responseContentType !== void 0 && !isJsonMediaType(responseContentType);
493
+ const typeExpression = mapType(bodySchema, registry, responseHint).expression;
494
+ if (isSuccessStatus(status)) {
495
+ const headersSchema = resolveResponseHeaders(responseValue);
496
+ const perStatusHeaderFields = headersSchema ? collectObjectFields(headersSchema, registry, `${baseHint}ResponseHeaders`) : [];
497
+ successResponses.push({
498
+ status,
499
+ type: typeExpression,
500
+ responseHeaders: perStatusHeaderFields,
501
+ isRaw,
502
+ isBinary
503
+ });
504
+ } else errorCases.push({
505
+ caseName: statusToCamelCase(status),
506
+ status,
507
+ type: typeExpression,
508
+ isRaw,
509
+ isBinary
510
+ });
511
+ }
512
+ if (route.body || route.query) {
513
+ const has400 = errorCases.some((c) => c.status === 400);
514
+ errorCases.push({
515
+ caseName: has400 ? "validationError" : statusToCamelCase(400),
516
+ status: 400,
517
+ type: "ValidationError",
518
+ isRaw: false,
519
+ isBinary: false
520
+ });
521
+ }
522
+ successResponses.sort((left, right) => left.status - right.status);
523
+ const resultHeaderFields = mergeHeaderFields(successResponses.map((entry) => entry.responseHeaders));
524
+ let successReturnType = "Void";
525
+ let successSumEnumName;
526
+ if (route.method !== "HEAD") {
527
+ const onlySuccess = successResponses[0];
528
+ if (successResponses.length === 1 && onlySuccess) successReturnType = onlySuccess.type;
529
+ else if (successResponses.length > 1) {
530
+ successSumEnumName = "Success";
531
+ successReturnType = successSumEnumName;
532
+ }
533
+ }
534
+ const resultWrapperName = successReturnType !== "Void" ? "Result" : void 0;
535
+ return {
536
+ name: methodName,
537
+ operationName: baseHint,
538
+ summary: route.summary,
539
+ description: route.description,
540
+ deprecated,
541
+ deprecationMessage,
542
+ pathParams,
543
+ declaredPathParams,
544
+ pathTemplate: route.path,
545
+ method: route.method,
546
+ body: bodyDescriptor,
547
+ query: queryFields,
548
+ headers: headerFields,
549
+ resultHeaderFields,
550
+ resultWrapperName,
551
+ successResponses,
552
+ successReturnType,
553
+ successSumEnumName,
554
+ failureEnumName: "Failure",
555
+ errorCases,
556
+ stream
557
+ };
558
+ };
559
+ const swiftGenerator = createGenerator((options, _api) => {
560
+ const flatMethods = [];
561
+ const groupMap = /* @__PURE__ */ new Map();
562
+ return {
563
+ processRoute({ routeKey, route, deprecated, deprecationMessage }) {
564
+ if (routeStreams(route) && soleStreamResponse(route) === void 0) {
565
+ console.warn(`[kizuna/swift] Skipping route "${routeKey}": a streamed status beside another 2xx status is not generated.`);
566
+ return;
567
+ }
568
+ const dotIndex = routeKey.indexOf(".");
569
+ if (dotIndex !== -1) {
570
+ const groupKey = routeKey.slice(0, dotIndex);
571
+ const leafName = routeKey.slice(dotIndex + 1).split(".").map((segment, index) => index === 0 ? segment : toPascalCase(segment)).join("");
572
+ const methods = groupMap.get(groupKey) ?? [];
573
+ if (methods.length === 0) groupMap.set(groupKey, methods);
574
+ methods.push(buildRouteMethod(routeKey, route, options.registry, deprecated, deprecationMessage, leafName));
575
+ } else flatMethods.push(buildRouteMethod(routeKey, route, options.registry, deprecated, deprecationMessage));
576
+ },
577
+ finalize() {
578
+ const groups = [];
579
+ for (const [groupKey, methods] of groupMap) groups.push({
580
+ groupKey,
581
+ structName: `${options.namespaceName}${toPascalCase(groupKey)}Client`,
582
+ propertyName: groupKey,
583
+ methods
584
+ });
585
+ return {
586
+ flatMethods,
587
+ groups
588
+ };
589
+ }
590
+ };
591
+ });
592
+ const SWIFT_KEYWORDS = new Set([
593
+ "associatedtype",
594
+ "class",
595
+ "deinit",
596
+ "enum",
597
+ "extension",
598
+ "fileprivate",
599
+ "func",
600
+ "import",
601
+ "init",
602
+ "inout",
603
+ "internal",
604
+ "let",
605
+ "open",
606
+ "operator",
607
+ "private",
608
+ "protocol",
609
+ "public",
610
+ "rethrows",
611
+ "static",
612
+ "struct",
613
+ "subscript",
614
+ "typealias",
615
+ "var",
616
+ "break",
617
+ "case",
618
+ "continue",
619
+ "default",
620
+ "defer",
621
+ "do",
622
+ "else",
623
+ "fallthrough",
624
+ "for",
625
+ "guard",
626
+ "if",
627
+ "in",
628
+ "repeat",
629
+ "return",
630
+ "switch",
631
+ "where",
632
+ "while",
633
+ "as",
634
+ "catch",
635
+ "false",
636
+ "is",
637
+ "nil",
638
+ "super",
639
+ "self",
640
+ "Self",
641
+ "throw",
642
+ "throws",
643
+ "true",
644
+ "try",
645
+ "Type",
646
+ "Protocol",
647
+ "description"
648
+ ]);
649
+ const escapeKeyword = (name) => {
650
+ return SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
651
+ };
652
+ const isValidSwiftIdentifier = (value) => {
653
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
654
+ };
655
+ /**
656
+ * Rewrite an enum value into a valid Swift case name, preserving the original
657
+ * as the rawValue. Already-valid identifiers (e.g. `snake_case`) are kept
658
+ * verbatim; others are camelCased, with a `_` prefix when they start with a
659
+ * digit. Falls back to `_` when nothing identifier-safe remains.
660
+ */
661
+ const sanitizeEnumCaseName = (value) => {
662
+ if (isValidSwiftIdentifier(value)) return value;
663
+ const camel = toCamelCase(value);
664
+ if (!camel) return "_";
665
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
666
+ };
667
+ const optionalize = (type, optional) => {
668
+ if (!optional) return type;
669
+ return type.endsWith("?") ? type : `${type}?`;
670
+ };
671
+ const emitNamedFactory = (writer, factoryName, fields, buildExpression) => {
672
+ const build = buildExpression ?? ((args) => `.init(${args})`);
673
+ if (fields.length === 0) {
674
+ writer.block(`public static func ${escapeKeyword(factoryName)}() -> Self`, () => {
675
+ writer.line(build(""));
676
+ });
677
+ return;
678
+ }
679
+ const params = fields.map((field) => {
680
+ const typeExpression = optionalize(field.type, field.optional);
681
+ const defaultPart = field.optional ? " = nil" : "";
682
+ return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
683
+ });
684
+ const args = fields.map((field) => `${field.name}: ${escapeKeyword(field.name)}`).join(", ");
685
+ if (params.length === 1) {
686
+ writer.block(`public static func ${escapeKeyword(factoryName)}(${params[0]}) -> Self`, () => {
687
+ writer.line(build(args));
688
+ });
689
+ return;
690
+ }
691
+ writer.line(`public static func ${escapeKeyword(factoryName)}(`);
692
+ for (let index = 0; index < params.length; index += 1) writer.line(` ${params[index]}${index === params.length - 1 ? "" : ","}`);
693
+ writer.block(") -> Self", () => {
694
+ writer.line(build(args));
695
+ });
696
+ };
697
+ const emitMemberwiseInit = (writer, fields, assignmentTarget = (field) => escapeKeyword(field.name)) => {
698
+ if (fields.length === 0) {
699
+ writer.line("public init() {}");
700
+ return;
701
+ }
702
+ const params = fields.map((field) => {
703
+ const typeExpression = optionalize(field.type, field.optional);
704
+ const defaultPart = field.optional ? " = nil" : "";
705
+ return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
706
+ });
707
+ if (params.length === 1) {
708
+ writer.block(`public init(${params[0]})`, () => {
709
+ for (const field of fields) writer.line(`self.${assignmentTarget(field)} = ${escapeKeyword(field.name)}`);
710
+ });
711
+ return;
712
+ }
713
+ writer.line("public init(");
714
+ for (let index = 0; index < params.length; index += 1) {
715
+ const suffix = index === params.length - 1 ? "" : ",";
716
+ writer.line(` ${params[index]}${suffix}`);
717
+ }
718
+ writer.block(")", () => {
719
+ for (const field of fields) writer.line(`self.${assignmentTarget(field)} = ${escapeKeyword(field.name)}`);
720
+ });
721
+ };
722
+ const deprecatedStorageName = (field, siblings) => {
723
+ const takenNames = new Set(siblings.map((sibling) => sibling.name));
724
+ let storageName = `_${field.name}`;
725
+ while (takenNames.has(storageName)) storageName = `_${storageName}`;
726
+ return storageName;
727
+ };
728
+ const unknownCaseName = (caseNames) => {
729
+ const taken = new Set(caseNames);
730
+ let candidate = "unknown";
731
+ while (taken.has(candidate)) candidate = `_${candidate}`;
732
+ return candidate;
733
+ };
734
+ const emitStringEnum = (writer, name, cases, unknownCase, description) => {
735
+ writer.blank();
736
+ writer.docComment(description);
737
+ if (!unknownCase) {
738
+ writer.block(`public enum ${name}: String, Codable, Sendable`, () => {
739
+ for (const caseName of cases) writer.line(`case ${escapeKeyword(sanitizeEnumCaseName(caseName))} = ${stringLiteral(caseName)}`);
740
+ });
741
+ return;
742
+ }
743
+ const caseNames = cases.map((caseName) => sanitizeEnumCaseName(caseName));
744
+ const unknown = unknownCaseName(caseNames);
745
+ writer.block(`public enum ${name}: RawRepresentable, Codable, Sendable, Hashable`, () => {
746
+ for (const caseName of caseNames) writer.line(`case ${escapeKeyword(caseName)}`);
747
+ writer.line(`case ${escapeKeyword(unknown)}(String)`);
748
+ writer.blank();
749
+ writer.block("public init(rawValue: String)", () => {
750
+ writer.line("switch rawValue {");
751
+ for (let index = 0; index < cases.length; index += 1) writer.line(`case ${stringLiteral(cases[index])}: self = .${escapeKeyword(caseNames[index])}`);
752
+ writer.line(`default: self = .${escapeKeyword(unknown)}(rawValue)`);
753
+ writer.line("}");
754
+ });
755
+ writer.blank();
756
+ writer.block("public var rawValue: String", () => {
757
+ writer.line("switch self {");
758
+ for (let index = 0; index < cases.length; index += 1) writer.line(`case .${escapeKeyword(caseNames[index])}: return ${stringLiteral(cases[index])}`);
759
+ writer.line(`case let .${escapeKeyword(unknown)}(value): return value`);
760
+ writer.line("}");
761
+ });
762
+ writer.blank();
763
+ writer.block("public init(from decoder: Decoder) throws", () => {
764
+ writer.line("let container = try decoder.singleValueContainer()");
765
+ writer.line("self.init(rawValue: try container.decode(String.self))");
766
+ });
767
+ writer.blank();
768
+ writer.block("public func encode(to encoder: Encoder) throws", () => {
769
+ writer.line("var container = encoder.singleValueContainer()");
770
+ writer.line("try container.encode(rawValue)");
771
+ });
772
+ });
773
+ };
774
+ const ownedTypePath = (typeName, ownedTypeMap) => {
775
+ const owningStruct = ownedTypeMap.get(typeName);
776
+ if (owningStruct === void 0) return typeName;
777
+ return `${ownedTypePath(owningStruct, ownedTypeMap)}.${shortTypeName(typeName, owningStruct)}`;
778
+ };
779
+ const relativeOwnedPath = (typeName, ownedTypeMap, enclosingName) => {
780
+ const path = ownedTypePath(typeName, ownedTypeMap);
781
+ const prefix = `${enclosingName}.`;
782
+ return path.startsWith(prefix) ? path.slice(prefix.length) : void 0;
783
+ };
784
+ const qualifyOwnedVariants = (type, ownedTypeMap, enclosingName) => ({
785
+ ...type,
786
+ variants: type.variants.map((variant) => ({
787
+ ...variant,
788
+ payloadType: relativeOwnedPath(variant.payloadType, ownedTypeMap, enclosingName) ?? ownedTypePath(variant.payloadType, ownedTypeMap)
789
+ }))
790
+ });
791
+ const emitTypes = (writer, types, context, ownedTypeMap = /* @__PURE__ */ new Map(), ownedTypeLookup = /* @__PURE__ */ new Map()) => {
792
+ for (const type of types) {
793
+ writer.blank();
794
+ writer.docComment(type.description);
795
+ if (type.kind === "struct") emitStruct(writer, type, context, ownedTypeMap, ownedTypeLookup);
796
+ else if (type.kind === "enum") emitStringEnum(writer, type.name, type.cases, type.unknownCase, type.description);
797
+ else emitDiscriminatedEnum(writer, qualifyOwnedVariants(type, ownedTypeMap, type.name), context, type.name, ownedTypeMap, ownedTypeLookup);
798
+ }
799
+ };
800
+ const emitOwnedTypes = (writer, ownerName, context, ownedTypeMap, ownedTypeLookup) => {
801
+ for (const [ownedName, owningType] of ownedTypeMap) {
802
+ if (owningType !== ownerName) continue;
803
+ const ownedType = ownedTypeLookup.get(ownedName);
804
+ if (!ownedType) continue;
805
+ const shortName = shortTypeName(ownedName, ownerName);
806
+ if (ownedType.kind === "enum") emitStringEnum(writer, shortName, ownedType.cases, ownedType.unknownCase, ownedType.description);
807
+ else if (ownedType.kind === "struct") emitStruct(writer, {
808
+ ...ownedType,
809
+ name: shortName
810
+ }, context, ownedTypeMap, ownedTypeLookup, ownedName);
811
+ else if (ownedType.kind === "discriminated-enum") emitDiscriminatedEnum(writer, qualifyOwnedVariants({
812
+ ...ownedType,
813
+ name: shortName
814
+ }, ownedTypeMap, ownedName), context, ownedName, ownedTypeMap, ownedTypeLookup);
815
+ }
816
+ };
817
+ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, registryName) => {
818
+ const lookupName = registryName ?? type.name;
819
+ const hasFile = type.fields.some((field) => field.isFile);
820
+ const conformances = hasFile ? "Sendable, Equatable" : "Codable, Sendable, Equatable";
821
+ const resolveOwnedType = (raw) => {
822
+ const optional = raw.endsWith("?");
823
+ const stripped = optional ? raw.slice(0, -1) : raw;
824
+ const isArray = stripped.startsWith("[") && stripped.endsWith("]");
825
+ const inner = isArray ? stripped.slice(1, -1) : stripped;
826
+ if (ownedTypeMap.get(inner) !== lookupName) return raw;
827
+ const short = shortTypeName(inner, lookupName);
828
+ const resolved = isArray ? `[${short}]` : short;
829
+ return optional ? `${resolved}?` : resolved;
830
+ };
831
+ const resolveFieldType = (fieldType, fieldOptional) => {
832
+ return optionalize(resolveOwnedType(fieldType), fieldOptional);
833
+ };
834
+ const adjustedFields = type.fields.map((field) => ({
835
+ ...field,
836
+ type: resolveOwnedType(field.type)
837
+ }));
838
+ const needsCodingKeys = !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
839
+ writer.block(`public struct ${type.name}: ${conformances}`, () => {
840
+ emitOwnedTypes(writer, lookupName, context, ownedTypeMap, ownedTypeLookup);
841
+ for (const field of type.fields) {
842
+ const fieldType = resolveFieldType(field.type, field.optional);
843
+ if (field.deprecated) {
844
+ writer.line(`private let ${deprecatedStorageName(field, type.fields)}: ${fieldType}`);
845
+ writer.docComment(field.description);
846
+ writer.line(deprecatedAttribute(field.deprecationMessage));
847
+ writer.line(`public var ${escapeKeyword(field.name)}: ${fieldType} { ${deprecatedStorageName(field, type.fields)} }`);
848
+ } else {
849
+ writer.docComment(field.description);
850
+ writer.line(`public let ${escapeKeyword(field.name)}: ${fieldType}`);
851
+ }
852
+ }
853
+ if (needsCodingKeys) {
854
+ writer.blank();
855
+ writer.block("private enum CodingKeys: String, CodingKey", () => {
856
+ for (const field of type.fields) if (field.deprecated) writer.line(`case ${deprecatedStorageName(field, type.fields)} = ${stringLiteral(field.wireName)}`);
857
+ else if (field.name === field.wireName) writer.line(`case ${escapeKeyword(field.name)}`);
858
+ else writer.line(`case ${escapeKeyword(field.name)} = ${stringLiteral(field.wireName)}`);
859
+ });
860
+ }
861
+ writer.blank();
862
+ emitMemberwiseInit(writer, adjustedFields, (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name));
863
+ });
864
+ };
865
+ const emitDiscriminatedEnum = (writer, type, context, registryName, ownedTypeMap, ownedTypeLookup) => {
866
+ const lookupName = registryName ?? type.name;
867
+ writer.block(`public enum ${type.name}: Codable, Sendable, Equatable`, () => {
868
+ if (ownedTypeMap && ownedTypeLookup) emitOwnedTypes(writer, lookupName, context, ownedTypeMap, ownedTypeLookup);
869
+ for (const variant of type.variants) writer.line(`case ${escapeKeyword(variant.caseName)}(${variant.payloadType})`);
870
+ for (const variant of type.variants) {
871
+ const payloadStruct = registryStruct(variant.payloadRegistryName, context);
872
+ if (!payloadStruct) continue;
873
+ const isDiscriminator = (field) => field.wireName === type.discriminator || field.name === type.discriminator;
874
+ const factoryParams = payloadStruct.fields.filter((field) => !isDiscriminator(field)).map((field) => {
875
+ const defaultPart = field.optional ? " = nil" : "";
876
+ const fieldType = (ownedTypeMap && relativeOwnedPath(field.type, ownedTypeMap, lookupName)) ?? resolveType(field.type, void 0, context);
877
+ return `${escapeKeyword(field.name)}: ${optionalize(fieldType, field.optional)}${defaultPart}`;
878
+ }).join(", ");
879
+ const payloadArgs = payloadStruct.fields.map((field) => {
880
+ if (isDiscriminator(field)) {
881
+ const literal = field.type === "String" ? stringLiteral(variant.literal) : variant.literal;
882
+ return `${field.name}: ${literal}`;
883
+ }
884
+ return `${field.name}: ${escapeKeyword(field.name)}`;
885
+ }).join(", ");
886
+ writer.block(`public static func ${escapeKeyword(variant.caseName)}(${factoryParams}) -> ${type.name}`, () => {
887
+ writer.line(`.${escapeKeyword(variant.caseName)}(${variant.payloadType}(${payloadArgs}))`);
888
+ });
889
+ }
890
+ const variantStructs = type.variants.map((variant) => ({
891
+ variant,
892
+ struct: registryStruct(variant.payloadRegistryName, context)
893
+ }));
894
+ const firstStruct = variantStructs[0]?.struct;
895
+ if (firstStruct && variantStructs.every(({ struct }) => struct !== void 0)) {
896
+ const shared = firstStruct.fields.filter((field) => variantStructs.every(({ struct }) => struct.fields.some((other) => other.name === field.name && other.type === field.type && other.optional === field.optional)));
897
+ for (const field of shared) {
898
+ const fieldType = optionalize((ownedTypeMap && relativeOwnedPath(field.type, ownedTypeMap, lookupName)) ?? resolveType(field.type, void 0, context), field.optional);
899
+ writer.blank();
900
+ writer.block(`public var ${escapeKeyword(field.name)}: ${fieldType}`, () => {
901
+ writer.line("switch self {");
902
+ for (const { variant } of variantStructs) writer.line(`case .${escapeKeyword(variant.caseName)}(let payload): return payload.${escapeKeyword(field.name)}`);
903
+ writer.line("}");
904
+ });
905
+ }
906
+ }
907
+ writer.blank();
908
+ writer.block("private enum DiscriminatorKey: String, CodingKey", () => {
909
+ writer.line(`case discriminator = ${stringLiteral(type.discriminator)}`);
910
+ });
911
+ writer.blank();
912
+ writer.block("public init(from decoder: Decoder) throws", () => {
913
+ writer.line("let container = try decoder.container(keyedBy: DiscriminatorKey.self)");
914
+ writer.line("let kind = try container.decode(String.self, forKey: .discriminator)");
915
+ writer.line("let single = try decoder.singleValueContainer()");
916
+ writer.line("switch kind {");
917
+ for (const variant of type.variants) {
918
+ writer.line(`case ${stringLiteral(variant.literal)}:`);
919
+ writer.line(` self = .${escapeKeyword(variant.caseName)}(try single.decode(${variant.payloadType}.self))`);
920
+ }
921
+ writer.line("default:");
922
+ writer.line(" throw DecodingError.dataCorruptedError(forKey: .discriminator, in: container, debugDescription: \"Unknown discriminator: \\(kind)\")");
923
+ writer.line("}");
924
+ });
925
+ writer.blank();
926
+ writer.block("public func encode(to encoder: Encoder) throws", () => {
927
+ writer.line("var single = encoder.singleValueContainer()");
928
+ writer.line("switch self {");
929
+ for (const variant of type.variants) {
930
+ writer.line(`case .${escapeKeyword(variant.caseName)}(let payload):`);
931
+ writer.line(" try single.encode(payload)");
932
+ }
933
+ writer.line("}");
934
+ });
935
+ });
936
+ };
937
+ const buildOperationTypeMap = (allMethods, registry) => {
938
+ const operationNames = new Set(allMethods.map((method) => method.operationName));
939
+ const operationTypeMap = /* @__PURE__ */ new Map();
940
+ for (const type of registry.all()) {
941
+ let bestMatch;
942
+ for (const operationName of operationNames) if (isHintPrefix(type.name, operationName)) {
943
+ if (!bestMatch || operationName.length > bestMatch.length) bestMatch = operationName;
944
+ }
945
+ if (bestMatch !== void 0) operationTypeMap.set(type.name, bestMatch);
946
+ }
947
+ return operationTypeMap;
948
+ };
949
+ const SWIFT_PRIMITIVE_TYPES = new Set([
950
+ "String",
951
+ "Int",
952
+ "Double",
953
+ "Bool",
954
+ "Date",
955
+ "Void",
956
+ "Foundation.Data"
957
+ ]);
958
+ const resolveType = (typeName, currentOperation, context, scope = "actor") => {
959
+ const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
960
+ const optional = typeName.endsWith("?");
961
+ const base = optional ? typeName.slice(0, -1) : typeName;
962
+ if (SWIFT_PRIMITIVE_TYPES.has(base)) return typeName;
963
+ if (base.startsWith("[") && base.endsWith("]")) {
964
+ const resolved = resolveType(base.slice(1, -1), currentOperation, context, scope);
965
+ return optional ? `[${resolved}]?` : `[${resolved}]`;
966
+ }
967
+ if (base === "MultipartFile" || base === "ValidationError" || base === "ValidationIssue") {
968
+ const qualified = `${clientName}.${base}`;
969
+ return optional ? `${qualified}?` : qualified;
970
+ }
971
+ if (base === "AnyCodable") {
972
+ const qualified = `${clientName}.JSONValue`;
973
+ return optional ? `${qualified}?` : qualified;
974
+ }
975
+ if (fileLevelTypeNames.has(base)) return optional ? `${base}?` : base;
976
+ const owningStruct = context.ownedTypeMap.get(base);
977
+ if (owningStruct !== void 0) {
978
+ const short = shortTypeName(base, owningStruct);
979
+ const ownerResolved = resolveType(owningStruct, currentOperation, context, scope).replace(/\?$/, "");
980
+ return optional ? `${ownerResolved}.${short}?` : `${ownerResolved}.${short}`;
981
+ }
982
+ const owningOp = operationTypeMap.get(base);
983
+ if (owningOp === void 0) return optional ? `${namespaceName}.${base}?` : `${namespaceName}.${base}`;
984
+ const short = localTypeName(base, owningOp);
985
+ if (owningOp === currentOperation && scope === "operation-enum") return optional ? `${short}?` : short;
986
+ return optional ? `${clientName}.${owningOp}.${short}?` : `${clientName}.${owningOp}.${short}`;
987
+ };
988
+ const localizeType = (type, operationName, context) => {
989
+ const shortName = localTypeName(type.name, operationName);
990
+ if (type.kind === "struct") return {
991
+ ...type,
992
+ name: shortName,
993
+ fields: type.fields.map((field) => ({
994
+ ...field,
995
+ type: resolveType(field.type, operationName, context, "operation-enum")
996
+ }))
997
+ };
998
+ if (type.kind === "enum") return {
999
+ ...type,
1000
+ name: shortName
1001
+ };
1002
+ return {
1003
+ ...type,
1004
+ name: shortName,
1005
+ variants: type.variants.map((variant) => ({
1006
+ ...variant,
1007
+ payloadType: resolveType(variant.payloadType, operationName, context, "operation-enum")
1008
+ }))
1009
+ };
1010
+ };
1011
+ const streamElementType = (method, context, scope) => {
1012
+ const stream = method.stream;
1013
+ if (stream.mode === "text") return "String";
1014
+ if (stream.mode === "binary") return "Foundation.Data";
1015
+ if (stream.events) return scope === "operation-enum" ? "Event" : `${context.clientName}.${method.operationName}.Event`;
1016
+ return resolveType(stream.messageType, method.operationName, context, scope);
1017
+ };
1018
+ const emitEventEnum = (writer, method, context) => {
1019
+ const events = method.stream?.events;
1020
+ if (!events) return;
1021
+ writer.blank();
1022
+ writer.block("public enum Event: Sendable, Equatable", () => {
1023
+ for (const event of events) writer.line(`case ${escapeKeyword(event.caseName)}(${resolveType(event.type, method.operationName, context, "operation-enum")})`);
1024
+ });
1025
+ };
1026
+ /**
1027
+ * The per-route call tracker, for a stream that carries tool events. It folds
1028
+ * the events into one row per call, keyed by the `id` tying a call to its
1029
+ * result, so a view renders a list rather than a switch.
1030
+ */
1031
+ const emitToolTracking = (writer, method) => {
1032
+ const events = method.stream?.events;
1033
+ if (!events) return;
1034
+ const names = new Set(events.map((event) => event.name));
1035
+ if (!names.has("tool_call") || !names.has("tool_result")) return;
1036
+ writer.blank();
1037
+ writer.block("public struct ToolCallRecord: Identifiable, Sendable, Equatable", () => {
1038
+ writer.line("/// How far along the call is.");
1039
+ writer.block("public enum State: Sendable, Equatable", () => {
1040
+ writer.line("case running");
1041
+ writer.line("case done");
1042
+ writer.line("case failed");
1043
+ });
1044
+ writer.blank();
1045
+ writer.line("public let id: String");
1046
+ writer.line("public let name: String");
1047
+ writer.line("public var state: State");
1048
+ writer.line("/// The call as it arrived, to read its input.");
1049
+ writer.line("public var call: ToolCall?");
1050
+ writer.line("/// The result once it answered, to read its output.");
1051
+ writer.line("public var result: ToolResult?");
1052
+ writer.line("/// What the tool reported when it failed.");
1053
+ writer.line("public var message: String?");
1054
+ });
1055
+ writer.blank();
1056
+ writer.line("/// Fold a stream's events into one row per tool call, in the order the calls arrived.");
1057
+ writer.block("public static func readToolCalls(_ events: [Event]) -> [ToolCallRecord]", () => {
1058
+ writer.line("var order: [String] = []");
1059
+ writer.line("var calls: [String: ToolCallRecord] = [:]");
1060
+ writer.blank();
1061
+ writer.block("func at(_ id: String, _ name: String) -> ToolCallRecord", () => {
1062
+ writer.line("if let existing = calls[id] { return existing }");
1063
+ writer.line("order.append(id)");
1064
+ writer.line("return ToolCallRecord(id: id, name: name, state: .running, call: nil, result: nil, message: nil)");
1065
+ });
1066
+ writer.blank();
1067
+ writer.block("for event in events", () => {
1068
+ writer.line("switch event {");
1069
+ if (names.has("tool_call")) {
1070
+ writer.line("case .tool_call(let payload):");
1071
+ writer.line(" var tracked = at(payload.id, payload.name)");
1072
+ writer.line(" tracked.call = payload");
1073
+ writer.line(" calls[payload.id] = tracked");
1074
+ }
1075
+ if (names.has("tool_result")) {
1076
+ writer.line("case .tool_result(let payload):");
1077
+ writer.line(" var tracked = at(payload.id, payload.name)");
1078
+ writer.line(" tracked.state = .done");
1079
+ writer.line(" tracked.result = payload");
1080
+ writer.line(" calls[payload.id] = tracked");
1081
+ }
1082
+ if (names.has("tool_error")) {
1083
+ writer.line("case .tool_error(let payload):");
1084
+ writer.line(" var tracked = at(payload.id, payload.name.rawValue)");
1085
+ writer.line(" tracked.state = .failed");
1086
+ writer.line(" tracked.message = payload.message");
1087
+ writer.line(" calls[payload.id] = tracked");
1088
+ }
1089
+ writer.line("default:");
1090
+ writer.line(" continue");
1091
+ writer.line("}");
1092
+ });
1093
+ writer.blank();
1094
+ writer.line("return order.compactMap { calls[$0] }");
1095
+ });
1096
+ };
1097
+ const emitResultStruct = (writer, method, context) => {
1098
+ if (!method.resultWrapperName) return;
1099
+ writer.blank();
1100
+ writer.block("public struct Result: Sendable", () => {
1101
+ const bodyType = method.stream ? `AsyncThrowingStream<${streamElementType(method, context, "operation-enum")}, Swift.Error>` : method.successSumEnumName ? "Success" : resolveType(method.successReturnType, method.operationName, context, "operation-enum");
1102
+ const hasHeaders = method.resultHeaderFields.length > 0;
1103
+ writer.line(`public let body: ${bodyType}`);
1104
+ if (hasHeaders) {
1105
+ writer.line("public let headers: Headers");
1106
+ writer.blank();
1107
+ writer.block("public struct Headers: Sendable", () => {
1108
+ for (const field of method.resultHeaderFields) {
1109
+ const rawType = resolveType(field.type, method.operationName, context, "operation-enum");
1110
+ writer.line(`public let ${escapeKeyword(field.name)}: ${optionalize(rawType, field.optional)}`);
1111
+ }
1112
+ });
1113
+ }
1114
+ writer.blank();
1115
+ if (hasHeaders) writer.block(`public init(body: ${bodyType}, headers: Headers)`, () => {
1116
+ writer.line("self.body = body");
1117
+ writer.line("self.headers = headers");
1118
+ });
1119
+ else writer.block(`public init(body: ${bodyType})`, () => {
1120
+ writer.line("self.body = body");
1121
+ });
1122
+ });
1123
+ };
1124
+ const emitGroupStruct = (writer, structName, factoryName, fields, method, context) => {
1125
+ const resolved = fields.map((field) => ({
1126
+ ...field,
1127
+ type: resolveType(field.type, method.operationName, context, "operation-enum")
1128
+ }));
1129
+ writer.blank();
1130
+ writer.block(`public struct ${structName}: Sendable`, () => {
1131
+ for (const field of resolved) writer.line(`public let ${escapeKeyword(field.name)}: ${optionalize(field.type, field.optional)}`);
1132
+ writer.blank();
1133
+ emitMemberwiseInit(writer, resolved);
1134
+ writer.blank();
1135
+ emitNamedFactory(writer, factoryName, resolved);
1136
+ });
1137
+ };
1138
+ const emitBodyType = (writer, method, context) => {
1139
+ const body = method.body;
1140
+ if (!body || body.kind === "json-empty") return;
1141
+ const operationName = method.operationName;
1142
+ writer.blank();
1143
+ if (body.kind === "multipart") {
1144
+ const resolved = body.flattened.map((field) => ({
1145
+ ...field,
1146
+ type: resolveType(field.type, operationName, context, "operation-enum")
1147
+ }));
1148
+ writer.block("public struct Body: Sendable", () => {
1149
+ for (const field of resolved) writer.line(`public let ${escapeKeyword(field.name)}: ${optionalize(field.type, field.optional)}`);
1150
+ writer.blank();
1151
+ emitMemberwiseInit(writer, resolved);
1152
+ writer.blank();
1153
+ emitNamedFactory(writer, "body", resolved);
1154
+ });
1155
+ return;
1156
+ }
1157
+ const payloadType = body.structName ? resolveType(body.structName, operationName, context, "operation-enum") : `${context.clientName}.JSONValue`;
1158
+ writer.block("public struct Body: Sendable", () => {
1159
+ writer.line(`public let payload: ${payloadType}`);
1160
+ writer.blank();
1161
+ writer.block(`public init(payload: ${payloadType})`, () => {
1162
+ writer.line("self.payload = payload");
1163
+ });
1164
+ writer.blank();
1165
+ if (body.kind === "json-flat") emitNamedFactory(writer, "body", body.flattened.map((field) => ({
1166
+ ...field,
1167
+ type: resolveType(field.type, operationName, context, "operation-enum")
1168
+ })), (args) => `.init(payload: ${payloadType}(${args}))`);
1169
+ else writer.block(`public static func body(_ value: ${payloadType}) -> Self`, () => {
1170
+ writer.line(".init(payload: value)");
1171
+ });
1172
+ });
1173
+ };
1174
+ const emitRequestGroupStructs = (writer, method, context) => {
1175
+ if (method.pathParams.length > 0) emitGroupStruct(writer, "Params", "params", pathParamFields(method), method, context);
1176
+ if (method.query.length > 0) emitGroupStruct(writer, "Query", "query", method.query, method, context);
1177
+ if (method.headers.length > 0) emitGroupStruct(writer, "Headers", "headers", method.headers, method, context);
1178
+ emitBodyType(writer, method, context);
1179
+ };
1180
+ const emitFailureEnum = (writer, method, context) => {
1181
+ writer.blank();
1182
+ const hasDecodedResponse = method.successReturnType !== "Void" || method.errorCases.some((c) => c.type !== "Void");
1183
+ const failureProtocol = hasDecodedResponse ? "KizunaDecodableFailure" : "KizunaFailure";
1184
+ writer.block(`public enum Failure: Swift.Error, Sendable, ${failureProtocol}`, () => {
1185
+ writer.line("case requestFailed(Swift.Error)");
1186
+ writer.line("case invalidRequest");
1187
+ writer.line("case cancelled");
1188
+ writer.line("case invalidResponse");
1189
+ if (hasDecodedResponse) writer.line("case decoding(Swift.Error, statusCode: Int, data: Foundation.Data)");
1190
+ writer.line("case unexpectedStatus(Int, Foundation.Data)");
1191
+ for (const errorCase of method.errorCases) if (errorCase.type === "Void") writer.line(`case ${escapeKeyword(errorCase.caseName)}`);
1192
+ else {
1193
+ const resolved = resolveType(errorCase.type, method.operationName, context, "operation-enum");
1194
+ writer.line(`case ${escapeKeyword(errorCase.caseName)}(${resolved})`);
1195
+ }
1196
+ writer.blank();
1197
+ writer.block("public var isCancelled: Bool", () => {
1198
+ writer.line("if case .cancelled = self { return true }");
1199
+ writer.line("return false");
1200
+ });
1201
+ });
1202
+ };
1203
+ const emitSuccessSumEnum = (writer, method, context) => {
1204
+ if (!method.successSumEnumName) return;
1205
+ writer.blank();
1206
+ writer.block("public enum Success: Sendable, Equatable", () => {
1207
+ for (const successResponse of method.successResponses) {
1208
+ const resolved = resolveType(successResponse.type, method.operationName, context, "operation-enum");
1209
+ if (resolved === "Void") writer.line(`case status${successResponse.status}`);
1210
+ else writer.line(`case status${successResponse.status}(${resolved})`);
1211
+ }
1212
+ });
1213
+ };
1214
+ const emitKizunaFailureProtocols = (writer) => {
1215
+ writer.blank();
1216
+ writer.block("public protocol KizunaFailure: Swift.Error", () => {
1217
+ writer.line("static func requestFailed(_ error: Swift.Error) -> Self");
1218
+ writer.line("static var invalidRequest: Self { get }");
1219
+ writer.line("static var cancelled: Self { get }");
1220
+ writer.line("static var invalidResponse: Self { get }");
1221
+ writer.line("static func unexpectedStatus(_ status: Int, _ data: Foundation.Data) -> Self");
1222
+ writer.line("var isCancelled: Bool { get }");
1223
+ });
1224
+ writer.blank();
1225
+ writer.block("public protocol KizunaDecodableFailure: KizunaFailure", () => {
1226
+ writer.line("static func decoding(_ error: Swift.Error, statusCode: Int, data: Foundation.Data) -> Self");
1227
+ });
1228
+ };
1229
+ const closure = (writer, opener, body) => {
1230
+ writer.line(opener);
1231
+ writer.indent(body);
1232
+ writer.line("}");
1233
+ };
1234
+ const emitKizunaStreaming = (writer) => {
1235
+ writer.blank();
1236
+ writer.block("struct ServerSentEvent: Sendable", () => {
1237
+ writer.line("let event: String?");
1238
+ writer.line("let data: String");
1239
+ writer.line("let id: String?");
1240
+ writer.line("let retry: Int?");
1241
+ });
1242
+ writer.blank();
1243
+ writer.block("static func open<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?, failure: Failure.Type) async throws(Failure) -> (URLSession.AsyncBytes, Int, HTTPURLResponse)", () => {
1244
+ writer.line("if let requestMiddleware {");
1245
+ writer.line(" do { try await requestMiddleware(&request) }");
1246
+ writer.line(" catch is CancellationError { throw Failure.cancelled }");
1247
+ writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1248
+ writer.line(" catch { throw Failure.requestFailed(error) }");
1249
+ writer.line("}");
1250
+ writer.line("let bytes: URLSession.AsyncBytes");
1251
+ writer.line("let response: URLResponse");
1252
+ writer.line("do { (bytes, response) = try await session.bytes(for: request) }");
1253
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1254
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1255
+ writer.line("catch { throw Failure.requestFailed(error) }");
1256
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1257
+ writer.line("return (bytes, httpResponse.statusCode, httpResponse)");
1258
+ });
1259
+ writer.blank();
1260
+ writer.block("static func collect<Failure: KizunaFailure>(_ bytes: URLSession.AsyncBytes, failure: Failure.Type) async throws(Failure) -> Foundation.Data", () => {
1261
+ writer.line("var data = Foundation.Data()");
1262
+ writer.line("do { for try await byte in bytes { data.append(byte) } }");
1263
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1264
+ writer.line("catch { throw Failure.requestFailed(error) }");
1265
+ writer.line("return data");
1266
+ });
1267
+ writer.blank();
1268
+ writer.block("static func lines(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<String, Swift.Error>", () => {
1269
+ closure(writer, "AsyncThrowingStream { continuation in", () => {
1270
+ writer.block("let task = Task", () => {
1271
+ writer.line("var buffer: [UInt8] = []");
1272
+ writer.line("var pendingCarriageReturn = false");
1273
+ writer.block("do", () => {
1274
+ writer.block("for try await byte in bytes", () => {
1275
+ writer.line("if pendingCarriageReturn {");
1276
+ writer.line(" pendingCarriageReturn = false");
1277
+ writer.line(" if byte == 0x0A { continue }");
1278
+ writer.line("}");
1279
+ writer.line("if byte == 0x0A || byte == 0x0D {");
1280
+ writer.line(" pendingCarriageReturn = byte == 0x0D");
1281
+ writer.line(" continuation.yield(String(decoding: buffer, as: UTF8.self))");
1282
+ writer.line(" buffer.removeAll(keepingCapacity: true)");
1283
+ writer.line(" continue");
1284
+ writer.line("}");
1285
+ writer.line("buffer.append(byte)");
1286
+ });
1287
+ writer.line("if !buffer.isEmpty { continuation.yield(String(decoding: buffer, as: UTF8.self)) }");
1288
+ writer.line("continuation.finish()");
1289
+ });
1290
+ writer.line("catch { continuation.finish(throwing: error) }");
1291
+ });
1292
+ writer.line("continuation.onTermination = { _ in task.cancel() }");
1293
+ });
1294
+ });
1295
+ writer.blank();
1296
+ writer.block("static func chunks(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<Foundation.Data, Swift.Error>", () => {
1297
+ closure(writer, "AsyncThrowingStream { continuation in", () => {
1298
+ writer.block("let task = Task", () => {
1299
+ writer.line("var buffer = Foundation.Data()");
1300
+ writer.block("do", () => {
1301
+ writer.block("for try await byte in bytes", () => {
1302
+ writer.line("buffer.append(byte)");
1303
+ writer.line("if buffer.count >= 16_384 {");
1304
+ writer.line(" continuation.yield(buffer)");
1305
+ writer.line(" buffer = Foundation.Data()");
1306
+ writer.line("}");
1307
+ });
1308
+ writer.line("if !buffer.isEmpty { continuation.yield(buffer) }");
1309
+ writer.line("continuation.finish()");
1310
+ });
1311
+ writer.line("catch { continuation.finish(throwing: error) }");
1312
+ });
1313
+ writer.line("continuation.onTermination = { _ in task.cancel() }");
1314
+ });
1315
+ });
1316
+ writer.blank();
1317
+ writer.block("static func serverSentEvents(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
1318
+ closure(writer, "AsyncThrowingStream { continuation in", () => {
1319
+ writer.block("let task = Task", () => {
1320
+ writer.line("var eventType = \"\"");
1321
+ writer.line("var dataLines: [String] = []");
1322
+ writer.line("var lastEventId = \"\"");
1323
+ writer.line("var retry: Int? = nil");
1324
+ writer.block("do", () => {
1325
+ writer.block("for try await line in lines(bytes)", () => {
1326
+ writer.line("if line.isEmpty {");
1327
+ writer.line(" if !dataLines.isEmpty {");
1328
+ writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
1329
+ writer.line(" dataLines = []");
1330
+ writer.line(" retry = nil");
1331
+ writer.line(" }");
1332
+ writer.line(" eventType = \"\"");
1333
+ writer.line(" continue");
1334
+ writer.line("}");
1335
+ writer.line("if line.hasPrefix(\":\") { continue }");
1336
+ writer.line("let field: Substring");
1337
+ writer.line("var value: Substring");
1338
+ writer.line("if let separator = line.firstIndex(of: \":\") {");
1339
+ writer.line(" field = line[..<separator]");
1340
+ writer.line(" value = line[line.index(after: separator)...]");
1341
+ writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
1342
+ writer.line("} else {");
1343
+ writer.line(" field = Substring(line)");
1344
+ writer.line(" value = \"\"");
1345
+ writer.line("}");
1346
+ writer.line("switch field {");
1347
+ writer.line("case \"event\": eventType = String(value)");
1348
+ writer.line("case \"data\": dataLines.append(String(value))");
1349
+ writer.line("case \"id\": if !value.contains(\"\\0\") { lastEventId = String(value) }");
1350
+ writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy(\\.isNumber), let milliseconds = Int(value) { retry = milliseconds }");
1351
+ writer.line("default: break");
1352
+ writer.line("}");
1353
+ });
1354
+ writer.line("continuation.finish()");
1355
+ });
1356
+ writer.line("catch { continuation.finish(throwing: error) }");
1357
+ });
1358
+ writer.line("continuation.onTermination = { _ in task.cancel() }");
1359
+ });
1360
+ });
1361
+ writer.blank();
1362
+ writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
1363
+ closure(writer, "AsyncThrowingStream { continuation in", () => {
1364
+ writer.block("let task = Task", () => {
1365
+ writer.block("do", () => {
1366
+ writer.block("for try await event in serverSentEvents(bytes)", () => {
1367
+ writer.line("if let decoded = try decode(event, decoder) { continuation.yield(decoded) }");
1368
+ });
1369
+ writer.line("continuation.finish()");
1370
+ });
1371
+ writer.line("catch { continuation.finish(throwing: error) }");
1372
+ });
1373
+ writer.line("continuation.onTermination = { _ in task.cancel() }");
1374
+ });
1375
+ });
1376
+ };
1377
+ const emitKizunaNamespace = (writer, options) => {
1378
+ writer.blank();
1379
+ writer.block("private enum Kizuna", () => {
1380
+ writer.block("@Sendable static func decodeDate(_ decoder: Decoder) throws -> Date", () => {
1381
+ writer.line("let container = try decoder.singleValueContainer()");
1382
+ writer.line("let raw = try container.decode(String.self)");
1383
+ writer.line("if let date = try? Date(raw, strategy: Date.ISO8601FormatStyle(includingFractionalSeconds: true)) { return date }");
1384
+ writer.line("if let date = try? Date(raw, strategy: Date.ISO8601FormatStyle()) { return date }");
1385
+ writer.line("throw DecodingError.dataCorruptedError(in: container, debugDescription: \"Invalid ISO8601 date: \\(raw)\")");
1386
+ });
1387
+ writer.blank();
1388
+ writer.block("static func encodeDate(_ date: Date) -> String", () => {
1389
+ writer.line("date.formatted(Date.ISO8601FormatStyle(includingFractionalSeconds: true))");
1390
+ });
1391
+ writer.blank();
1392
+ writer.block("static func makeJSONEncoder() -> JSONEncoder", () => {
1393
+ writer.line("let encoder = JSONEncoder()");
1394
+ writer.line("encoder.dateEncodingStrategy = .custom { date, encoder in");
1395
+ writer.line(" var container = encoder.singleValueContainer()");
1396
+ writer.line(" try container.encode(encodeDate(date))");
1397
+ writer.line("}");
1398
+ writer.line("return encoder");
1399
+ });
1400
+ writer.blank();
1401
+ writer.block("static func makeJSONDecoder() -> JSONDecoder", () => {
1402
+ writer.line("let decoder = JSONDecoder()");
1403
+ writer.line("decoder.dateDecodingStrategy = .custom(decodeDate)");
1404
+ writer.line("return decoder");
1405
+ });
1406
+ writer.blank();
1407
+ writer.line("private static let pathSegmentAllowed: CharacterSet = {");
1408
+ writer.line(" var allowed = CharacterSet.urlPathAllowed");
1409
+ writer.line(" allowed.remove(charactersIn: \"/\")");
1410
+ writer.line(" return allowed");
1411
+ writer.line("}()");
1412
+ writer.blank();
1413
+ writer.block("static func encodePathSegment<Value>(_ value: Value) -> String", () => {
1414
+ writer.line("let text = stringifyQueryValue(value).first ?? \"\"");
1415
+ writer.line("return text.addingPercentEncoding(withAllowedCharacters: pathSegmentAllowed) ?? text");
1416
+ });
1417
+ writer.blank();
1418
+ writer.block("static func appendPath(_ base: URL, _ path: String) -> URL", () => {
1419
+ writer.line("base.appending(path: path, directoryHint: .notDirectory)");
1420
+ });
1421
+ writer.blank();
1422
+ writer.block("static func stringifyQueryValue(_ value: Any) -> [String]", () => {
1423
+ writer.line("switch value {");
1424
+ writer.line("case let date as Date:");
1425
+ writer.line(" return [encodeDate(date)]");
1426
+ writer.line("case let array as [Any]:");
1427
+ writer.line(" return array.flatMap { stringifyQueryValue($0) }");
1428
+ writer.line("case let raw as any RawRepresentable where raw.rawValue is CustomStringConvertible:");
1429
+ writer.line(" return [String(describing: raw.rawValue)]");
1430
+ writer.line("case let convertible as CustomStringConvertible:");
1431
+ writer.line(" return [convertible.description]");
1432
+ writer.line("default:");
1433
+ writer.line(" return [String(describing: value)]");
1434
+ writer.line("}");
1435
+ });
1436
+ writer.blank();
1437
+ writer.block("static func queryItems<Value>(name: String, value: Value?) -> [URLQueryItem]", () => {
1438
+ writer.line("guard let value else { return [] }");
1439
+ writer.line("return stringifyQueryValue(value).map { URLQueryItem(name: name, value: $0) }");
1440
+ });
1441
+ writer.blank();
1442
+ writer.block("static func setHeader<Value>(_ request: inout URLRequest, name: String, value: Value?)", () => {
1443
+ writer.line("guard let value else { return }");
1444
+ writer.line("request.setValue(stringifyQueryValue(value).joined(separator: \", \"), forHTTPHeaderField: name)");
1445
+ });
1446
+ writer.blank();
1447
+ writer.block("static func makeURL<Failure: KizunaFailure>(baseURL: URL, path: String, queryItems: [URLQueryItem], failure: Failure.Type) throws(Failure) -> URL", () => {
1448
+ writer.line("guard var components = URLComponents(url: appendPath(baseURL, path), resolvingAgainstBaseURL: false) else {");
1449
+ writer.line(" throw Failure.invalidRequest");
1450
+ writer.line("}");
1451
+ writer.line("if !queryItems.isEmpty { components.queryItems = queryItems }");
1452
+ writer.line("guard let url = components.url else { throw Failure.invalidRequest }");
1453
+ writer.line("return url");
1454
+ });
1455
+ writer.blank();
1456
+ writer.block("static func encodeBody<Value: Encodable, Failure: KizunaFailure>(_ request: inout URLRequest, value: Value, using encoder: JSONEncoder, failure: Failure.Type) throws(Failure)", () => {
1457
+ writer.line("do { request.httpBody = try encoder.encode(value) }");
1458
+ writer.line("catch { throw Failure.requestFailed(error) }");
1459
+ });
1460
+ writer.blank();
1461
+ writer.block("static func send<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?, failure: Failure.Type) async throws(Failure) -> (Foundation.Data, Int, HTTPURLResponse)", () => {
1462
+ writer.line("if let requestMiddleware {");
1463
+ writer.line(" do { try await requestMiddleware(&request) }");
1464
+ writer.line(" catch is CancellationError { throw Failure.cancelled }");
1465
+ writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1466
+ writer.line(" catch { throw Failure.requestFailed(error) }");
1467
+ writer.line("}");
1468
+ writer.line("let data: Foundation.Data");
1469
+ writer.line("let response: URLResponse");
1470
+ writer.line("do { (data, response) = try await session.data(for: request) }");
1471
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1472
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1473
+ writer.line("catch { throw Failure.requestFailed(error) }");
1474
+ writer.line("if let responseMiddleware { await responseMiddleware(request, data, response) }");
1475
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1476
+ writer.line("return (data, httpResponse.statusCode, httpResponse)");
1477
+ });
1478
+ writer.blank();
1479
+ writer.block("static func decode<Value: Decodable, Failure: KizunaDecodableFailure>(_ type: Value.Type, from data: Foundation.Data, using decoder: JSONDecoder, statusCode: Int, failure: Failure.Type) throws(Failure) -> Value", () => {
1480
+ writer.line("do { return try decoder.decode(Value.self, from: data) }");
1481
+ writer.line("catch { throw Failure.decoding(error, statusCode: statusCode, data: data) }");
1482
+ });
1483
+ if (options.multiError) {
1484
+ writer.blank();
1485
+ writer.block("static func firstError<Failure: KizunaDecodableFailure>(statusCode: Int, data: Foundation.Data, _ attempts: [() -> Failure?]) -> Failure", () => {
1486
+ writer.line("for attempt in attempts {");
1487
+ writer.line(" if let failure = attempt() { return failure }");
1488
+ writer.line("}");
1489
+ writer.line("return Failure.decoding(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: \"No matching type for status \\(statusCode)\")), statusCode: statusCode, data: data)");
1490
+ });
1491
+ }
1492
+ if (options.streaming) emitKizunaStreaming(writer);
1493
+ if (options.multipart) {
1494
+ writer.blank();
1495
+ writer.block("struct MultipartBuilder", () => {
1496
+ writer.line("let boundary: String");
1497
+ writer.line("private var data = Foundation.Data()");
1498
+ writer.blank();
1499
+ writer.block("init(boundary: String = \"kizuna-\\(UUID().uuidString)\")", () => {
1500
+ writer.line("self.boundary = boundary");
1501
+ });
1502
+ writer.blank();
1503
+ writer.block("mutating func appendField(name: String, value: String)", () => {
1504
+ writer.line("data.append(\"--\\(boundary)\\r\\n\".data(using: .utf8)!)");
1505
+ writer.line("data.append(\"Content-Disposition: form-data; name=\\\"\\(name)\\\"\\r\\n\\r\\n\".data(using: .utf8)!)");
1506
+ writer.line("data.append(value.data(using: .utf8)!)");
1507
+ writer.line("data.append(\"\\r\\n\".data(using: .utf8)!)");
1508
+ });
1509
+ writer.blank();
1510
+ writer.block(`mutating func appendFile(name: String, file: ${options.clientName}.MultipartFile)`, () => {
1511
+ writer.line("data.append(\"--\\(boundary)\\r\\n\".data(using: .utf8)!)");
1512
+ writer.line("data.append(\"Content-Disposition: form-data; name=\\\"\\(name)\\\"; filename=\\\"\\(file.filename)\\\"\\r\\n\".data(using: .utf8)!)");
1513
+ writer.line("data.append(\"Content-Type: \\(file.mimeType)\\r\\n\\r\\n\".data(using: .utf8)!)");
1514
+ writer.line("data.append(file.data)");
1515
+ writer.line("data.append(\"\\r\\n\".data(using: .utf8)!)");
1516
+ });
1517
+ writer.blank();
1518
+ writer.block("func finalize() -> Foundation.Data", () => {
1519
+ writer.line("var result = data");
1520
+ writer.line("result.append(\"--\\(boundary)--\\r\\n\".data(using: .utf8)!)");
1521
+ writer.line("return result");
1522
+ });
1523
+ writer.blank();
1524
+ writer.block("var contentType: String", () => {
1525
+ writer.line("\"multipart/form-data; boundary=\\(boundary)\"");
1526
+ });
1527
+ });
1528
+ }
1529
+ });
1530
+ };
1531
+ const pathParamFields = (method) => method.pathParams.map((name) => {
1532
+ const declared = method.declaredPathParams.find((field) => field.wireName === name);
1533
+ return declared ? {
1534
+ ...declared,
1535
+ optional: false
1536
+ } : {
1537
+ name,
1538
+ wireName: name,
1539
+ type: "String",
1540
+ optional: false
1541
+ };
1542
+ });
1543
+ const registryStruct = (typeName, context) => {
1544
+ const found = context.registry.get(typeName);
1545
+ return found && found.kind === "struct" && found.fields.length > 0 ? found : void 0;
1546
+ };
1547
+ const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
1548
+ const groupTypeRef = (operationName, structName, context) => `${context.clientName}.${operationName}.${structName}`;
1549
+ const bodyParamType = (method, context) => {
1550
+ if (!method.body || method.body.kind === "json-empty") return void 0;
1551
+ return groupTypeRef(method.operationName, "Body", context);
1552
+ };
1553
+ const methodGroups = (method, context) => {
1554
+ const operationName = method.operationName;
1555
+ const groups = [];
1556
+ if (method.pathParams.length > 0) groups.push({
1557
+ varName: "params",
1558
+ type: groupTypeRef(operationName, "Params", context),
1559
+ factory: "params",
1560
+ required: true
1561
+ });
1562
+ const bodyType = bodyParamType(method, context);
1563
+ if (bodyType) groups.push({
1564
+ varName: "body",
1565
+ type: bodyType,
1566
+ factory: "body",
1567
+ required: true
1568
+ });
1569
+ if (method.query.length > 0) groups.push({
1570
+ varName: "query",
1571
+ type: groupTypeRef(operationName, "Query", context),
1572
+ factory: "query",
1573
+ required: method.query.some((field) => !field.optional)
1574
+ });
1575
+ if (method.headers.length > 0) groups.push({
1576
+ varName: "headers",
1577
+ type: groupTypeRef(operationName, "Headers", context),
1578
+ factory: "headers",
1579
+ required: method.headers.some((field) => !field.optional)
1580
+ });
1581
+ return [...groups.filter((group) => group.required), ...groups.filter((group) => !group.required)];
1582
+ };
1583
+ const buildMethodParameters = (method, context) => methodGroups(method, context).map((group) => `_ ${group.varName}: ${group.type}${group.required ? "" : ` = .${group.factory}()`}`);
1584
+ const deprecatedAttribute = (message) => {
1585
+ if (message === void 0 || message === "") return "@available(*, deprecated)";
1586
+ return `@available(*, deprecated, message: ${stringLiteral(message)})`;
1587
+ };
1588
+ const failureRef = (method, context) => `${context.clientName}.${method.operationName}.${method.failureEnumName}`;
1589
+ /**
1590
+ * Statement that binds `name` to the decoded response body. JSON responses go
1591
+ * through `Kizuna.decode`; binary responses are the raw `Data`; other raw
1592
+ * (non-JSON) responses are read as a UTF-8 string.
1593
+ */
1594
+ const decodeBodyStatement = (name, resolvedType, response, failure, receiver = "") => {
1595
+ if (response.isBinary) return ` let ${name} = data`;
1596
+ if (response.isRaw) return ` let ${name} = String(decoding: data, as: UTF8.self)`;
1597
+ return ` let ${name} = try Kizuna.decode(${resolvedType}.self, from: data, using: ${receiver}decoder, statusCode: statusCode, failure: ${failure}.self)`;
1598
+ };
1599
+ const hasMultiErrorGroup = (method) => {
1600
+ const counts = /* @__PURE__ */ new Map();
1601
+ for (const errorCase of method.errorCases) counts.set(errorCase.status, (counts.get(errorCase.status) ?? 0) + 1);
1602
+ return [...counts.values()].some((count) => count > 1);
1603
+ };
1604
+ const emitMethodBody = (writer, method, context, receiver = "") => {
1605
+ const failure = failureRef(method, context);
1606
+ const pathDecl = method.pathParams.length > 0 ? "var" : "let";
1607
+ writer.line(`${pathDecl} path = ${stringLiteral(method.pathTemplate)}`);
1608
+ for (const field of pathParamFields(method)) {
1609
+ const accessor = groupMemberAccessor("params", field);
1610
+ writer.line(`path = path.replacingOccurrences(of: ${stringLiteral(`:${field.name}`)}, with: Kizuna.encodePathSegment(${accessor}))`);
1611
+ }
1612
+ let queryItemsExpression = "[]";
1613
+ if (method.query.length > 0) {
1614
+ writer.line("var queryItems: [URLQueryItem] = []");
1615
+ for (const field of method.query) {
1616
+ const accessor = groupMemberAccessor("query", field);
1617
+ writer.line(`queryItems += Kizuna.queryItems(name: ${stringLiteral(field.wireName)}, value: ${accessor})`);
1618
+ }
1619
+ queryItemsExpression = "queryItems";
1620
+ }
1621
+ writer.line(`let url = try Kizuna.makeURL(baseURL: ${receiver}baseURL, path: path, queryItems: ${queryItemsExpression}, failure: ${failure}.self)`);
1622
+ writer.line(`var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: ${receiver}timeout)`);
1623
+ writer.line(`request.httpMethod = ${stringLiteral(method.method)}`);
1624
+ if (context.requestContextFields.length > 0) writer.line(`for (name, value) in ${receiver}requestContextHeaders { request.setValue(value, forHTTPHeaderField: name) }`);
1625
+ if (method.body) emitBodyEncoding(writer, method, context, receiver);
1626
+ for (const field of method.headers) {
1627
+ const accessor = groupMemberAccessor("headers", field);
1628
+ writer.line(`Kizuna.setHeader(&request, name: ${stringLiteral(field.wireName)}, value: ${accessor})`);
1629
+ }
1630
+ const responseBinding = method.resultHeaderFields.length > 0 ? "httpResponse" : "_";
1631
+ if (method.stream) {
1632
+ emitStreamMethodTail(writer, method, context, receiver, responseBinding);
1633
+ return;
1634
+ }
1635
+ writer.line(`let (data, statusCode, ${responseBinding}) = try await Kizuna.send(&request, session: ${receiver}session, requestMiddleware: ${receiver}requestMiddleware, responseMiddleware: ${receiver}responseMiddleware, failure: ${failure}.self)`);
1636
+ writer.line("switch statusCode {");
1637
+ for (const successResponse of method.successResponses) {
1638
+ writer.line(`case ${successResponse.status}:`);
1639
+ if (method.resultWrapperName) {
1640
+ const qualifiedResult = `${context.clientName}.${method.operationName}.${method.resultWrapperName}`;
1641
+ const hasHeaders = method.resultHeaderFields.length > 0;
1642
+ const headersInitArgs = method.resultHeaderFields.map((field) => `${escapeKeyword(field.name)}: ${escapeKeyword(field.name)}`).join(", ");
1643
+ if (method.successSumEnumName) {
1644
+ const resolved = resolveType(successResponse.type, method.operationName, context);
1645
+ if (resolved === "Void") {
1646
+ for (const field of successResponse.responseHeaders) writer.line(` let ${escapeKeyword(field.name)} = httpResponse.value(forHTTPHeaderField: ${stringLiteral(field.wireName)})`);
1647
+ if (hasHeaders) writer.line(` return ${qualifiedResult}(body: .status${successResponse.status}, headers: .init(${headersInitArgs}))`);
1648
+ else writer.line(` return ${qualifiedResult}(body: .status${successResponse.status})`);
1649
+ } else {
1650
+ writer.line(decodeBodyStatement("payload", resolved, successResponse, failure, receiver));
1651
+ for (const field of successResponse.responseHeaders) writer.line(` let ${escapeKeyword(field.name)} = httpResponse.value(forHTTPHeaderField: ${stringLiteral(field.wireName)})`);
1652
+ if (hasHeaders) writer.line(` return ${qualifiedResult}(body: .status${successResponse.status}(payload), headers: .init(${headersInitArgs}))`);
1653
+ else writer.line(` return ${qualifiedResult}(body: .status${successResponse.status}(payload))`);
1654
+ }
1655
+ } else {
1656
+ const resolved = resolveType(successResponse.type, method.operationName, context);
1657
+ writer.line(decodeBodyStatement("body", resolved, successResponse, failure, receiver));
1658
+ for (const field of successResponse.responseHeaders) writer.line(` let ${escapeKeyword(field.name)} = httpResponse.value(forHTTPHeaderField: ${stringLiteral(field.wireName)})`);
1659
+ if (hasHeaders) writer.line(` return ${qualifiedResult}(body: body, headers: .init(${headersInitArgs}))`);
1660
+ else writer.line(` return ${qualifiedResult}(body: body)`);
1661
+ }
1662
+ } else writer.line(" return");
1663
+ }
1664
+ emitErrorCases(writer, method, context, receiver);
1665
+ writer.line("default:");
1666
+ writer.line(` throw ${failure}.unexpectedStatus(statusCode, data)`);
1667
+ writer.line("}");
1668
+ };
1669
+ const emitErrorCases = (writer, method, context, receiver, prelude) => {
1670
+ const failure = failureRef(method, context);
1671
+ const grouped = /* @__PURE__ */ new Map();
1672
+ for (const errorCase of method.errorCases) {
1673
+ const existing = grouped.get(errorCase.status);
1674
+ if (existing) existing.push(errorCase);
1675
+ else grouped.set(errorCase.status, [errorCase]);
1676
+ }
1677
+ for (const [status, cases] of grouped) {
1678
+ writer.line(`case ${status}:`);
1679
+ if (prelude) writer.line(` ${prelude}`);
1680
+ const firstCase = cases[0];
1681
+ if (cases.length === 1 && firstCase) if (firstCase.type === "Void") writer.line(` throw ${failure}.${escapeKeyword(firstCase.caseName)}`);
1682
+ else {
1683
+ const resolved = resolveType(firstCase.type, method.operationName, context);
1684
+ writer.line(decodeBodyStatement("payload", resolved, firstCase, failure, receiver));
1685
+ writer.line(` throw ${failure}.${escapeKeyword(firstCase.caseName)}(payload)`);
1686
+ }
1687
+ else {
1688
+ writer.line(` throw Kizuna.firstError(statusCode: statusCode, data: data, [`);
1689
+ for (const errorCase of cases) {
1690
+ const caseRef = `${failure}.${escapeKeyword(errorCase.caseName)}`;
1691
+ if (errorCase.type === "Void") writer.line(` { ${caseRef} },`);
1692
+ else if (errorCase.isBinary) writer.line(` { ${caseRef}(data) },`);
1693
+ else if (errorCase.isRaw) writer.line(` { ${caseRef}(String(decoding: data, as: UTF8.self)) },`);
1694
+ else {
1695
+ const resolved = resolveType(errorCase.type, method.operationName, context);
1696
+ writer.line(` { (try? ${receiver}decoder.decode(${resolved}.self, from: data)).map(${caseRef}) },`);
1697
+ }
1698
+ }
1699
+ writer.line(" ])");
1700
+ }
1701
+ }
1702
+ };
1703
+ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding) => {
1704
+ const failure = failureRef(method, context);
1705
+ const stream = method.stream;
1706
+ const successResponse = method.successResponses.find((candidate) => candidate.status === stream.status);
1707
+ const qualifiedResult = `${context.clientName}.${method.operationName}.${method.resultWrapperName}`;
1708
+ const hasHeaders = method.resultHeaderFields.length > 0;
1709
+ const collect = `let data = try await Kizuna.collect(bytes, failure: ${failure}.self)`;
1710
+ writer.line(`let (bytes, statusCode, ${responseBinding}) = try await Kizuna.open(&request, session: ${receiver}session, requestMiddleware: ${receiver}requestMiddleware, failure: ${failure}.self)`);
1711
+ writer.line("switch statusCode {");
1712
+ writer.line(`case ${stream.status}:`);
1713
+ writer.indent(() => {
1714
+ if (stream.mode === "text") writer.line("let body = Kizuna.lines(bytes)");
1715
+ else if (stream.mode === "binary") writer.line("let body = Kizuna.chunks(bytes)");
1716
+ else if (stream.events) {
1717
+ const events = stream.events;
1718
+ closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${streamElementType(method, context, "actor")}? in`, () => {
1719
+ writer.line("switch event.event {");
1720
+ for (const event of events) {
1721
+ const payloadType = resolveType(event.type, method.operationName, context);
1722
+ writer.line(`case ${stringLiteral(event.name)}: return .${escapeKeyword(event.caseName)}(try decoder.decode(${payloadType}.self, from: Foundation.Data(event.data.utf8)))`);
1723
+ }
1724
+ writer.line("default: return nil");
1725
+ writer.line("}");
1726
+ });
1727
+ } else {
1728
+ const elementType = streamElementType(method, context, "actor");
1729
+ closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${elementType}? in`, () => {
1730
+ writer.line(`try decoder.decode(${elementType}.self, from: Foundation.Data(event.data.utf8))`);
1731
+ });
1732
+ }
1733
+ for (const field of successResponse.responseHeaders) writer.line(`let ${escapeKeyword(field.name)} = httpResponse.value(forHTTPHeaderField: ${stringLiteral(field.wireName)})`);
1734
+ if (hasHeaders) {
1735
+ const headersInitArgs = method.resultHeaderFields.map((field) => `${escapeKeyword(field.name)}: ${escapeKeyword(field.name)}`).join(", ");
1736
+ writer.line(`return ${qualifiedResult}(body: body, headers: .init(${headersInitArgs}))`);
1737
+ } else writer.line(`return ${qualifiedResult}(body: body)`);
1738
+ });
1739
+ emitErrorCases(writer, method, context, receiver, collect);
1740
+ writer.line("default:");
1741
+ writer.line(` ${collect}`);
1742
+ writer.line(` throw ${failure}.unexpectedStatus(statusCode, data)`);
1743
+ writer.line("}");
1744
+ };
1745
+ const methodSuccessType = (method, context) => {
1746
+ const { clientName } = context;
1747
+ if (method.resultWrapperName) return `${clientName}.${method.operationName}.${method.resultWrapperName}`;
1748
+ if (method.successReturnType === "Void") return "Void";
1749
+ if (method.successSumEnumName) return `${clientName}.${method.operationName}.Success`;
1750
+ return resolveType(method.successReturnType, method.operationName, context);
1751
+ };
1752
+ const buildMethodSignature = (method, context) => {
1753
+ const failure = failureRef(method, context);
1754
+ const params = buildMethodParameters(method, context);
1755
+ const successType = methodSuccessType(method, context);
1756
+ const returnType = successType === "Void" ? "" : ` -> ${successType}`;
1757
+ return `public func ${escapeKeyword(method.name)}(${params.join(", ")}) async throws(${failure})${returnType}`;
1758
+ };
1759
+ const emitMethod = (writer, method, context) => {
1760
+ writer.docComment(method.summary ?? method.description);
1761
+ if (method.deprecated) writer.line(deprecatedAttribute(method.deprecationMessage));
1762
+ writer.block(buildMethodSignature(method, context), () => {
1763
+ emitMethodBody(writer, method, context);
1764
+ });
1765
+ };
1766
+ const emitSubClientMethod = (writer, method, context) => {
1767
+ writer.docComment(method.summary ?? method.description);
1768
+ if (method.deprecated) writer.line(deprecatedAttribute(method.deprecationMessage));
1769
+ writer.block(buildMethodSignature(method, context), () => {
1770
+ emitMethodBody(writer, method, context, "client.");
1771
+ });
1772
+ };
1773
+ const emitSubClientStruct = (writer, group, clientName, context) => {
1774
+ writer.blank();
1775
+ writer.block(`public struct ${group.structName}: Sendable`, () => {
1776
+ writer.line(`private let client: ${clientName}`);
1777
+ writer.blank();
1778
+ writer.block(`init(client: ${clientName})`, () => {
1779
+ writer.line("self.client = client");
1780
+ });
1781
+ for (const method of group.methods) {
1782
+ writer.blank();
1783
+ emitSubClientMethod(writer, method, context);
1784
+ }
1785
+ });
1786
+ };
1787
+ const emitBodyEncoding = (writer, method, context, receiver = "") => {
1788
+ if (!method.body) return;
1789
+ const body = method.body;
1790
+ const failure = failureRef(method, context);
1791
+ if (body.kind === "multipart") {
1792
+ writer.line("var multipart = Kizuna.MultipartBuilder()");
1793
+ for (const field of body.multipartFields) {
1794
+ const flattenedField = body.flattened.find((candidate) => candidate.name === field.name);
1795
+ const accessor = `body.${escapeKeyword(field.name)}`;
1796
+ if (field.isFile || flattenedField?.isFile === true) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
1797
+ else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
1798
+ }
1799
+ writer.line("request.httpBody = multipart.finalize()");
1800
+ writer.line("request.setValue(multipart.contentType, forHTTPHeaderField: \"Content-Type\")");
1801
+ return;
1802
+ }
1803
+ writer.line("request.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")");
1804
+ if (body.kind === "json-empty") {
1805
+ writer.line("request.httpBody = Data(\"{}\".utf8)");
1806
+ return;
1807
+ }
1808
+ writer.line(`try Kizuna.encodeBody(&request, value: body.payload, using: ${receiver}encoder, failure: ${failure}.self)`);
1809
+ };
1810
+ const emitClient = (writer, config, partition, context, typesByOperation) => {
1811
+ const { clientName } = config;
1812
+ const { flatMethods, groups } = partition;
1813
+ const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
1814
+ const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
1815
+ const contextFields = context.requestContextFields;
1816
+ const contextRequired = contextFields.some((field) => !field.optional);
1817
+ writer.blank();
1818
+ writer.block(`public final class ${clientName}: Sendable`, () => {
1819
+ if (contextFields.length > 0) {
1820
+ writer.blank();
1821
+ writer.docComment("Values sent as headers on every request, from the api's request context.");
1822
+ writer.block("public struct RequestContext: Sendable, Equatable", () => {
1823
+ for (const field of contextFields) writer.line(`public var ${escapeKeyword(field.name)}: ${field.type}`);
1824
+ writer.blank();
1825
+ const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${field.optional ? " = nil" : ""}`).join(", ");
1826
+ writer.block(`public init(${initParams})`, () => {
1827
+ for (const field of contextFields) writer.line(`self.${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`);
1828
+ });
1829
+ writer.blank();
1830
+ writer.block("var headerFields: [String: String]", () => {
1831
+ writer.line("var fields: [String: String] = [:]");
1832
+ for (const field of contextFields) if (field.optional) writer.line(`if let ${escapeKeyword(field.name)} { fields[${stringLiteral(field.wireName)}] = ${escapeKeyword(field.name)} }`);
1833
+ else writer.line(`fields[${stringLiteral(field.wireName)}] = ${escapeKeyword(field.name)}`);
1834
+ writer.line("return fields");
1835
+ });
1836
+ });
1837
+ }
1838
+ if (usesMultipart) {
1839
+ writer.blank();
1840
+ writer.block("public struct MultipartFile: Sendable, Equatable", () => {
1841
+ writer.line("public let data: Foundation.Data");
1842
+ writer.line("public let filename: String");
1843
+ writer.line("public let mimeType: String");
1844
+ writer.blank();
1845
+ writer.block("public init(data: Data, filename: String, mimeType: String = \"application/octet-stream\")", () => {
1846
+ writer.line("self.data = data");
1847
+ writer.line("self.filename = filename");
1848
+ writer.line("self.mimeType = mimeType");
1849
+ });
1850
+ });
1851
+ }
1852
+ if (allMethods.some((method) => method.errorCases.some((c) => c.type === "ValidationError"))) {
1853
+ writer.blank();
1854
+ writer.block("public struct ValidationError: Codable, Sendable, Equatable", () => {
1855
+ writer.line("public let type: String");
1856
+ writer.line("public let title: String");
1857
+ writer.line("public let status: Int");
1858
+ writer.line("public let detail: String");
1859
+ writer.line("public let errors: [ValidationIssue]");
1860
+ writer.blank();
1861
+ writer.block("public init(type: String, title: String, status: Int, detail: String, errors: [ValidationIssue])", () => {
1862
+ writer.line("self.type = type");
1863
+ writer.line("self.title = title");
1864
+ writer.line("self.status = status");
1865
+ writer.line("self.detail = detail");
1866
+ writer.line("self.errors = errors");
1867
+ });
1868
+ });
1869
+ writer.blank();
1870
+ writer.block("public struct ValidationIssue: Codable, Sendable, Equatable", () => {
1871
+ writer.line("public let code: String");
1872
+ writer.line("public let path: [String]");
1873
+ writer.line("public let message: String");
1874
+ writer.blank();
1875
+ writer.block("public init(code: String, path: [String], message: String)", () => {
1876
+ writer.line("self.code = code");
1877
+ writer.line("self.path = path");
1878
+ writer.line("self.message = message");
1879
+ });
1880
+ });
1881
+ }
1882
+ if (config.anyCodable) {
1883
+ writer.blank();
1884
+ writer.docComment("A decoded JSON value of unknown shape. Codable and correctly Equatable by structure.");
1885
+ writer.block("public enum JSONValue: Codable, Sendable, Equatable", () => {
1886
+ writer.line("case null");
1887
+ writer.line("case bool(Bool)");
1888
+ writer.line("case int(Int)");
1889
+ writer.line("case double(Double)");
1890
+ writer.line("case string(String)");
1891
+ writer.line("case array([JSONValue])");
1892
+ writer.line("case object([String: JSONValue])");
1893
+ writer.blank();
1894
+ writer.block("public init(from decoder: Decoder) throws", () => {
1895
+ writer.line("let container = try decoder.singleValueContainer()");
1896
+ writer.line("if container.decodeNil() {");
1897
+ writer.line(" self = .null");
1898
+ writer.line("} else if let value = try? container.decode(Bool.self) {");
1899
+ writer.line(" self = .bool(value)");
1900
+ writer.line("} else if let value = try? container.decode(Int.self) {");
1901
+ writer.line(" self = .int(value)");
1902
+ writer.line("} else if let value = try? container.decode(Double.self) {");
1903
+ writer.line(" self = .double(value)");
1904
+ writer.line("} else if let value = try? container.decode(String.self) {");
1905
+ writer.line(" self = .string(value)");
1906
+ writer.line("} else if let value = try? container.decode([JSONValue].self) {");
1907
+ writer.line(" self = .array(value)");
1908
+ writer.line("} else if let value = try? container.decode([String: JSONValue].self) {");
1909
+ writer.line(" self = .object(value)");
1910
+ writer.line("} else {");
1911
+ writer.line(" throw DecodingError.dataCorruptedError(in: container, debugDescription: \"Unsupported JSON value\")");
1912
+ writer.line("}");
1913
+ });
1914
+ writer.blank();
1915
+ writer.block("public func encode(to encoder: Encoder) throws", () => {
1916
+ writer.line("var container = encoder.singleValueContainer()");
1917
+ writer.line("switch self {");
1918
+ writer.line("case .null: try container.encodeNil()");
1919
+ writer.line("case .bool(let value): try container.encode(value)");
1920
+ writer.line("case .int(let value): try container.encode(value)");
1921
+ writer.line("case .double(let value): try container.encode(value)");
1922
+ writer.line("case .string(let value): try container.encode(value)");
1923
+ writer.line("case .array(let value): try container.encode(value)");
1924
+ writer.line("case .object(let value): try container.encode(value)");
1925
+ writer.line("}");
1926
+ });
1927
+ });
1928
+ }
1929
+ writer.line("public let baseURL: URL");
1930
+ writer.line("public let session: URLSession");
1931
+ writer.line("public let timeout: TimeInterval");
1932
+ if (contextFields.length > 0) writer.line("let requestContextHeaders: [String: String]");
1933
+ writer.line("public let requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?");
1934
+ writer.line("public let responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?");
1935
+ writer.line("let encoder: JSONEncoder");
1936
+ writer.line("let decoder: JSONDecoder");
1937
+ writer.blank();
1938
+ const contextInitParam = contextFields.length > 0 ? `, requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}` : "";
1939
+ writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
1940
+ writer.line("self.baseURL = baseURL");
1941
+ writer.line("self.session = session");
1942
+ writer.line("self.timeout = timeout");
1943
+ if (contextFields.length > 0) writer.line("self.requestContextHeaders = requestContext.headerFields");
1944
+ writer.line("self.requestMiddleware = requestMiddleware");
1945
+ writer.line("self.responseMiddleware = responseMiddleware");
1946
+ writer.line("self.encoder = Kizuna.makeJSONEncoder()");
1947
+ writer.line("self.decoder = Kizuna.makeJSONDecoder()");
1948
+ });
1949
+ if (groups.length > 0) for (const group of groups) {
1950
+ writer.blank();
1951
+ writer.block(`public var ${escapeKeyword(group.propertyName)}: ${group.structName}`, () => {
1952
+ writer.line(`${group.structName}(client: self)`);
1953
+ });
1954
+ }
1955
+ for (const method of allMethods) {
1956
+ const localTypes = (typesByOperation.get(method.operationName) ?? []).map((type) => localizeType(type, method.operationName, context));
1957
+ writer.blank();
1958
+ writer.block(`public enum ${method.operationName}`, () => {
1959
+ emitTypes(writer, localTypes, context);
1960
+ emitRequestGroupStructs(writer, method, context);
1961
+ emitSuccessSumEnum(writer, method, context);
1962
+ emitEventEnum(writer, method, context);
1963
+ emitToolTracking(writer, method);
1964
+ emitResultStruct(writer, method, context);
1965
+ emitFailureEnum(writer, method, context);
1966
+ });
1967
+ }
1968
+ for (const method of flatMethods) {
1969
+ writer.blank();
1970
+ emitMethod(writer, method, context);
1971
+ }
1972
+ });
1973
+ };
1974
+ /**
1975
+ * Generate a Swift API client from a kizuna config.
1976
+ *
1977
+ * - `namespaceName`: the actor class. Defaults to `APIClient`.
1978
+ */
1979
+ const generateSwiftClient = (api, options) => {
1980
+ const { namespaceName, camelCaseProperties = false, unknownEnumCase = false } = options;
1981
+ const registry = new TypeRegistry(camelCaseProperties, unknownEnumCase);
1982
+ const partition = swiftGenerator(api, {
1983
+ namespaceName,
1984
+ registry
1985
+ });
1986
+ const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
1987
+ const operationTypeMap = buildOperationTypeMap(allMethods, registry);
1988
+ const clientName = `${namespaceName}Client`;
1989
+ const writer = new SwiftWriter();
1990
+ writer.line("// Generated by @kizunajs/swift. Do not edit by hand.");
1991
+ writer.blank();
1992
+ writer.line("import Foundation");
1993
+ writer.line("#if canImport(FoundationNetworking)");
1994
+ writer.line("import FoundationNetworking");
1995
+ writer.line("#endif");
1996
+ writer.blank();
1997
+ const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
1998
+ const usesMultiError = allMethods.some(hasMultiErrorGroup);
1999
+ const sharedTypes = [];
2000
+ const typesByOperation = /* @__PURE__ */ new Map();
2001
+ for (const type of registry.all()) {
2002
+ const owningOp = operationTypeMap.get(type.name);
2003
+ if (owningOp !== void 0) {
2004
+ const bucket = typesByOperation.get(owningOp) ?? [];
2005
+ if (bucket.length === 0) typesByOperation.set(owningOp, bucket);
2006
+ bucket.push(type);
2007
+ } else sharedTypes.push(type);
2008
+ }
2009
+ const fileLevelTypeNames = /* @__PURE__ */ new Set();
2010
+ const allStructNames = sharedTypes.filter((type) => type.kind === "struct").map((type) => type.name);
2011
+ const ownedTypeMap = /* @__PURE__ */ new Map();
2012
+ const sharedTypeNames = new Set(sharedTypes.map((type) => type.name));
2013
+ for (const type of sharedTypes) {
2014
+ if (registry.isExplicitId(type.name)) continue;
2015
+ const unionOwner = registry.unionVariantOwner(type.name);
2016
+ if (unionOwner !== void 0 && sharedTypeNames.has(unionOwner)) {
2017
+ ownedTypeMap.set(type.name, unionOwner);
2018
+ continue;
2019
+ }
2020
+ let bestMatch;
2021
+ for (const structName of allStructNames) {
2022
+ if (structName === type.name) continue;
2023
+ if (isHintPrefix(type.name, structName) && (!bestMatch || structName.length > bestMatch.length)) bestMatch = structName;
2024
+ }
2025
+ if (bestMatch !== void 0) ownedTypeMap.set(type.name, bestMatch);
2026
+ }
2027
+ const ownedTypeLookup = new Map(sharedTypes.filter((type) => ownedTypeMap.has(type.name)).map((type) => [type.name, type]));
2028
+ const topLevelSharedTypes = sharedTypes.filter((type) => !ownedTypeMap.has(type.name));
2029
+ const requestContextFields = [];
2030
+ for (const declaration of Object.values(api.requestContext ?? {})) {
2031
+ const headersSchema = declaration.headers;
2032
+ if (!headersSchema) continue;
2033
+ requestContextFields.push(...collectObjectFields(headersSchema, registry, "RequestContext"));
2034
+ }
2035
+ const context = {
2036
+ namespaceName,
2037
+ clientName,
2038
+ operationTypeMap,
2039
+ fileLevelTypeNames,
2040
+ ownedTypeMap,
2041
+ registry,
2042
+ requestContextFields
2043
+ };
2044
+ writer.block(`public enum ${namespaceName}`, () => {
2045
+ emitTypes(writer, topLevelSharedTypes, context, ownedTypeMap, ownedTypeLookup);
2046
+ });
2047
+ emitClient(writer, {
2048
+ clientName,
2049
+ anyCodable: registry.usesAnyCodable
2050
+ }, partition, context, typesByOperation);
2051
+ for (const group of partition.groups) emitSubClientStruct(writer, group, clientName, context);
2052
+ emitKizunaFailureProtocols(writer);
2053
+ emitKizunaNamespace(writer, {
2054
+ multipart: usesMultipart,
2055
+ multiError: usesMultiError,
2056
+ streaming: allMethods.some((method) => method.stream !== void 0),
2057
+ clientName
2058
+ });
2059
+ for (const warning of registry.warnings()) process.stderr.write(`[kizuna/swift] AnyCodable fallback at ${warning}\n`);
2060
+ return writer.toString();
2061
+ };
2062
+ //#endregion
2063
+ export { generateSwiftClient as t };