@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 +17 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generator-D2AtmA6u.mjs → generator-2GeebFHv.mjs} +261 -58
- package/dist/{generator-v_aUFbbV.cjs → generator-Bk6JRrTJ.cjs} +265 -56
- 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
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
|
|
30
|
+
kizuna generate
|
|
15
31
|
```
|
|
16
32
|
|
|
17
33
|
## Documentation
|
package/dist/cli.cjs
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as generateSwiftClient } from "./generator-
|
|
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 {
|
|
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: `[${
|
|
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
|
|
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
|
|
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,39 @@ const emitKizunaStreaming = (writer) => {
|
|
|
1240
1341
|
writer.line("let retry: Int?");
|
|
1241
1342
|
});
|
|
1242
1343
|
writer.blank();
|
|
1243
|
-
writer.block(
|
|
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
|
|
1251
|
-
writer.line("
|
|
1252
|
-
writer.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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(
|
|
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
|
|
1469
|
-
writer.line("
|
|
1470
|
-
writer.
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
|
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
|
|
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
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|