@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.
- package/LICENSE.MD +9 -0
- package/README.md +19 -0
- package/dist/cli.cjs +63 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +64 -0
- package/dist/generator-D2AtmA6u.mjs +2063 -0
- package/dist/generator-v_aUFbbV.cjs +2068 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.mjs +19 -0
- package/package.json +79 -0
|
@@ -0,0 +1,2068 @@
|
|
|
1
|
+
let kizunajs_generator = require("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 (0, kizunajs_generator.sanitizeFieldName)(key);
|
|
99
|
+
const camel = (0, kizunajs_generator.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 = (0, kizunajs_generator.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}${(0, kizunajs_generator.toPascalCase)(key)}`);
|
|
109
|
+
const deprecation = (0, kizunajs_generator.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: (0, kizunajs_generator.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 ((0, kizunajs_generator.isFileSchema)(schema)) return {
|
|
130
|
+
expression: "MultipartFile",
|
|
131
|
+
optional: false,
|
|
132
|
+
isFile: true
|
|
133
|
+
};
|
|
134
|
+
if ((0, kizunajs_generator.isBinarySchema)(schema)) return {
|
|
135
|
+
expression: "Foundation.Data",
|
|
136
|
+
optional: false
|
|
137
|
+
};
|
|
138
|
+
const id = (0, kizunajs_generator.readMetaId)(schema);
|
|
139
|
+
const def = (0, kizunajs_generator.readDef)(schema);
|
|
140
|
+
const discriminated = (0, kizunajs_generator.readDiscriminatedUnion)(schema);
|
|
141
|
+
if (discriminated) {
|
|
142
|
+
const enumName = id ?? (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.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: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
151
|
+
});
|
|
152
|
+
const variants = [];
|
|
153
|
+
for (const option of discriminated.options) {
|
|
154
|
+
const variantId = (0, kizunajs_generator.readMetaId)(option);
|
|
155
|
+
const literal = (0, kizunajs_generator.readDiscriminatorStringLiteral)(option, discriminated.discriminator);
|
|
156
|
+
const variantResult = mapType(option, registry, variantId ?? `${enumName}${(0, kizunajs_generator.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: (0, kizunajs_generator.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: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
188
|
+
});
|
|
189
|
+
const fields = objectFields(schema, registry, id);
|
|
190
|
+
registry.replace({
|
|
191
|
+
kind: "struct",
|
|
192
|
+
name: id,
|
|
193
|
+
fields,
|
|
194
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
expression: id,
|
|
199
|
+
optional: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const { inner, optional } = (0, kizunajs_generator.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 ? (0, kizunajs_generator.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: (0, kizunajs_generator.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 = (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.toPascalCase)(hint));
|
|
263
|
+
if (!registry.has(structName)) {
|
|
264
|
+
registry.add({
|
|
265
|
+
kind: "struct",
|
|
266
|
+
name: structName,
|
|
267
|
+
fields: [],
|
|
268
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
269
|
+
});
|
|
270
|
+
const fields = objectFields(schema, registry, structName);
|
|
271
|
+
registry.replace({
|
|
272
|
+
kind: "struct",
|
|
273
|
+
name: structName,
|
|
274
|
+
fields,
|
|
275
|
+
description: (0, kizunajs_generator.readMetaDescription)(schema)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
expression: structName,
|
|
280
|
+
optional: false
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case "enum": {
|
|
284
|
+
const enumName = id ?? (0, kizunajs_generator.sanitizeIdentifier)((0, kizunajs_generator.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: (0, kizunajs_generator.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 = (0, kizunajs_generator.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((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
|
|
393
|
+
};
|
|
394
|
+
const objectShapeKeys = (schema, camelCase = false) => {
|
|
395
|
+
return Object.entries((0, kizunajs_generator.readObjectShape)(schema) ?? {}).map(([key, value]) => ({
|
|
396
|
+
name: propertyName(key, camelCase),
|
|
397
|
+
wireName: key,
|
|
398
|
+
isFile: (0, kizunajs_generator.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 : (0, kizunajs_generator.toPascalCase)(segment)).join("") : routeKey;
|
|
405
|
+
const methodName = methodNameOverride ?? fullJoinedName;
|
|
406
|
+
const baseHint = (0, kizunajs_generator.toPascalCase)(fullJoinedName);
|
|
407
|
+
const pathParams = (0, kizunajs_generator.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 && !(0, kizunajs_generator.isVoidSchema)(route.body)) {
|
|
413
|
+
const bodyHint = (0, kizunajs_generator.readMetaId)(route.body) ?? `${baseHint}Input`;
|
|
414
|
+
const isMultipart = route.contentType === "multipart/form-data";
|
|
415
|
+
if ((0, kizunajs_generator.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 = (0, kizunajs_generator.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 ((0, kizunajs_generator.isStreamResponse)(responseValue)) {
|
|
460
|
+
const mode = (0, kizunajs_generator.streamMode)(responseValue);
|
|
461
|
+
if (mode === "events" && (0, kizunajs_generator.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}${(0, kizunajs_generator.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 = (0, kizunajs_generator.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 = (0, kizunajs_generator.resolveResponseBody)(responseValue);
|
|
490
|
+
const responseContentType = (0, kizunajs_generator.resolveResponseContentType)(responseValue);
|
|
491
|
+
const isBinary = (0, kizunajs_generator.isBinarySchema)(bodySchema);
|
|
492
|
+
const isRaw = isBinary || responseContentType !== void 0 && !(0, kizunajs_generator.isJsonMediaType)(responseContentType);
|
|
493
|
+
const typeExpression = mapType(bodySchema, registry, responseHint).expression;
|
|
494
|
+
if ((0, kizunajs_generator.isSuccessStatus)(status)) {
|
|
495
|
+
const headersSchema = (0, kizunajs_generator.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: (0, kizunajs_generator.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" : (0, kizunajs_generator.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 = (0, kizunajs_generator.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 = (0, kizunajs_generator.createGenerator)((options, _api) => {
|
|
560
|
+
const flatMethods = [];
|
|
561
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
562
|
+
return {
|
|
563
|
+
processRoute({ routeKey, route, deprecated, deprecationMessage }) {
|
|
564
|
+
if ((0, kizunajs_generator.routeStreams)(route) && (0, kizunajs_generator.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 : (0, kizunajs_generator.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}${(0, kizunajs_generator.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 = (0, kizunajs_generator.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)}.${(0, kizunajs_generator.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 = (0, kizunajs_generator.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 = (0, kizunajs_generator.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 ((0, kizunajs_generator.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 = (0, kizunajs_generator.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 = (0, kizunajs_generator.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 = (0, kizunajs_generator.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 ((0, kizunajs_generator.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
|
+
Object.defineProperty(exports, "generateSwiftClient", {
|
|
2064
|
+
enumerable: true,
|
|
2065
|
+
get: function() {
|
|
2066
|
+
return generateSwiftClient;
|
|
2067
|
+
}
|
|
2068
|
+
});
|