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

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/README.md CHANGED
@@ -10,8 +10,24 @@ pnpm add -D @kizunajs/swift
10
10
 
11
11
  ## Usage
12
12
 
13
+ Name the client in your config, and `kizuna generate` writes it with the rest:
14
+
15
+ ```ts
16
+ import { swiftClient } from '@kizunajs/swift';
17
+
18
+ export default defineConfig({
19
+ routes,
20
+ clients: [
21
+ swiftClient({
22
+ output: '../ios/MyApp/Generated/APIClient.swift',
23
+ namespace: 'API',
24
+ }),
25
+ ],
26
+ });
27
+ ```
28
+
13
29
  ```sh
14
- kizuna-swift generate --config kizuna.config.ts --output ios/MyApp/Generated/APIClient.swift --namespace-name API
30
+ kizuna generate
15
31
  ```
16
32
 
17
33
  ## Documentation
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_generator = require("./generator-v_aUFbbV.cjs");
2
+ const require_generator = require("./generator-Bk6JRrTJ.cjs");
3
3
  let node_fs = require("node:fs");
4
4
  let node_path = require("node:path");
5
5
  let node_util = require("node:util");
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as generateSwiftClient } from "./generator-D2AtmA6u.mjs";
2
+ import { t as generateSwiftClient } from "./generator-2GeebFHv.mjs";
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { parseArgs } from "node:util";
@@ -1,4 +1,4 @@
1
- import { createGenerator, isBinarySchema, isDiscriminatedUnionSchema, isFileSchema, isHintPrefix, isIntegerSchema, isJsonMediaType, isNamedStream, isObjectSchema, isStreamResponse, isSuccessStatus, isVoidSchema, localTypeName, mergeHeaderFields, parsePath, readDef, readDefType, readDeprecation, readDiscriminatedUnion, readDiscriminatorStringLiteral, readMetaDescription, readMetaId, readObjectShape, resolveResponseBody, resolveResponseContentType, resolveResponseHeaders, routeStreams, sanitizeFieldName, sanitizeIdentifier, shortTypeName, soleStreamResponse, statusToCamelCase, streamMode, toCamelCase, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
1
+ import { isBinarySchema, isDiscriminatedUnionSchema, isFileSchema, isHintPrefix, isIntegerSchema, isJsonMediaType, isNamedStream, isObjectSchema, isStreamResponse, isSuccessStatus, isVoidSchema, localTypeName, mergeHeaderFields, parsePath, readDef, readDefType, readDeprecation, readDiscriminatedUnion, readDiscriminatorStringLiteral, readMetaBrand, readMetaDescription, readMetaId, readObjectShape, resolveResponseBody, resolveResponseContentType, resolveResponseHeaders, routeStreams, sanitizeFieldName, sanitizeIdentifier, shortTypeName, soleStreamResponse, statusToCamelCase, streamMode, toCamelCase, toPascalCase, unwrapOptionalWrappers, walkApi } from "kizunajs/generator";
2
2
  //#region src/emit.ts
3
3
  var SwiftWriter = class {
4
4
  lines = [];
@@ -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 = readObjectShape(schema) ?? {};
113
+ const objectFields = (schema, registry, hint, shape = 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: 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 = readMetaBrand(schema);
149
+ if (brand === void 0 || !BRANDABLE_TYPES.has(result.expression)) return result;
150
+ const name = 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 (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 } = unwrapOptionalWrappers(schema);
203
- if (optional) {
234
+ const { inner, optional, nullable } = 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: `[${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(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 = 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 = 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,39 @@ 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(`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
1345
  writer.line("if let requestMiddleware {");
1245
1346
  writer.line(" do { try await requestMiddleware(&request) }");
1246
1347
  writer.line(" catch is CancellationError { throw Failure.cancelled }");
1247
1348
  writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1248
1349
  writer.line(" catch { throw Failure.requestFailed(error) }");
1249
1350
  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)");
1351
+ writer.line("let policy = retryPolicy(for: request, retry)");
1352
+ writer.line("var attempt = 1");
1353
+ writer.block("while true", () => {
1354
+ writer.line("let bytes: URLSession.AsyncBytes");
1355
+ writer.line("let response: URLResponse");
1356
+ writer.line("do { (bytes, response) = try await session.bytes(for: request) }");
1357
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1358
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1359
+ writer.line("catch {");
1360
+ writer.line(" if let policy, let delay = retryDelay(policy, attempt: attempt, response: nil) {");
1361
+ writer.line(" try await pause(delay, failure: Failure.self)");
1362
+ writer.line(" attempt += 1");
1363
+ writer.line(" continue");
1364
+ writer.line(" }");
1365
+ writer.line(" throw Failure.requestFailed(error)");
1366
+ writer.line("}");
1367
+ writer.line("if let responseMiddleware { await responseMiddleware(request, Foundation.Data(), response) }");
1368
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1369
+ writer.line("if let policy, let delay = retryDelay(policy, attempt: attempt, response: httpResponse) {");
1370
+ writer.line(" bytes.task.cancel()");
1371
+ writer.line(" try await pause(delay, failure: Failure.self)");
1372
+ writer.line(" attempt += 1");
1373
+ writer.line(" continue");
1374
+ writer.line("}");
1375
+ writer.line("return (bytes, httpResponse.statusCode, httpResponse)");
1376
+ });
1258
1377
  });
1259
1378
  writer.blank();
1260
1379
  writer.block("static func collect<Failure: KizunaFailure>(_ bytes: URLSession.AsyncBytes, failure: Failure.Type) async throws(Failure) -> Foundation.Data", () => {
@@ -1458,22 +1577,68 @@ const emitKizunaNamespace = (writer, options) => {
1458
1577
  writer.line("catch { throw Failure.requestFailed(error) }");
1459
1578
  });
1460
1579
  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)", () => {
1580
+ 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
1581
  writer.line("if let requestMiddleware {");
1463
1582
  writer.line(" do { try await requestMiddleware(&request) }");
1464
1583
  writer.line(" catch is CancellationError { throw Failure.cancelled }");
1465
1584
  writer.line(" catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1466
1585
  writer.line(" catch { throw Failure.requestFailed(error) }");
1467
1586
  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)");
1587
+ writer.line("let policy = retryPolicy(for: request, retry)");
1588
+ writer.line("var attempt = 1");
1589
+ writer.block("while true", () => {
1590
+ writer.line("let data: Foundation.Data");
1591
+ writer.line("let response: URLResponse");
1592
+ writer.line("do { (data, response) = try await session.data(for: request) }");
1593
+ writer.line("catch is CancellationError { throw Failure.cancelled }");
1594
+ writer.line("catch let error as URLError where error.code == .cancelled { throw Failure.cancelled }");
1595
+ writer.line("catch {");
1596
+ writer.line(" if let policy, let delay = retryDelay(policy, attempt: attempt, response: nil) {");
1597
+ writer.line(" try await pause(delay, failure: Failure.self)");
1598
+ writer.line(" attempt += 1");
1599
+ writer.line(" continue");
1600
+ writer.line(" }");
1601
+ writer.line(" throw Failure.requestFailed(error)");
1602
+ writer.line("}");
1603
+ writer.line("if let responseMiddleware { await responseMiddleware(request, data, response) }");
1604
+ writer.line("guard let httpResponse = response as? HTTPURLResponse else { throw Failure.invalidResponse }");
1605
+ writer.line("if let policy, let delay = retryDelay(policy, attempt: attempt, response: httpResponse) {");
1606
+ writer.line(" try await pause(delay, failure: Failure.self)");
1607
+ writer.line(" attempt += 1");
1608
+ writer.line(" continue");
1609
+ writer.line("}");
1610
+ writer.line("return (data, httpResponse.statusCode, httpResponse)");
1611
+ });
1612
+ });
1613
+ writer.blank();
1614
+ writer.block(`static func retryPolicy(for request: URLRequest, _ retry: ${options.clientName}.RetryPolicy?) -> ${options.clientName}.RetryPolicy?`, () => {
1615
+ writer.line("let idempotent = [\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"].contains(request.httpMethod ?? \"GET\")");
1616
+ writer.line("return idempotent && request.httpBodyStream == nil ? retry : nil");
1617
+ });
1618
+ writer.blank();
1619
+ writer.block(`static func retryDelay(_ policy: ${options.clientName}.RetryPolicy, attempt: Int, response: HTTPURLResponse?) -> TimeInterval?`, () => {
1620
+ writer.line("guard attempt < policy.maximumAttempts else { return nil }");
1621
+ writer.line("let backoff = Double.random(in: 0...min(policy.maximumDelay, policy.initialDelay * pow(2, Double(attempt - 1))))");
1622
+ writer.line("guard let response else { return backoff }");
1623
+ writer.line("guard [429, 503].contains(response.statusCode) else { return nil }");
1624
+ writer.line("guard let asked = retryAfter(response.value(forHTTPHeaderField: \"Retry-After\")) else { return backoff }");
1625
+ writer.line("return asked <= policy.maximumDelay ? asked : nil");
1626
+ });
1627
+ writer.blank();
1628
+ writer.block("static func retryAfter(_ header: String?) -> TimeInterval?", () => {
1629
+ writer.line("guard let value = header?.trimmingCharacters(in: .whitespaces) else { return nil }");
1630
+ writer.line("if let seconds = Int(value), seconds >= 0 { return TimeInterval(seconds) }");
1631
+ writer.line("let formatter = DateFormatter()");
1632
+ writer.line("formatter.locale = Locale(identifier: \"en_US_POSIX\")");
1633
+ writer.line("formatter.timeZone = TimeZone(identifier: \"GMT\")");
1634
+ writer.line("formatter.dateFormat = \"EEE, dd MMM yyyy HH:mm:ss zzz\"");
1635
+ writer.line("guard let date = formatter.date(from: value) else { return nil }");
1636
+ writer.line("return max(0, date.timeIntervalSinceNow)");
1637
+ });
1638
+ writer.blank();
1639
+ writer.block("static func pause<Failure: KizunaFailure>(_ seconds: TimeInterval, failure: Failure.Type) async throws(Failure)", () => {
1640
+ writer.line("do { try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) }");
1641
+ writer.line("catch { throw Failure.cancelled }");
1477
1642
  });
1478
1643
  writer.blank();
1479
1644
  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 +1654,7 @@ const emitKizunaNamespace = (writer, options) => {
1489
1654
  writer.line("return Failure.decoding(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: \"No matching type for status \\(statusCode)\")), statusCode: statusCode, data: data)");
1490
1655
  });
1491
1656
  }
1492
- if (options.streaming) emitKizunaStreaming(writer);
1657
+ if (options.streaming) emitKizunaStreaming(writer, options.clientName);
1493
1658
  if (options.multipart) {
1494
1659
  writer.blank();
1495
1660
  writer.block("struct MultipartBuilder", () => {
@@ -1570,13 +1735,13 @@ const methodGroups = (method, context) => {
1570
1735
  varName: "query",
1571
1736
  type: groupTypeRef(operationName, "Query", context),
1572
1737
  factory: "query",
1573
- required: method.query.some((field) => !field.optional)
1738
+ required: method.query.some((field) => !hasDefault(field))
1574
1739
  });
1575
1740
  if (method.headers.length > 0) groups.push({
1576
1741
  varName: "headers",
1577
1742
  type: groupTypeRef(operationName, "Headers", context),
1578
1743
  factory: "headers",
1579
- required: method.headers.some((field) => !field.optional)
1744
+ required: method.headers.some((field) => !hasDefault(field))
1580
1745
  });
1581
1746
  return [...groups.filter((group) => group.required), ...groups.filter((group) => !group.required)];
1582
1747
  };
@@ -1632,7 +1797,7 @@ const emitMethodBody = (writer, method, context, receiver = "") => {
1632
1797
  emitStreamMethodTail(writer, method, context, receiver, responseBinding);
1633
1798
  return;
1634
1799
  }
1635
- writer.line(`let (data, statusCode, ${responseBinding}) = try await Kizuna.send(&request, session: ${receiver}session, requestMiddleware: ${receiver}requestMiddleware, responseMiddleware: ${receiver}responseMiddleware, failure: ${failure}.self)`);
1800
+ 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
1801
  writer.line("switch statusCode {");
1637
1802
  for (const successResponse of method.successResponses) {
1638
1803
  writer.line(`case ${successResponse.status}:`);
@@ -1707,7 +1872,7 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
1707
1872
  const qualifiedResult = `${context.clientName}.${method.operationName}.${method.resultWrapperName}`;
1708
1873
  const hasHeaders = method.resultHeaderFields.length > 0;
1709
1874
  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)`);
1875
+ 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
1876
  writer.line("switch statusCode {");
1712
1877
  writer.line(`case ${stream.status}:`);
1713
1878
  writer.indent(() => {
@@ -1792,9 +1957,15 @@ const emitBodyEncoding = (writer, method, context, receiver = "") => {
1792
1957
  writer.line("var multipart = Kizuna.MultipartBuilder()");
1793
1958
  for (const field of body.multipartFields) {
1794
1959
  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}))`);
1960
+ const isFile = field.isFile || flattenedField?.isFile === true;
1961
+ const append = (accessor) => {
1962
+ if (isFile) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
1963
+ else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
1964
+ };
1965
+ if (flattenedField?.optional === true) writer.block(`if let value = body.${escapeKeyword(field.name)}`, () => {
1966
+ append("value");
1967
+ });
1968
+ else append(`body.${escapeKeyword(field.name)}`);
1798
1969
  }
1799
1970
  writer.line("request.httpBody = multipart.finalize()");
1800
1971
  writer.line("request.setValue(multipart.contentType, forHTTPHeaderField: \"Content-Type\")");
@@ -1813,7 +1984,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1813
1984
  const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
1814
1985
  const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
1815
1986
  const contextFields = context.requestContextFields;
1816
- const contextRequired = contextFields.some((field) => !field.optional);
1987
+ const contextRequired = contextFields.some((field) => !hasDefault(field));
1817
1988
  writer.blank();
1818
1989
  writer.block(`public final class ${clientName}: Sendable`, () => {
1819
1990
  if (contextFields.length > 0) {
@@ -1822,7 +1993,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1822
1993
  writer.block("public struct RequestContext: Sendable, Equatable", () => {
1823
1994
  for (const field of contextFields) writer.line(`public var ${escapeKeyword(field.name)}: ${field.type}`);
1824
1995
  writer.blank();
1825
- const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${field.optional ? " = nil" : ""}`).join(", ");
1996
+ const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`).join(", ");
1826
1997
  writer.block(`public init(${initParams})`, () => {
1827
1998
  for (const field of contextFields) writer.line(`self.${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`);
1828
1999
  });
@@ -1835,6 +2006,22 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1835
2006
  });
1836
2007
  });
1837
2008
  }
2009
+ writer.blank();
2010
+ 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.");
2011
+ writer.block("public struct RetryPolicy: Sendable", () => {
2012
+ writer.docComment("Attempts in all, the first included.");
2013
+ writer.line("public var maximumAttempts: Int");
2014
+ writer.docComment("Seconds the first backoff waits at most.");
2015
+ writer.line("public var initialDelay: TimeInterval");
2016
+ writer.docComment("Seconds any wait may take.");
2017
+ writer.line("public var maximumDelay: TimeInterval");
2018
+ writer.blank();
2019
+ writer.block("public init(maximumAttempts: Int = 3, initialDelay: TimeInterval = 0.5, maximumDelay: TimeInterval = 30)", () => {
2020
+ writer.line("self.maximumAttempts = maximumAttempts");
2021
+ writer.line("self.initialDelay = initialDelay");
2022
+ writer.line("self.maximumDelay = maximumDelay");
2023
+ });
2024
+ });
1838
2025
  if (usesMultipart) {
1839
2026
  writer.blank();
1840
2027
  writer.block("public struct MultipartFile: Sendable, Equatable", () => {
@@ -1932,14 +2119,16 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1932
2119
  if (contextFields.length > 0) writer.line("let requestContextHeaders: [String: String]");
1933
2120
  writer.line("public let requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?");
1934
2121
  writer.line("public let responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?");
2122
+ writer.line("public let retry: RetryPolicy?");
1935
2123
  writer.line("let encoder: JSONEncoder");
1936
2124
  writer.line("let decoder: JSONDecoder");
1937
2125
  writer.blank();
1938
2126
  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)`, () => {
2127
+ writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
1940
2128
  writer.line("self.baseURL = baseURL");
1941
2129
  writer.line("self.session = session");
1942
2130
  writer.line("self.timeout = timeout");
2131
+ writer.line("self.retry = retry");
1943
2132
  if (contextFields.length > 0) writer.line("self.requestContextHeaders = requestContext.headerFields");
1944
2133
  writer.line("self.requestMiddleware = requestMiddleware");
1945
2134
  writer.line("self.responseMiddleware = responseMiddleware");
@@ -1971,18 +2160,8 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
1971
2160
  }
1972
2161
  });
1973
2162
  };
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
- });
2163
+ const renderSwiftClient = (api, partition, registry, options) => {
2164
+ const { namespaceName } = options;
1986
2165
  const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
1987
2166
  const operationTypeMap = buildOperationTypeMap(allMethods, registry);
1988
2167
  const clientName = `${namespaceName}Client`;
@@ -2043,6 +2222,10 @@ const generateSwiftClient = (api, options) => {
2043
2222
  };
2044
2223
  writer.block(`public enum ${namespaceName}`, () => {
2045
2224
  emitTypes(writer, topLevelSharedTypes, context, ownedTypeMap, ownedTypeLookup);
2225
+ for (const brand of registry.allBrands()) {
2226
+ writer.blank();
2227
+ emitBrand(writer, brand);
2228
+ }
2046
2229
  });
2047
2230
  emitClient(writer, {
2048
2231
  clientName,
@@ -2059,5 +2242,25 @@ const generateSwiftClient = (api, options) => {
2059
2242
  for (const warning of registry.warnings()) process.stderr.write(`[kizuna/swift] AnyCodable fallback at ${warning}\n`);
2060
2243
  return writer.toString();
2061
2244
  };
2245
+ /**
2246
+ * The Swift client's walk: the routes into a partition, then the file.
2247
+ */
2248
+ const swiftClientWalk = (api, options) => {
2249
+ const registry = new TypeRegistry(options.camelCaseProperties ?? false, options.unknownEnumCase ?? false);
2250
+ const routes = swiftRoutesWalk({
2251
+ namespaceName: options.namespaceName,
2252
+ registry
2253
+ });
2254
+ return {
2255
+ processRoute: routes.processRoute,
2256
+ finalize: () => renderSwiftClient(api, routes.finalize(), registry, options)
2257
+ };
2258
+ };
2259
+ /**
2260
+ * Generate a Swift API client from a kizuna config.
2261
+ *
2262
+ * - `namespaceName`: the actor class. Defaults to `APIClient`.
2263
+ */
2264
+ const generateSwiftClient = (api, options) => walkApi(api, swiftClientWalk(api, options), { skipHidden: true });
2062
2265
  //#endregion
2063
- export { generateSwiftClient as t };
2266
+ export { swiftClientWalk as n, generateSwiftClient as t };