@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.
- package/README.md +17 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generator-D2AtmA6u.mjs → generator-BQDx47ez.mjs} +393 -96
- package/dist/{generator-v_aUFbbV.cjs → generator-D8fdPAdY.cjs} +397 -94
- package/dist/index.cjs +35 -8
- package/dist/index.d.cts +20 -30
- package/dist/index.d.mts +20 -30
- package/dist/index.mjs +34 -9
- package/package.json +6 -6
|
@@ -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: `[${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
1251
|
-
writer.line("
|
|
1252
|
-
writer.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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(_
|
|
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
|
|
1324
|
-
writer.
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
writer.line("
|
|
1330
|
-
writer.line("
|
|
1331
|
-
writer.line("
|
|
1332
|
-
writer.line("
|
|
1333
|
-
writer.line("
|
|
1334
|
-
writer.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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(
|
|
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
|
|
1469
|
-
writer.line("
|
|
1470
|
-
writer.
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|