@kizunajs/swift 2.0.0-beta.2 → 2.0.0-beta.21

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.
@@ -45,6 +45,7 @@ var TypeRegistry = class {
45
45
  warningSet = /* @__PURE__ */ new Set();
46
46
  explicitIds = /* @__PURE__ */ new Set();
47
47
  unionVariantOwners = /* @__PURE__ */ new Map();
48
+ brands = /* @__PURE__ */ new Map();
48
49
  usesAnyCodable = false;
49
50
  constructor(camelCaseProperties = false, unknownEnumCase = false) {
50
51
  this.camelCaseProperties = camelCaseProperties;
@@ -82,6 +83,15 @@ var TypeRegistry = class {
82
83
  unionVariantOwner(payloadName) {
83
84
  return this.unionVariantOwners.get(payloadName);
84
85
  }
86
+ addBrand(brand) {
87
+ const existing = this.brands.get(brand.name);
88
+ if (existing !== void 0 && existing.rawType !== brand.rawType) throw new Error(`@kizunajs/swift: brand ${stringify(brand.name)} wraps both ${existing.rawType} and ${brand.rawType}. Give each its own name.`);
89
+ if (this.types.has(brand.name)) throw new Error(`@kizunajs/swift: brand ${stringify(brand.name)} has the same name as a model. Give each its own name.`);
90
+ this.brands.set(brand.name, brand);
91
+ }
92
+ allBrands() {
93
+ return Array.from(this.brands.values());
94
+ }
85
95
  warnAnyCodable(hint, reason) {
86
96
  this.usesAnyCodable = true;
87
97
  this.warningSet.add(`${hint}: ${reason}`);
@@ -100,8 +110,7 @@ const propertyName = (key, camelCase) => {
100
110
  if (!camel) return "field";
101
111
  return /^[0-9]/.test(camel) ? `_${camel}` : camel;
102
112
  };
103
- const objectFields = (schema, registry, hint) => {
104
- const shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {};
113
+ const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
105
114
  const fields = [];
106
115
  const seen = /* @__PURE__ */ new Map();
107
116
  for (const [key, value] of Object.entries(shape)) {
@@ -116,6 +125,7 @@ const objectFields = (schema, registry, hint) => {
116
125
  wireName: key,
117
126
  type: result.expression,
118
127
  optional: result.optional,
128
+ requiredNullable: result.requiredNullable,
119
129
  description: (0, kizunajs_generator.readMetaDescription)(value),
120
130
  isFile: result.isFile,
121
131
  deprecated: deprecation !== void 0,
@@ -125,7 +135,29 @@ const objectFields = (schema, registry, hint) => {
125
135
  return fields;
126
136
  };
127
137
  const stringify = (value) => JSON.stringify(value);
138
+ const BRANDABLE_TYPES = new Set([
139
+ "String",
140
+ "Int",
141
+ "Int64",
142
+ "Double",
143
+ "Bool",
144
+ "Date"
145
+ ]);
128
146
  const mapType = (schema, registry, hint) => {
147
+ const result = mapUnbrandedType(schema, registry, hint);
148
+ const brand = (0, kizunajs_generator.readMetaBrand)(schema);
149
+ if (brand === void 0 || !BRANDABLE_TYPES.has(result.expression)) return result;
150
+ const name = (0, kizunajs_generator.sanitizeIdentifier)(brand);
151
+ registry.addBrand({
152
+ name,
153
+ rawType: result.expression
154
+ });
155
+ return {
156
+ expression: name,
157
+ optional: false
158
+ };
159
+ };
160
+ const mapUnbrandedType = (schema, registry, hint) => {
129
161
  if ((0, kizunajs_generator.isFileSchema)(schema)) return {
130
162
  expression: "MultipartFile",
131
163
  optional: false,
@@ -199,12 +231,13 @@ const mapType = (schema, registry, hint) => {
199
231
  optional: false
200
232
  };
201
233
  }
202
- const { inner, optional } = (0, kizunajs_generator.unwrapOptionalWrappers)(schema);
203
- if (optional) {
234
+ const { inner, optional, nullable } = (0, kizunajs_generator.unwrapOptionalWrappers)(schema);
235
+ if (optional || nullable) {
204
236
  const innerResult = mapType(inner, registry, hint);
205
237
  return {
206
238
  expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
207
239
  optional: true,
240
+ requiredNullable: nullable && !optional,
208
241
  isFile: innerResult.isFile
209
242
  };
210
243
  }
@@ -253,8 +286,9 @@ const mapType = (schema, registry, hint) => {
253
286
  optional: false
254
287
  };
255
288
  }
289
+ const elementType = mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "");
256
290
  return {
257
- expression: `[${mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "")}]`,
291
+ expression: `[${(0, kizunajs_generator.unwrapOptionalWrappers)(element).nullable ? `${elementType}?` : elementType}]`,
258
292
  optional: false
259
293
  };
260
294
  }
@@ -388,6 +422,7 @@ const sanitizeCaseName = (literal) => {
388
422
  const collectObjectFields = (schema, registry, hint) => {
389
423
  return objectFields(schema, registry, hint);
390
424
  };
425
+ const hasDefault = (field) => field.optional && field.requiredNullable !== true;
391
426
  const objectFieldCount = (schema) => {
392
427
  return Object.keys((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
393
428
  };
@@ -556,7 +591,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
556
591
  stream
557
592
  };
558
593
  };
559
- const swiftGenerator = (0, kizunajs_generator.createGenerator)((options, _api) => {
594
+ const swiftRoutesWalk = (options) => {
560
595
  const flatMethods = [];
561
596
  const groupMap = /* @__PURE__ */ new Map();
562
597
  return {
@@ -588,7 +623,7 @@ const swiftGenerator = (0, kizunajs_generator.createGenerator)((options, _api) =
588
623
  };
589
624
  }
590
625
  };
591
- });
626
+ };
592
627
  const SWIFT_KEYWORDS = new Set([
593
628
  "associatedtype",
594
629
  "class",
@@ -664,6 +699,10 @@ const sanitizeEnumCaseName = (value) => {
664
699
  if (!camel) return "_";
665
700
  return /^[0-9]/.test(camel) ? `_${camel}` : camel;
666
701
  };
702
+ const defaultArgument = (field) => {
703
+ if (!hasDefault(field)) return "";
704
+ return " = nil";
705
+ };
667
706
  const optionalize = (type, optional) => {
668
707
  if (!optional) return type;
669
708
  return type.endsWith("?") ? type : `${type}?`;
@@ -678,7 +717,7 @@ const emitNamedFactory = (writer, factoryName, fields, buildExpression) => {
678
717
  }
679
718
  const params = fields.map((field) => {
680
719
  const typeExpression = optionalize(field.type, field.optional);
681
- const defaultPart = field.optional ? " = nil" : "";
720
+ const defaultPart = defaultArgument(field);
682
721
  return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
683
722
  });
684
723
  const args = fields.map((field) => `${field.name}: ${escapeKeyword(field.name)}`).join(", ");
@@ -701,7 +740,7 @@ const emitMemberwiseInit = (writer, fields, assignmentTarget = (field) => escape
701
740
  }
702
741
  const params = fields.map((field) => {
703
742
  const typeExpression = optionalize(field.type, field.optional);
704
- const defaultPart = field.optional ? " = nil" : "";
743
+ const defaultPart = defaultArgument(field);
705
744
  return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
706
745
  });
707
746
  if (params.length === 1) {
@@ -835,7 +874,9 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
835
874
  ...field,
836
875
  type: resolveOwnedType(field.type)
837
876
  }));
838
- const needsCodingKeys = !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
877
+ const needsEncode = !hasFile && type.fields.some((field) => field.requiredNullable === true);
878
+ const needsCodingKeys = needsEncode || !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
879
+ const storageName = (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name);
839
880
  writer.block(`public struct ${type.name}: ${conformances}`, () => {
840
881
  emitOwnedTypes(writer, lookupName, context, ownedTypeMap, ownedTypeLookup);
841
882
  for (const field of type.fields) {
@@ -858,8 +899,43 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
858
899
  else writer.line(`case ${escapeKeyword(field.name)} = ${stringLiteral(field.wireName)}`);
859
900
  });
860
901
  }
902
+ if (needsEncode) {
903
+ writer.blank();
904
+ writer.block("public func encode(to encoder: Encoder) throws", () => {
905
+ writer.line("var container = encoder.container(keyedBy: CodingKeys.self)");
906
+ for (const field of type.fields) {
907
+ const encodeCall = field.optional && field.requiredNullable !== true ? "encodeIfPresent" : "encode";
908
+ writer.line(`try container.${encodeCall}(${storageName(field)}, forKey: .${storageName(field)})`);
909
+ }
910
+ });
911
+ }
912
+ writer.blank();
913
+ emitMemberwiseInit(writer, adjustedFields, storageName);
914
+ });
915
+ };
916
+ /**
917
+ * A brand as a struct around its value, encoded as the bare value.
918
+ */
919
+ const emitBrand = (writer, brand) => {
920
+ writer.block(`public struct ${brand.name}: RawRepresentable, Codable, Hashable, Sendable`, () => {
921
+ writer.line(`public let rawValue: ${brand.rawType}`);
922
+ writer.blank();
923
+ writer.block(`public init(rawValue: ${brand.rawType})`, () => {
924
+ writer.line("self.rawValue = rawValue");
925
+ });
926
+ writer.blank();
927
+ writer.block(`public init(_ rawValue: ${brand.rawType})`, () => {
928
+ writer.line("self.rawValue = rawValue");
929
+ });
930
+ writer.blank();
931
+ writer.block("public init(from decoder: Decoder) throws", () => {
932
+ writer.line(`rawValue = try decoder.singleValueContainer().decode(${brand.rawType}.self)`);
933
+ });
861
934
  writer.blank();
862
- emitMemberwiseInit(writer, adjustedFields, (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name));
935
+ writer.block("public func encode(to encoder: Encoder) throws", () => {
936
+ writer.line("var container = encoder.singleValueContainer()");
937
+ writer.line("try container.encode(rawValue)");
938
+ });
863
939
  });
864
940
  };
865
941
  const emitDiscriminatedEnum = (writer, type, context, registryName, ownedTypeMap, ownedTypeLookup) => {
@@ -872,7 +948,7 @@ const emitDiscriminatedEnum = (writer, type, context, registryName, ownedTypeMap
872
948
  if (!payloadStruct) continue;
873
949
  const isDiscriminator = (field) => field.wireName === type.discriminator || field.name === type.discriminator;
874
950
  const factoryParams = payloadStruct.fields.filter((field) => !isDiscriminator(field)).map((field) => {
875
- const defaultPart = field.optional ? " = nil" : "";
951
+ const defaultPart = defaultArgument(field);
876
952
  const fieldType = (ownedTypeMap && relativeOwnedPath(field.type, ownedTypeMap, lookupName)) ?? resolveType(field.type, void 0, context);
877
953
  return `${escapeKeyword(field.name)}: ${optionalize(fieldType, field.optional)}${defaultPart}`;
878
954
  }).join(", ");
@@ -955,13 +1031,33 @@ const SWIFT_PRIMITIVE_TYPES = new Set([
955
1031
  "Void",
956
1032
  "Foundation.Data"
957
1033
  ]);
1034
+ /**
1035
+ * Where a dictionary's key ends and its value begins, or -1 for an array.
1036
+ */
1037
+ const dictionarySeparator = (inner) => {
1038
+ let depth = 0;
1039
+ for (let index = 0; index < inner.length; index += 1) {
1040
+ const character = inner[index];
1041
+ if (character === "[") depth += 1;
1042
+ else if (character === "]") depth -= 1;
1043
+ else if (character === ":" && depth === 0) return index;
1044
+ }
1045
+ return -1;
1046
+ };
958
1047
  const resolveType = (typeName, currentOperation, context, scope = "actor") => {
959
1048
  const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
960
1049
  const optional = typeName.endsWith("?");
961
1050
  const base = optional ? typeName.slice(0, -1) : typeName;
962
1051
  if (SWIFT_PRIMITIVE_TYPES.has(base)) return typeName;
963
1052
  if (base.startsWith("[") && base.endsWith("]")) {
964
- const resolved = resolveType(base.slice(1, -1), currentOperation, context, scope);
1053
+ const inner = base.slice(1, -1);
1054
+ const separator = dictionarySeparator(inner);
1055
+ if (separator !== -1) {
1056
+ const key = resolveType(inner.slice(0, separator).trim(), currentOperation, context, scope);
1057
+ const value = resolveType(inner.slice(separator + 1).trim(), currentOperation, context, scope);
1058
+ return optional ? `[${key}: ${value}]?` : `[${key}: ${value}]`;
1059
+ }
1060
+ const resolved = resolveType(inner, currentOperation, context, scope);
965
1061
  return optional ? `[${resolved}]?` : `[${resolved}]`;
966
1062
  }
967
1063
  if (base === "MultipartFile" || base === "ValidationError" || base === "ValidationIssue") {
@@ -1038,6 +1134,8 @@ const emitToolTracking = (writer, method) => {
1038
1134
  writer.line("/// How far along the call is.");
1039
1135
  writer.block("public enum State: Sendable, Equatable", () => {
1040
1136
  writer.line("case running");
1137
+ writer.line("/// The call waits for the person to approve it.");
1138
+ writer.line("case needsApproval");
1041
1139
  writer.line("case done");
1042
1140
  writer.line("case failed");
1043
1141
  });
@@ -1070,6 +1168,9 @@ const emitToolTracking = (writer, method) => {
1070
1168
  writer.line("case .tool_call(let payload):");
1071
1169
  writer.line(" var tracked = at(payload.id, payload.name)");
1072
1170
  writer.line(" tracked.call = payload");
1171
+ writer.line(" if payload.needsApproval == true, tracked.state == .running {");
1172
+ writer.line(" tracked.state = .needsApproval");
1173
+ writer.line(" }");
1073
1174
  writer.line(" calls[payload.id] = tracked");
1074
1175
  }
1075
1176
  if (names.has("tool_result")) {
@@ -1231,7 +1332,7 @@ const closure = (writer, opener, body) => {
1231
1332
  writer.indent(body);
1232
1333
  writer.line("}");
1233
1334
  };
1234
- const emitKizunaStreaming = (writer) => {
1335
+ const emitKizunaStreaming = (writer, clientName) => {
1235
1336
  writer.blank();
1236
1337
  writer.block("struct ServerSentEvent: Sendable", () => {
1237
1338
  writer.line("let event: String?");
@@ -1240,21 +1341,44 @@ const emitKizunaStreaming = (writer) => {
1240
1341
  writer.line("let retry: Int?");
1241
1342
  });
1242
1343
  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)", () => {
1344
+ writer.block("struct Reconnection: Sendable", () => {
1345
+ writer.line(`let policy: ${clientName}.ReconnectPolicy`);
1346
+ writer.line("let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes?");
1347
+ });
1348
+ writer.blank();
1349
+ writer.block(`static func open<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, retry: ${clientName}.RetryPolicy?, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?, failure: Failure.Type) async throws(Failure) -> (URLSession.AsyncBytes, Int, HTTPURLResponse)`, () => {
1244
1350
  writer.line("if let requestMiddleware {");
1245
1351
  writer.line(" do { try await requestMiddleware(&request) }");
1246
1352
  writer.line(" catch is CancellationError { throw Failure.cancelled }");
1247
1353
  writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1248
1354
  writer.line(" catch { throw Failure.requestFailed(error) }");
1249
1355
  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)");
1356
+ writer.line("let policy = retryPolicy(for: request, retry)");
1357
+ writer.line("var attempt = 1");
1358
+ writer.block("while true", () => {
1359
+ writer.line("let bytes: URLSession.AsyncBytes");
1360
+ writer.line("let response: URLResponse");
1361
+ writer.line("do { (bytes, response) = try await session.bytes(for: request) }");
1362
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1363
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1364
+ writer.line("catch {");
1365
+ writer.line(" if let policy, let delay = retryDelay(policy, attempt: attempt, response: nil) {");
1366
+ writer.line(" try await pause(delay, failure: Failure.self)");
1367
+ writer.line(" attempt += 1");
1368
+ writer.line(" continue");
1369
+ writer.line(" }");
1370
+ writer.line(" throw Failure.requestFailed(error)");
1371
+ writer.line("}");
1372
+ writer.line("if let responseMiddleware { await responseMiddleware(request, Foundation.Data(), response) }");
1373
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1374
+ writer.line("if let policy, let delay = retryDelay(policy, attempt: attempt, response: httpResponse) {");
1375
+ writer.line(" bytes.task.cancel()");
1376
+ writer.line(" try await pause(delay, failure: Failure.self)");
1377
+ writer.line(" attempt += 1");
1378
+ writer.line(" continue");
1379
+ writer.line("}");
1380
+ writer.line("return (bytes, httpResponse.statusCode, httpResponse)");
1381
+ });
1258
1382
  });
1259
1383
  writer.blank();
1260
1384
  writer.block("static func collect<Failure: KizunaFailure>(_ bytes: URLSession.AsyncBytes, failure: Failure.Type) async throws(Failure) -> Foundation.Data", () => {
@@ -1314,56 +1438,97 @@ const emitKizunaStreaming = (writer) => {
1314
1438
  });
1315
1439
  });
1316
1440
  writer.blank();
1317
- writer.block("static func serverSentEvents(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
1441
+ writer.block("static func serverSentEvents(_ first: URLSession.AsyncBytes, reconnect: Reconnection?) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
1318
1442
  closure(writer, "AsyncThrowingStream { continuation in", () => {
1319
1443
  writer.block("let task = Task", () => {
1320
- writer.line("var eventType = \"\"");
1321
- writer.line("var dataLines: [String] = []");
1322
1444
  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");
1445
+ writer.line("var reconnectionTime = reconnect?.policy.reconnectionTime ?? 3");
1446
+ writer.line("var bytes: URLSession.AsyncBytes? = first");
1447
+ writer.line("var attempts = 0");
1448
+ writer.line("var failure: Swift.Error? = nil");
1449
+ writer.block("while true", () => {
1450
+ writer.block("if let current = bytes", () => {
1451
+ writer.line("var eventType = \"\"");
1452
+ writer.line("var dataLines: [String] = []");
1453
+ writer.line("var pendingId = lastEventId");
1454
+ writer.line("var retry: Int? = nil");
1455
+ writer.line("var firstLine = true");
1456
+ writer.block("do", () => {
1457
+ writer.block("for try await received in lines(current)", () => {
1458
+ writer.line("let line = firstLine && received.hasPrefix(\"\\u{FEFF}\") ? String(received.dropFirst()) : received");
1459
+ writer.line("firstLine = false");
1460
+ writer.line("if line.isEmpty {");
1461
+ writer.line(" lastEventId = pendingId");
1462
+ writer.line(" if !dataLines.isEmpty {");
1463
+ writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
1464
+ writer.line(" attempts = 0");
1465
+ writer.line(" failure = nil");
1466
+ writer.line(" dataLines = []");
1467
+ writer.line(" retry = nil");
1468
+ writer.line(" }");
1469
+ writer.line(" eventType = \"\"");
1470
+ writer.line(" continue");
1471
+ writer.line("}");
1472
+ writer.line("if line.hasPrefix(\":\") { continue }");
1473
+ writer.line("let field: Substring");
1474
+ writer.line("var value: Substring");
1475
+ writer.line("if let separator = line.firstIndex(of: \":\") {");
1476
+ writer.line(" field = line[..<separator]");
1477
+ writer.line(" value = line[line.index(after: separator)...]");
1478
+ writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
1479
+ writer.line("} else {");
1480
+ writer.line(" field = Substring(line)");
1481
+ writer.line(" value = \"\"");
1482
+ writer.line("}");
1483
+ writer.line("switch field {");
1484
+ writer.line("case \"event\": eventType = String(value)");
1485
+ writer.line("case \"data\": dataLines.append(String(value))");
1486
+ writer.line("case \"id\": if !value.contains(\"\\0\") { pendingId = String(value) }");
1487
+ writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy({ $0.isASCII && $0.isNumber }), let milliseconds = Int(value) { retry = milliseconds; reconnectionTime = min(TimeInterval(milliseconds) / 1000, 3600) }");
1488
+ writer.line("default: break");
1489
+ writer.line("}");
1490
+ });
1491
+ });
1492
+ writer.line("catch { failure = error }");
1493
+ });
1494
+ writer.line("guard let reconnect, !Task.isCancelled, attempts < reconnect.policy.maximumAttempts else {");
1495
+ writer.line(" continuation.finish(throwing: failure)");
1496
+ writer.line(" return");
1497
+ writer.line("}");
1498
+ writer.line("attempts += 1");
1499
+ writer.line("let ceiling = min(reconnect.policy.maximumDelay, reconnectionTime * pow(2, Double(attempts - 1)))");
1500
+ writer.line("let delay = max(0, reconnectionTime) + Double.random(in: 0...max(0, ceiling - reconnectionTime))");
1501
+ writer.line("do { try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) }");
1502
+ writer.line("catch {");
1503
+ writer.line(" continuation.finish(throwing: failure)");
1504
+ writer.line(" return");
1505
+ writer.line("}");
1506
+ writer.block("do", () => {
1507
+ writer.line("guard let reopened = try await reconnect.reopen(lastEventId) else {");
1508
+ writer.line(" continuation.finish()");
1509
+ writer.line(" return");
1352
1510
  writer.line("}");
1511
+ writer.line("bytes = reopened");
1512
+ });
1513
+ writer.block("catch let error as URLError", () => {
1514
+ writer.line("failure = error");
1515
+ writer.line("bytes = nil");
1516
+ });
1517
+ writer.block("catch", () => {
1518
+ writer.line("continuation.finish(throwing: error)");
1519
+ writer.line("return");
1353
1520
  });
1354
- writer.line("continuation.finish()");
1355
1521
  });
1356
- writer.line("catch { continuation.finish(throwing: error) }");
1357
1522
  });
1358
1523
  writer.line("continuation.onTermination = { _ in task.cancel() }");
1359
1524
  });
1360
1525
  });
1361
1526
  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>", () => {
1527
+ writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, reconnect: Reconnection? = nil, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
1363
1528
  closure(writer, "AsyncThrowingStream { continuation in", () => {
1364
1529
  writer.block("let task = Task", () => {
1365
1530
  writer.block("do", () => {
1366
- writer.block("for try await event in serverSentEvents(bytes)", () => {
1531
+ writer.block("for try await event in serverSentEvents(bytes, reconnect: reconnect)", () => {
1367
1532
  writer.line("if let decoded = try decode(event, decoder) { continuation.yield(decoded) }");
1368
1533
  });
1369
1534
  writer.line("continuation.finish()");
@@ -1458,22 +1623,68 @@ const emitKizunaNamespace = (writer, options) => {
1458
1623
  writer.line("catch { throw Failure.requestFailed(error) }");
1459
1624
  });
1460
1625
  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)", () => {
1626
+ writer.block(`static func send<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, retry: ${options.clientName}.RetryPolicy?, 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
1627
  writer.line("if let requestMiddleware {");
1463
1628
  writer.line(" do { try await requestMiddleware(&request) }");
1464
1629
  writer.line(" catch is CancellationError { throw Failure.cancelled }");
1465
1630
  writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1466
1631
  writer.line(" catch { throw Failure.requestFailed(error) }");
1467
1632
  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)");
1633
+ writer.line("let policy = retryPolicy(for: request, retry)");
1634
+ writer.line("var attempt = 1");
1635
+ writer.block("while true", () => {
1636
+ writer.line("let data: Foundation.Data");
1637
+ writer.line("let response: URLResponse");
1638
+ writer.line("do { (data, response) = try await session.data(for: request) }");
1639
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1640
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1641
+ writer.line("catch {");
1642
+ writer.line(" if let policy, let delay = retryDelay(policy, attempt: attempt, response: nil) {");
1643
+ writer.line(" try await pause(delay, failure: Failure.self)");
1644
+ writer.line(" attempt += 1");
1645
+ writer.line(" continue");
1646
+ writer.line(" }");
1647
+ writer.line(" throw Failure.requestFailed(error)");
1648
+ writer.line("}");
1649
+ writer.line("if let responseMiddleware { await responseMiddleware(request, data, response) }");
1650
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1651
+ writer.line("if let policy, let delay = retryDelay(policy, attempt: attempt, response: httpResponse) {");
1652
+ writer.line(" try await pause(delay, failure: Failure.self)");
1653
+ writer.line(" attempt += 1");
1654
+ writer.line(" continue");
1655
+ writer.line("}");
1656
+ writer.line("return (data, httpResponse.statusCode, httpResponse)");
1657
+ });
1658
+ });
1659
+ writer.blank();
1660
+ writer.block(`static func retryPolicy(for request: URLRequest, _ retry: ${options.clientName}.RetryPolicy?) -> ${options.clientName}.RetryPolicy?`, () => {
1661
+ writer.line("let idempotent = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"].contains(request.httpMethod ?? \"GET\")");
1662
+ writer.line("return idempotent && request.httpBodyStream == nil ? retry : nil");
1663
+ });
1664
+ writer.blank();
1665
+ writer.block(`static func retryDelay(_ policy: ${options.clientName}.RetryPolicy, attempt: Int, response: HTTPURLResponse?) -> TimeInterval?`, () => {
1666
+ writer.line("guard attempt < policy.maximumAttempts else { return nil }");
1667
+ writer.line("let backoff = Double.random(in: 0...min(policy.maximumDelay, policy.initialDelay * pow(2, Double(attempt - 1))))");
1668
+ writer.line("guard let response else { return backoff }");
1669
+ writer.line("guard [429, 503].contains(response.statusCode) else { return nil }");
1670
+ writer.line("guard let asked = retryAfter(response.value(forHTTPHeaderField: \"Retry-After\")) else { return backoff }");
1671
+ writer.line("return asked <= policy.maximumDelay ? asked : nil");
1672
+ });
1673
+ writer.blank();
1674
+ writer.block("static func retryAfter(_ header: String?) -> TimeInterval?", () => {
1675
+ writer.line("guard let value = header?.trimmingCharacters(in: .whitespaces) else { return nil }");
1676
+ writer.line("if let seconds = Int(value), seconds >= 0 { return TimeInterval(seconds) }");
1677
+ writer.line("let formatter = DateFormatter()");
1678
+ writer.line("formatter.locale = Locale(identifier: \"en_US_POSIX\")");
1679
+ writer.line("formatter.timeZone = TimeZone(identifier: \"GMT\")");
1680
+ writer.line("formatter.dateFormat = \"EEE, dd MMM yyyy HH:mm:ss zzz\"");
1681
+ writer.line("guard let date = formatter.date(from: value) else { return nil }");
1682
+ writer.line("return max(0, date.timeIntervalSinceNow)");
1683
+ });
1684
+ writer.blank();
1685
+ writer.block("static func pause<Failure: KizunaFailure>(_ seconds: TimeInterval, failure: Failure.Type) async throws(Failure)", () => {
1686
+ writer.line("do { try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) }");
1687
+ writer.line("catch { throw Failure.cancelled }");
1477
1688
  });
1478
1689
  writer.blank();
1479
1690
  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", () => {
@@ -1489,7 +1700,7 @@ const emitKizunaNamespace = (writer, options) => {
1489
1700
  writer.line("return Failure.decoding(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: \"No matching type for status \\(statusCode)\")), statusCode: statusCode, data: data)");
1490
1701
  });
1491
1702
  }
1492
- if (options.streaming) emitKizunaStreaming(writer);
1703
+ if (options.streaming) emitKizunaStreaming(writer, options.clientName);
1493
1704
  if (options.multipart) {
1494
1705
  writer.blank();
1495
1706
  writer.block("struct MultipartBuilder", () => {
@@ -1570,13 +1781,13 @@ const methodGroups = (method, context) => {
1570
1781
  varName: "query",
1571
1782
  type: groupTypeRef(operationName, "Query", context),
1572
1783
  factory: "query",
1573
- required: method.query.some((field) => !field.optional)
1784
+ required: method.query.some((field) => !hasDefault(field))
1574
1785
  });
1575
1786
  if (method.headers.length > 0) groups.push({
1576
1787
  varName: "headers",
1577
1788
  type: groupTypeRef(operationName, "Headers", context),
1578
1789
  factory: "headers",
1579
- required: method.headers.some((field) => !field.optional)
1790
+ required: method.headers.some((field) => !hasDefault(field))
1580
1791
  });
1581
1792
  return [...groups.filter((group) => group.required), ...groups.filter((group) => !group.required)];
1582
1793
  };
@@ -1632,7 +1843,7 @@ const emitMethodBody = (writer, method, context, receiver = "") => {
1632
1843
  emitStreamMethodTail(writer, method, context, receiver, responseBinding);
1633
1844
  return;
1634
1845
  }
1635
- writer.line(`let (data, statusCode, ${responseBinding}) = try await Kizuna.send(&request, session: ${receiver}session, requestMiddleware: ${receiver}requestMiddleware, responseMiddleware: ${receiver}responseMiddleware, failure: ${failure}.self)`);
1846
+ writer.line(`let (data, statusCode, ${responseBinding}) = try await Kizuna.send(&request, session: ${receiver}session, retry: ${receiver}retry, requestMiddleware: ${receiver}requestMiddleware, responseMiddleware: ${receiver}responseMiddleware, failure: ${failure}.self)`);
1636
1847
  writer.line("switch statusCode {");
1637
1848
  for (const successResponse of method.successResponses) {
1638
1849
  writer.line(`case ${successResponse.status}:`);
@@ -1700,6 +1911,35 @@ const emitErrorCases = (writer, method, context, receiver, prelude) => {
1700
1911
  }
1701
1912
  }
1702
1913
  };
1914
+ const IDEMPOTENT_METHODS = new Set([
1915
+ "GET",
1916
+ "HEAD",
1917
+ "OPTIONS",
1918
+ "PUT",
1919
+ "DELETE"
1920
+ ]);
1921
+ /**
1922
+ * Writes the closure that reopens an event stream and returns the argument handing it to `Kizuna.events`, or nothing
1923
+ * for a method a client may not resend.
1924
+ */
1925
+ const emitReopen = (writer, method, receiver, failure, status) => {
1926
+ if (!IDEMPOTENT_METHODS.has(method.method)) return "";
1927
+ closure(writer, `let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes? = { [request, session = ${receiver}session, retry = ${receiver}retry, requestMiddleware = ${receiver}requestMiddleware, responseMiddleware = ${receiver}responseMiddleware] lastEventId in`, () => {
1928
+ writer.line("var again = request");
1929
+ writer.line("if !lastEventId.isEmpty { again.setValue(lastEventId, forHTTPHeaderField: \"Last-Event-ID\") }");
1930
+ writer.line("let reopened: (URLSession.AsyncBytes, Int, HTTPURLResponse)");
1931
+ writer.line(`do { reopened = try await Kizuna.open(&again, session: session, retry: retry, requestMiddleware: requestMiddleware, responseMiddleware: responseMiddleware, failure: ${failure}.self) }`);
1932
+ writer.line(`catch let failure as ${failure} {`);
1933
+ writer.line(" if case .requestFailed(let underlying) = failure { throw underlying }");
1934
+ writer.line(" throw failure");
1935
+ writer.line("}");
1936
+ writer.line("if reopened.1 == 204 { return nil }");
1937
+ writer.line(`guard reopened.1 == ${status} else { throw ${failure}.unexpectedStatus(reopened.1, Foundation.Data()) }`);
1938
+ writer.line(`guard reopened.2.value(forHTTPHeaderField: "Content-Type")?.lowercased().hasPrefix("text/event-stream") == true else { throw ${failure}.invalidResponse }`);
1939
+ writer.line("return reopened.0");
1940
+ });
1941
+ return `, reconnect: ${receiver}reconnect.map { Kizuna.Reconnection(policy: $0, reopen: reopen) }`;
1942
+ };
1703
1943
  const emitStreamMethodTail = (writer, method, context, receiver, responseBinding) => {
1704
1944
  const failure = failureRef(method, context);
1705
1945
  const stream = method.stream;
@@ -1707,7 +1947,7 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
1707
1947
  const qualifiedResult = `${context.clientName}.${method.operationName}.${method.resultWrapperName}`;
1708
1948
  const hasHeaders = method.resultHeaderFields.length > 0;
1709
1949
  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)`);
1950
+ writer.line(`let (bytes, statusCode, ${responseBinding}) = try await Kizuna.open(&request, session: ${receiver}session, retry: ${receiver}retry, requestMiddleware: ${receiver}requestMiddleware, responseMiddleware: ${receiver}responseMiddleware, failure: ${failure}.self)`);
1711
1951
  writer.line("switch statusCode {");
1712
1952
  writer.line(`case ${stream.status}:`);
1713
1953
  writer.indent(() => {
@@ -1715,7 +1955,8 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
1715
1955
  else if (stream.mode === "binary") writer.line("let body = Kizuna.chunks(bytes)");
1716
1956
  else if (stream.events) {
1717
1957
  const events = stream.events;
1718
- closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${streamElementType(method, context, "actor")}? in`, () => {
1958
+ const elementType = streamElementType(method, context, "actor");
1959
+ closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
1719
1960
  writer.line("switch event.event {");
1720
1961
  for (const event of events) {
1721
1962
  const payloadType = resolveType(event.type, method.operationName, context);
@@ -1726,7 +1967,7 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
1726
1967
  });
1727
1968
  } else {
1728
1969
  const elementType = streamElementType(method, context, "actor");
1729
- closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${elementType}? in`, () => {
1970
+ closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
1730
1971
  writer.line(`try decoder.decode(${elementType}.self, from: Foundation.Data(event.data.utf8))`);
1731
1972
  });
1732
1973
  }
@@ -1792,9 +2033,15 @@ const emitBodyEncoding = (writer, method, context, receiver = "") => {
1792
2033
  writer.line("var multipart = Kizuna.MultipartBuilder()");
1793
2034
  for (const field of body.multipartFields) {
1794
2035
  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}))`);
2036
+ const isFile = field.isFile || flattenedField?.isFile === true;
2037
+ const append = (accessor) => {
2038
+ if (isFile) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
2039
+ else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
2040
+ };
2041
+ if (flattenedField?.optional === true) writer.block(`if let value = body.${escapeKeyword(field.name)}`, () => {
2042
+ append("value");
2043
+ });
2044
+ else append(`body.${escapeKeyword(field.name)}`);
1798
2045
  }
1799
2046
  writer.line("request.httpBody = multipart.finalize()");
1800
2047
  writer.line("request.setValue(multipart.contentType, forHTTPHeaderField: \"Content-Type\")");
@@ -1813,7 +2060,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1813
2060
  const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
1814
2061
  const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
1815
2062
  const contextFields = context.requestContextFields;
1816
- const contextRequired = contextFields.some((field) => !field.optional);
2063
+ const contextRequired = contextFields.some((field) => !hasDefault(field));
1817
2064
  writer.blank();
1818
2065
  writer.block(`public final class ${clientName}: Sendable`, () => {
1819
2066
  if (contextFields.length > 0) {
@@ -1822,7 +2069,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1822
2069
  writer.block("public struct RequestContext: Sendable, Equatable", () => {
1823
2070
  for (const field of contextFields) writer.line(`public var ${escapeKeyword(field.name)}: ${field.type}`);
1824
2071
  writer.blank();
1825
- const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${field.optional ? " = nil" : ""}`).join(", ");
2072
+ const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`).join(", ");
1826
2073
  writer.block(`public init(${initParams})`, () => {
1827
2074
  for (const field of contextFields) writer.line(`self.${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`);
1828
2075
  });
@@ -1835,6 +2082,38 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1835
2082
  });
1836
2083
  });
1837
2084
  }
2085
+ writer.blank();
2086
+ writer.docComment("Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or was answered 429 or 503, waiting what Retry-After asks or backing off.");
2087
+ writer.block("public struct RetryPolicy: Sendable", () => {
2088
+ writer.docComment("Attempts in all, the first included.");
2089
+ writer.line("public var maximumAttempts: Int");
2090
+ writer.docComment("Seconds the first backoff waits at most.");
2091
+ writer.line("public var initialDelay: TimeInterval");
2092
+ writer.docComment("Seconds any wait may take.");
2093
+ writer.line("public var maximumDelay: TimeInterval");
2094
+ writer.blank();
2095
+ writer.block("public init(maximumAttempts: Int = 3, initialDelay: TimeInterval = 0.5, maximumDelay: TimeInterval = 30)", () => {
2096
+ writer.line("self.maximumAttempts = maximumAttempts");
2097
+ writer.line("self.initialDelay = initialDelay");
2098
+ writer.line("self.maximumDelay = maximumDelay");
2099
+ });
2100
+ });
2101
+ writer.blank();
2102
+ writer.docComment("Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops, sending the id of the last event as Last-Event-ID.");
2103
+ writer.block("public struct ReconnectPolicy: Sendable", () => {
2104
+ writer.docComment("Reconnections in a row before the stream fails. An event received resets the count.");
2105
+ writer.line("public var maximumAttempts: Int");
2106
+ writer.docComment("Seconds to wait before reconnecting, until the server sends a retry field.");
2107
+ writer.line("public var reconnectionTime: TimeInterval");
2108
+ writer.docComment("Seconds the wait may grow to after reconnections in a row that fail.");
2109
+ writer.line("public var maximumDelay: TimeInterval");
2110
+ writer.blank();
2111
+ writer.block("public init(maximumAttempts: Int = 5, reconnectionTime: TimeInterval = 3, maximumDelay: TimeInterval = 30)", () => {
2112
+ writer.line("self.maximumAttempts = maximumAttempts");
2113
+ writer.line("self.reconnectionTime = reconnectionTime");
2114
+ writer.line("self.maximumDelay = maximumDelay");
2115
+ });
2116
+ });
1838
2117
  if (usesMultipart) {
1839
2118
  writer.blank();
1840
2119
  writer.block("public struct MultipartFile: Sendable, Equatable", () => {
@@ -1932,14 +2211,18 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1932
2211
  if (contextFields.length > 0) writer.line("let requestContextHeaders: [String: String]");
1933
2212
  writer.line("public let requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?");
1934
2213
  writer.line("public let responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?");
2214
+ writer.line("public let retry: RetryPolicy?");
2215
+ writer.line("public let reconnect: ReconnectPolicy?");
1935
2216
  writer.line("let encoder: JSONEncoder");
1936
2217
  writer.line("let decoder: JSONDecoder");
1937
2218
  writer.blank();
1938
2219
  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)`, () => {
2220
+ writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil, reconnect: ReconnectPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
1940
2221
  writer.line("self.baseURL = baseURL");
1941
2222
  writer.line("self.session = session");
1942
2223
  writer.line("self.timeout = timeout");
2224
+ writer.line("self.retry = retry");
2225
+ writer.line("self.reconnect = reconnect");
1943
2226
  if (contextFields.length > 0) writer.line("self.requestContextHeaders = requestContext.headerFields");
1944
2227
  writer.line("self.requestMiddleware = requestMiddleware");
1945
2228
  writer.line("self.responseMiddleware = responseMiddleware");
@@ -1971,18 +2254,8 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1971
2254
  }
1972
2255
  });
1973
2256
  };
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
- });
2257
+ const renderSwiftClient = (api, partition, registry, options) => {
2258
+ const { namespaceName } = options;
1986
2259
  const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
1987
2260
  const operationTypeMap = buildOperationTypeMap(allMethods, registry);
1988
2261
  const clientName = `${namespaceName}Client`;
@@ -2043,6 +2316,10 @@ const generateSwiftClient = (api, options) => {
2043
2316
  };
2044
2317
  writer.block(`public enum ${namespaceName}`, () => {
2045
2318
  emitTypes(writer, topLevelSharedTypes, context, ownedTypeMap, ownedTypeLookup);
2319
+ for (const brand of registry.allBrands()) {
2320
+ writer.blank();
2321
+ emitBrand(writer, brand);
2322
+ }
2046
2323
  });
2047
2324
  emitClient(writer, {
2048
2325
  clientName,
@@ -2059,6 +2336,26 @@ const generateSwiftClient = (api, options) => {
2059
2336
  for (const warning of registry.warnings()) process.stderr.write(`[kizuna/swift] AnyCodable fallback at ${warning}\n`);
2060
2337
  return writer.toString();
2061
2338
  };
2339
+ /**
2340
+ * The Swift client's walk: the routes into a partition, then the file.
2341
+ */
2342
+ const swiftClientWalk = (api, options) => {
2343
+ const registry = new TypeRegistry(options.camelCaseProperties ?? false, options.unknownEnumCase ?? false);
2344
+ const routes = swiftRoutesWalk({
2345
+ namespaceName: options.namespaceName,
2346
+ registry
2347
+ });
2348
+ return {
2349
+ processRoute: routes.processRoute,
2350
+ finalize: () => renderSwiftClient(api, routes.finalize(), registry, options)
2351
+ };
2352
+ };
2353
+ /**
2354
+ * Generate a Swift API client from a kizuna config.
2355
+ *
2356
+ * - `namespaceName`: the actor class. Defaults to `APIClient`.
2357
+ */
2358
+ const generateSwiftClient = (api, options) => (0, kizunajs_generator.walkApi)(api, swiftClientWalk(api, options), { skipHidden: true });
2062
2359
  //#endregion
2063
2360
  Object.defineProperty(exports, "generateSwiftClient", {
2064
2361
  enumerable: true,
@@ -2066,3 +2363,9 @@ Object.defineProperty(exports, "generateSwiftClient", {
2066
2363
  return generateSwiftClient;
2067
2364
  }
2068
2365
  });
2366
+ Object.defineProperty(exports, "swiftClientWalk", {
2367
+ enumerable: true,
2368
+ get: function() {
2369
+ return swiftClientWalk;
2370
+ }
2371
+ });