@kizunajs/kotlin 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 +18 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generator-s2pn7vqP.cjs → generator-Bg4HlRb3.cjs} +194 -45
- package/dist/{generator-BvNpSarY.mjs → generator-D4GvH1-a.mjs} +190 -47
- package/dist/index.cjs +39 -8
- package/dist/index.d.cts +22 -34
- package/dist/index.d.mts +22 -34
- package/dist/index.mjs +38 -9
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -10,8 +10,25 @@ pnpm add -D @kizunajs/kotlin
|
|
|
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 { kotlinClient } from '@kizunajs/kotlin';
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
routes,
|
|
20
|
+
clients: [
|
|
21
|
+
kotlinClient({
|
|
22
|
+
output: '../android/app/src/main/kotlin/com/example/APIClient.kt',
|
|
23
|
+
namespace: 'API',
|
|
24
|
+
package: 'com.example',
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
13
30
|
```sh
|
|
14
|
-
kizuna
|
|
31
|
+
kizuna generate
|
|
15
32
|
```
|
|
16
33
|
|
|
17
34
|
## 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 generateKotlinClient } from "./generator-
|
|
2
|
+
import { t as generateKotlinClient } from "./generator-D4GvH1-a.mjs";
|
|
3
3
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -56,6 +56,7 @@ var TypeRegistry = class {
|
|
|
56
56
|
explicitIds = /* @__PURE__ */ new Set();
|
|
57
57
|
sealedVariantPayloads = /* @__PURE__ */ new Set();
|
|
58
58
|
sealedVariantPaths = /* @__PURE__ */ new Map();
|
|
59
|
+
brands = /* @__PURE__ */ new Map();
|
|
59
60
|
usesJsonElement = false;
|
|
60
61
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
61
62
|
this.camelCaseProperties = camelCaseProperties;
|
|
@@ -99,6 +100,15 @@ var TypeRegistry = class {
|
|
|
99
100
|
sealedVariantPath(name) {
|
|
100
101
|
return this.sealedVariantPaths.get(name);
|
|
101
102
|
}
|
|
103
|
+
addBrand(brand) {
|
|
104
|
+
const existing = this.brands.get(brand.name);
|
|
105
|
+
if (existing !== void 0 && existing.rawType !== brand.rawType) throw new Error(`@kizunajs/kotlin: brand ${stringify(brand.name)} wraps both ${existing.rawType} and ${brand.rawType}. Give each its own name.`);
|
|
106
|
+
if (this.types.has(brand.name)) throw new Error(`@kizunajs/kotlin: brand ${stringify(brand.name)} has the same name as a model. Give each its own name.`);
|
|
107
|
+
this.brands.set(brand.name, brand);
|
|
108
|
+
}
|
|
109
|
+
allBrands() {
|
|
110
|
+
return Array.from(this.brands.values());
|
|
111
|
+
}
|
|
102
112
|
warnJsonElement(hint, reason) {
|
|
103
113
|
this.usesJsonElement = true;
|
|
104
114
|
this.warningSet.add(`${hint}: ${reason}`);
|
|
@@ -117,8 +127,7 @@ const propertyName = (key, camelCase) => {
|
|
|
117
127
|
if (!camel) return "field";
|
|
118
128
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
119
129
|
};
|
|
120
|
-
const objectFields = (schema, registry, hint) => {
|
|
121
|
-
const shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {};
|
|
130
|
+
const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
|
|
122
131
|
const fields = [];
|
|
123
132
|
const seen = /* @__PURE__ */ new Map();
|
|
124
133
|
for (const [key, value] of Object.entries(shape)) {
|
|
@@ -133,6 +142,7 @@ const objectFields = (schema, registry, hint) => {
|
|
|
133
142
|
wireName: key,
|
|
134
143
|
type: result.expression,
|
|
135
144
|
optional: result.optional,
|
|
145
|
+
requiredNullable: result.requiredNullable,
|
|
136
146
|
description: (0, kizunajs_generator.readMetaDescription)(value),
|
|
137
147
|
isFile: result.isFile,
|
|
138
148
|
deprecated: deprecation !== void 0,
|
|
@@ -142,12 +152,38 @@ const objectFields = (schema, registry, hint) => {
|
|
|
142
152
|
return fields;
|
|
143
153
|
};
|
|
144
154
|
const stringify = (value) => JSON.stringify(value);
|
|
155
|
+
const BRANDABLE_TYPES = new Set([
|
|
156
|
+
"String",
|
|
157
|
+
"Int",
|
|
158
|
+
"Long",
|
|
159
|
+
"Double",
|
|
160
|
+
"Boolean",
|
|
161
|
+
"Instant"
|
|
162
|
+
]);
|
|
145
163
|
const mapType = (schema, registry, hint) => {
|
|
164
|
+
const result = mapUnbrandedType(schema, registry, hint);
|
|
165
|
+
const brand = (0, kizunajs_generator.readMetaBrand)(schema);
|
|
166
|
+
if (brand === void 0 || !BRANDABLE_TYPES.has(result.expression)) return result;
|
|
167
|
+
const name = (0, kizunajs_generator.sanitizeIdentifier)(brand);
|
|
168
|
+
registry.addBrand({
|
|
169
|
+
name,
|
|
170
|
+
rawType: result.expression
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
expression: name,
|
|
174
|
+
optional: false
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const mapUnbrandedType = (schema, registry, hint) => {
|
|
146
178
|
if ((0, kizunajs_generator.isFileSchema)(schema)) return {
|
|
147
179
|
expression: "MultipartFile",
|
|
148
180
|
optional: false,
|
|
149
181
|
isFile: true
|
|
150
182
|
};
|
|
183
|
+
if ((0, kizunajs_generator.isBinarySchema)(schema)) return {
|
|
184
|
+
expression: "ByteArray",
|
|
185
|
+
optional: false
|
|
186
|
+
};
|
|
151
187
|
const id = (0, kizunajs_generator.readMetaId)(schema);
|
|
152
188
|
const def = (0, kizunajs_generator.readDef)(schema);
|
|
153
189
|
const discriminated = (0, kizunajs_generator.readDiscriminatedUnion)(schema);
|
|
@@ -223,12 +259,13 @@ const mapType = (schema, registry, hint) => {
|
|
|
223
259
|
optional: false
|
|
224
260
|
};
|
|
225
261
|
}
|
|
226
|
-
const { inner, optional } = (0, kizunajs_generator.unwrapOptionalWrappers)(schema);
|
|
227
|
-
if (optional) {
|
|
262
|
+
const { inner, optional, nullable } = (0, kizunajs_generator.unwrapOptionalWrappers)(schema);
|
|
263
|
+
if (optional || nullable) {
|
|
228
264
|
const innerResult = mapType(inner, registry, hint);
|
|
229
265
|
return {
|
|
230
266
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
231
267
|
optional: true,
|
|
268
|
+
requiredNullable: nullable && !optional,
|
|
232
269
|
isFile: innerResult.isFile
|
|
233
270
|
};
|
|
234
271
|
}
|
|
@@ -277,8 +314,9 @@ const mapType = (schema, registry, hint) => {
|
|
|
277
314
|
optional: false
|
|
278
315
|
};
|
|
279
316
|
}
|
|
317
|
+
const elementType = mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "");
|
|
280
318
|
return {
|
|
281
|
-
expression: `List<${
|
|
319
|
+
expression: `List<${(0, kizunajs_generator.unwrapOptionalWrappers)(element).nullable ? `${elementType}?` : elementType}>`,
|
|
282
320
|
optional: false
|
|
283
321
|
};
|
|
284
322
|
}
|
|
@@ -412,6 +450,7 @@ const sanitizeCaseName = (literal) => {
|
|
|
412
450
|
const collectObjectFields = (schema, registry, hint) => {
|
|
413
451
|
return objectFields(schema, registry, hint);
|
|
414
452
|
};
|
|
453
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true;
|
|
415
454
|
const objectFieldCount = (schema) => {
|
|
416
455
|
return Object.keys((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
|
|
417
456
|
};
|
|
@@ -576,7 +615,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
576
615
|
stream
|
|
577
616
|
};
|
|
578
617
|
};
|
|
579
|
-
const
|
|
618
|
+
const kotlinRoutesWalk = (options) => {
|
|
580
619
|
const flatMethods = [];
|
|
581
620
|
const groupMap = /* @__PURE__ */ new Map();
|
|
582
621
|
return {
|
|
@@ -608,7 +647,7 @@ const kotlinGenerator = (0, kizunajs_generator.createGenerator)((options, _api)
|
|
|
608
647
|
};
|
|
609
648
|
}
|
|
610
649
|
};
|
|
611
|
-
}
|
|
650
|
+
};
|
|
612
651
|
const KOTLIN_KEYWORDS = new Set([
|
|
613
652
|
"as",
|
|
614
653
|
"break",
|
|
@@ -642,6 +681,10 @@ const KOTLIN_KEYWORDS = new Set([
|
|
|
642
681
|
const escapeKeyword = (name) => {
|
|
643
682
|
return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
644
683
|
};
|
|
684
|
+
const defaultArgument = (field) => {
|
|
685
|
+
if (!hasDefault(field)) return "";
|
|
686
|
+
return " = null";
|
|
687
|
+
};
|
|
645
688
|
const optionalize = (type, optional) => {
|
|
646
689
|
if (!optional) return type;
|
|
647
690
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -653,8 +696,13 @@ const KOTLIN_PRIMITIVE_TYPES = new Set([
|
|
|
653
696
|
"Long",
|
|
654
697
|
"Boolean",
|
|
655
698
|
"Instant",
|
|
656
|
-
"Unit"
|
|
699
|
+
"Unit",
|
|
700
|
+
"ByteArray"
|
|
657
701
|
]);
|
|
702
|
+
/**
|
|
703
|
+
* How a response body is read. Raw bytes are the body already.
|
|
704
|
+
*/
|
|
705
|
+
const decodeBody = (resolved) => resolved === "ByteArray" ? "data" : `json.decodeFromString<${resolved}>(data.decodeToString())`;
|
|
658
706
|
const resolveType = (typeName, currentOperation, context, scope = "client") => {
|
|
659
707
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
660
708
|
const optional = typeName.endsWith("?");
|
|
@@ -797,7 +845,7 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
797
845
|
if (!hasFile) writer.line("@Serializable");
|
|
798
846
|
const params = adjustedFields.map((field) => {
|
|
799
847
|
const typeExpression = optionalize(field.type, field.optional);
|
|
800
|
-
const defaultPart = field
|
|
848
|
+
const defaultPart = defaultArgument(field);
|
|
801
849
|
const serialNameAnnotation = needsSerialName && (field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name)) ? `@SerialName(${stringLiteral(field.wireName)}) ` : "";
|
|
802
850
|
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
803
851
|
});
|
|
@@ -890,7 +938,7 @@ const emitSealedClass = (writer, type, registry, ownedTypeMap, ownedTypeLookup)
|
|
|
890
938
|
const params = fields.map((field) => {
|
|
891
939
|
const variantPath = registry.sealedVariantPath(variant.payloadType) ?? variant.payloadType;
|
|
892
940
|
const typeExpression = optionalize(relativeOwnedPath(field.type, ownedTypeMap, variantPath, registry) ?? ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
893
|
-
const defaultPart = field
|
|
941
|
+
const defaultPart = defaultArgument(field);
|
|
894
942
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
895
943
|
});
|
|
896
944
|
if (params.length === 1) writer.line(`data class ${variant.caseName}(${params[0]}) : ${type.name}`);
|
|
@@ -927,6 +975,16 @@ const emitType = (writer, type, ownedTypeMap = /* @__PURE__ */ new Map(), ownedT
|
|
|
927
975
|
else if (type.kind === "enum-class") emitEnumClass(writer, type.name, type.cases, type.unknownCase, type.description);
|
|
928
976
|
else if (registry) emitSealedClass(writer, type, registry, ownedTypeMap, ownedTypeLookup);
|
|
929
977
|
};
|
|
978
|
+
/**
|
|
979
|
+
* A brand as a value class that serializes and prints as the bare value.
|
|
980
|
+
*/
|
|
981
|
+
const emitBrand = (writer, brand) => {
|
|
982
|
+
writer.line("@Serializable");
|
|
983
|
+
writer.line("@JvmInline");
|
|
984
|
+
writer.block(`value class ${brand.name}(val value: ${brand.rawType})`, () => {
|
|
985
|
+
writer.line(`override fun toString(): String = ${brand.rawType === "String" ? "value" : "value.toString()"}`);
|
|
986
|
+
});
|
|
987
|
+
};
|
|
930
988
|
const emitTypes = (writer, types, ownedTypeMap = /* @__PURE__ */ new Map(), ownedTypeLookup = /* @__PURE__ */ new Map(), registry) => {
|
|
931
989
|
for (const type of types) emitType(writer, type, ownedTypeMap, ownedTypeLookup, void 0, registry);
|
|
932
990
|
};
|
|
@@ -1002,7 +1060,7 @@ const emitOperationResultTypes = (writer, method, context) => {
|
|
|
1002
1060
|
writer.line(") {");
|
|
1003
1061
|
writer.indent(() => {
|
|
1004
1062
|
writer.line("/** How far along the call is. */");
|
|
1005
|
-
writer.line("enum class State { Running, Done, Failed }");
|
|
1063
|
+
writer.line("enum class State { Running, NeedsApproval, Done, Failed }");
|
|
1006
1064
|
});
|
|
1007
1065
|
writer.line("}");
|
|
1008
1066
|
writer.blank();
|
|
@@ -1012,7 +1070,12 @@ const emitOperationResultTypes = (writer, method, context) => {
|
|
|
1012
1070
|
writer.block("for (event in events)", () => {
|
|
1013
1071
|
writer.line("when (event) {");
|
|
1014
1072
|
writer.indent(() => {
|
|
1015
|
-
writer.line("is Event.ToolCall ->
|
|
1073
|
+
writer.line("is Event.ToolCall -> {");
|
|
1074
|
+
writer.indent(() => {
|
|
1075
|
+
writer.line("val tracked = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(call = event.data)");
|
|
1076
|
+
writer.line("calls[event.data.id] = if (event.data.needsApproval == true && tracked.state == ToolCallRecord.State.Running) tracked.copy(state = ToolCallRecord.State.NeedsApproval) else tracked");
|
|
1077
|
+
});
|
|
1078
|
+
writer.line("}");
|
|
1016
1079
|
writer.line("is Event.ToolResult -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(state = ToolCallRecord.State.Done, result = event.data)");
|
|
1017
1080
|
if (eventNames.has("tool_error")) writer.line("is Event.ToolError -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name.wireValue)).copy(state = ToolCallRecord.State.Failed, message = event.data.message)");
|
|
1018
1081
|
writer.line("else -> Unit");
|
|
@@ -1074,7 +1137,7 @@ const groupTypeRef = (operationName, className, context) => `${context.clientNam
|
|
|
1074
1137
|
const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
|
|
1075
1138
|
const groupClassField = (field) => {
|
|
1076
1139
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1077
|
-
const defaultPart = field
|
|
1140
|
+
const defaultPart = defaultArgument(field);
|
|
1078
1141
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1079
1142
|
};
|
|
1080
1143
|
const emitGroupClass = (writer, className, fields, method, context) => {
|
|
@@ -1133,13 +1196,13 @@ const methodChannels = (method) => {
|
|
|
1133
1196
|
if (method.query.length > 0) channels.push({
|
|
1134
1197
|
varName: "query",
|
|
1135
1198
|
className: "Query",
|
|
1136
|
-
required: method.query.some((field) => !field
|
|
1199
|
+
required: method.query.some((field) => !hasDefault(field)),
|
|
1137
1200
|
fields: method.query
|
|
1138
1201
|
});
|
|
1139
1202
|
if (method.headers.length > 0) channels.push({
|
|
1140
1203
|
varName: "headers",
|
|
1141
1204
|
className: "Headers",
|
|
1142
|
-
required: method.headers.some((field) => !field
|
|
1205
|
+
required: method.headers.some((field) => !hasDefault(field)),
|
|
1143
1206
|
fields: method.headers
|
|
1144
1207
|
});
|
|
1145
1208
|
if (hasBodyGroup(method)) {
|
|
@@ -1188,7 +1251,7 @@ const emitArgsScaffolding = (writer, method, context) => {
|
|
|
1188
1251
|
};
|
|
1189
1252
|
const factorySignature = (channel) => channel.fields.map((field) => {
|
|
1190
1253
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1191
|
-
const defaultPart = field
|
|
1254
|
+
const defaultPart = defaultArgument(field);
|
|
1192
1255
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1193
1256
|
}).join(", ");
|
|
1194
1257
|
const factoryConstruction = (channel) => {
|
|
@@ -1245,10 +1308,16 @@ const emitBodyEncoding = (writer, method, context) => {
|
|
|
1245
1308
|
if (body.kind === "multipart") {
|
|
1246
1309
|
writer.line("val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)");
|
|
1247
1310
|
for (const field of body.multipartFields) {
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1311
|
+
const flattenedField = body.flattened.find((flatField) => flatField.name === field.name);
|
|
1312
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1313
|
+
const append = (accessor) => {
|
|
1314
|
+
if (isFile) writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.filename, ${accessor}.data.toRequestBody(${accessor}.mimeType.toMediaType()))`);
|
|
1315
|
+
else writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.toString())`);
|
|
1316
|
+
};
|
|
1317
|
+
if (flattenedField?.optional === true) writer.block(`if (${groupMemberAccessor("body", field)} != null)`, () => {
|
|
1318
|
+
append(groupMemberAccessor("body", field));
|
|
1319
|
+
});
|
|
1320
|
+
else append(groupMemberAccessor("body", field));
|
|
1252
1321
|
}
|
|
1253
1322
|
writer.line("requestBody = multipartBuilder.build()");
|
|
1254
1323
|
return;
|
|
@@ -1324,7 +1393,7 @@ const emitMethodBody = (writer, method, context) => {
|
|
|
1324
1393
|
else setHeader(accessor);
|
|
1325
1394
|
}
|
|
1326
1395
|
writer.line("requestInterceptor?.invoke(requestBuilder)");
|
|
1327
|
-
writer.line("val httpResponse = Kizuna.
|
|
1396
|
+
writer.line("val httpResponse = Kizuna.send(client, requestBuilder.build(), retry)");
|
|
1328
1397
|
if (method.stream) {
|
|
1329
1398
|
emitStreamMethodTail(writer, method, context);
|
|
1330
1399
|
return;
|
|
@@ -1344,7 +1413,7 @@ const emitMethodBody = (writer, method, context) => {
|
|
|
1344
1413
|
writer.indent(() => {
|
|
1345
1414
|
const resolved = resolveType(successResponse.type, method.operationName, context);
|
|
1346
1415
|
writer.block("try", () => {
|
|
1347
|
-
writer.line(`val payload =
|
|
1416
|
+
writer.line(`val payload = ${decodeBody(resolved)}`);
|
|
1348
1417
|
const bodyExpr = isMultiSuccess ? `${operationRef}.Success.Status${successResponse.status}(payload)` : "payload";
|
|
1349
1418
|
if (hasHeaders) {
|
|
1350
1419
|
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
@@ -1381,7 +1450,7 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1381
1450
|
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1382
1451
|
writer.line("val payload = try {");
|
|
1383
1452
|
writer.indent(() => {
|
|
1384
|
-
writer.line(
|
|
1453
|
+
writer.line(decodeBody(resolved));
|
|
1385
1454
|
});
|
|
1386
1455
|
writer.line(`} catch (error: Exception) { throw ${failureRef}.Decoding(error, statusCode, data) }`);
|
|
1387
1456
|
writer.line(`throw ${failureRef}.${caseName}(body = payload)`);
|
|
@@ -1394,7 +1463,7 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1394
1463
|
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1395
1464
|
writer.line(`val ${errorCase.caseName} = try {`);
|
|
1396
1465
|
writer.indent(() => {
|
|
1397
|
-
writer.line(
|
|
1466
|
+
writer.line(decodeBody(resolved));
|
|
1398
1467
|
});
|
|
1399
1468
|
writer.line("} catch (_: Exception) { null }");
|
|
1400
1469
|
writer.line(`if (${errorCase.caseName} != null) throw ${failureRef}.${caseName}(body = ${errorCase.caseName})`);
|
|
@@ -1476,7 +1545,7 @@ const emitSubClientMethod = (writer, method, context) => {
|
|
|
1476
1545
|
};
|
|
1477
1546
|
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1478
1547
|
writer.blank();
|
|
1479
|
-
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?)`, () => {
|
|
1548
|
+
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?)`, () => {
|
|
1480
1549
|
for (const method of group.methods) {
|
|
1481
1550
|
writer.blank();
|
|
1482
1551
|
emitSubClientMethod(writer, method, context);
|
|
@@ -1581,6 +1650,47 @@ const emitKizunaObject = (writer, options) => {
|
|
|
1581
1650
|
writer.line("}");
|
|
1582
1651
|
});
|
|
1583
1652
|
writer.blank();
|
|
1653
|
+
writer.block(`suspend fun send(client: OkHttpClient, request: Request, retry: ${options.clientName}.RetryPolicy?): Response`, () => {
|
|
1654
|
+
writer.line("val idempotent = request.method in setOf(\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\")");
|
|
1655
|
+
writer.line("val policy = retry?.takeIf { idempotent && request.body?.isOneShot() != true }");
|
|
1656
|
+
writer.line("var attempt = 1");
|
|
1657
|
+
writer.block("while (true)", () => {
|
|
1658
|
+
writer.line("val response = try {");
|
|
1659
|
+
writer.line(" execute(client, request)");
|
|
1660
|
+
writer.line("} catch (error: java.io.IOException) {");
|
|
1661
|
+
writer.line(" val delay = policy?.let { retryDelay(it, attempt, null) } ?: throw error");
|
|
1662
|
+
writer.line(" kotlinx.coroutines.delay(delay)");
|
|
1663
|
+
writer.line(" attempt += 1");
|
|
1664
|
+
writer.line(" continue");
|
|
1665
|
+
writer.line("}");
|
|
1666
|
+
writer.line("val delay = policy?.let { retryDelay(it, attempt, response) } ?: return response");
|
|
1667
|
+
writer.line("response.close()");
|
|
1668
|
+
writer.line("kotlinx.coroutines.delay(delay)");
|
|
1669
|
+
writer.line("attempt += 1");
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
writer.blank();
|
|
1673
|
+
writer.block(`fun retryDelay(policy: ${options.clientName}.RetryPolicy, attempt: Int, response: Response?): Long?`, () => {
|
|
1674
|
+
writer.line("if (attempt >= policy.maximumAttempts) return null");
|
|
1675
|
+
writer.line("val ceiling = minOf(policy.maximumDelay.inWholeMilliseconds, policy.initialDelay.inWholeMilliseconds shl (attempt - 1))");
|
|
1676
|
+
writer.line("val backoff = (Math.random() * ceiling).toLong()");
|
|
1677
|
+
writer.line("if (response == null) return backoff");
|
|
1678
|
+
writer.line("if (response.code != 429 && response.code != 503) return null");
|
|
1679
|
+
writer.line("val asked = retryAfter(response.header(\"Retry-After\")) ?: return backoff");
|
|
1680
|
+
writer.line("return if (asked <= policy.maximumDelay.inWholeMilliseconds) asked else null");
|
|
1681
|
+
});
|
|
1682
|
+
writer.blank();
|
|
1683
|
+
writer.block("fun retryAfter(header: String?): Long?", () => {
|
|
1684
|
+
writer.line("val value = header?.trim() ?: return null");
|
|
1685
|
+
writer.line("value.toLongOrNull()?.let { return if (it >= 0) it * 1000 else null }");
|
|
1686
|
+
writer.line("val date = try {");
|
|
1687
|
+
writer.line(" java.time.ZonedDateTime.parse(value, java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME)");
|
|
1688
|
+
writer.line("} catch (_: java.time.format.DateTimeParseException) {");
|
|
1689
|
+
writer.line(" return null");
|
|
1690
|
+
writer.line("}");
|
|
1691
|
+
writer.line("return maxOf(0L, date.toInstant().toEpochMilli() - System.currentTimeMillis())");
|
|
1692
|
+
});
|
|
1693
|
+
writer.blank();
|
|
1584
1694
|
writer.block("suspend fun execute(client: OkHttpClient, request: Request): Response", () => {
|
|
1585
1695
|
writer.line("return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->");
|
|
1586
1696
|
writer.indent(() => {
|
|
@@ -1608,20 +1718,34 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1608
1718
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1609
1719
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1610
1720
|
const contextFields = context.requestContextFields;
|
|
1611
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1721
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1612
1722
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1613
1723
|
writer.blank();
|
|
1614
|
-
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null)`, () => {
|
|
1724
|
+
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null)`, () => {
|
|
1615
1725
|
if (contextFields.length > 0) {
|
|
1616
1726
|
writer.blank();
|
|
1617
1727
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
1618
|
-
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${field
|
|
1728
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`));
|
|
1619
1729
|
writer.blank();
|
|
1620
1730
|
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1621
1731
|
for (const field of contextFields) if (field.optional) writer.line(`if (requestContext.${escapeKeyword(field.name)} != null) put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)}!!)`);
|
|
1622
1732
|
else writer.line(`put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)})`);
|
|
1623
1733
|
});
|
|
1624
1734
|
}
|
|
1735
|
+
writer.blank();
|
|
1736
|
+
writer.line("/**");
|
|
1737
|
+
writer.line(" * Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or was answered 429 or 503,");
|
|
1738
|
+
writer.line(" * waiting what Retry-After asks or backing off.");
|
|
1739
|
+
writer.line(" *");
|
|
1740
|
+
writer.line(" * @property maximumAttempts Attempts in all, the first included.");
|
|
1741
|
+
writer.line(" * @property initialDelay What the first backoff waits at most.");
|
|
1742
|
+
writer.line(" * @property maximumDelay What any wait may take.");
|
|
1743
|
+
writer.line(" */");
|
|
1744
|
+
emitConstructorClass(writer, "data class RetryPolicy", [
|
|
1745
|
+
"val maximumAttempts: Int = 3",
|
|
1746
|
+
"val initialDelay: kotlin.time.Duration = 500.milliseconds",
|
|
1747
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1748
|
+
]);
|
|
1625
1749
|
if (usesMultipart) {
|
|
1626
1750
|
writer.blank();
|
|
1627
1751
|
emitConstructorClass(writer, "data class MultipartFile", [
|
|
@@ -1677,7 +1801,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1677
1801
|
}
|
|
1678
1802
|
if (groups.length > 0) for (const group of groups) {
|
|
1679
1803
|
writer.blank();
|
|
1680
|
-
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor)`);
|
|
1804
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry)`);
|
|
1681
1805
|
}
|
|
1682
1806
|
for (const method of flatMethods) {
|
|
1683
1807
|
writer.blank();
|
|
@@ -1685,21 +1809,8 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1685
1809
|
}
|
|
1686
1810
|
});
|
|
1687
1811
|
};
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
*
|
|
1691
|
-
* @param api - What `defineConfig` assembled.
|
|
1692
|
-
* @param config - Override the generated names:
|
|
1693
|
-
* - `namespaceName`: the object wrapping shared types.
|
|
1694
|
-
* - `packageName`: optional package declaration for the generated file.
|
|
1695
|
-
*/
|
|
1696
|
-
const generateKotlinClient = (api, config) => {
|
|
1697
|
-
const { namespaceName, packageName, camelCaseProperties = false, unknownEnumCase = false } = config;
|
|
1698
|
-
const registry = new TypeRegistry(camelCaseProperties, unknownEnumCase);
|
|
1699
|
-
const partition = kotlinGenerator(api, {
|
|
1700
|
-
namespaceName,
|
|
1701
|
-
registry
|
|
1702
|
-
});
|
|
1812
|
+
const renderKotlinClient = (api, partition, registry, config) => {
|
|
1813
|
+
const { namespaceName, packageName, camelCaseProperties = false } = config;
|
|
1703
1814
|
const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
|
|
1704
1815
|
const operationTypeMap = buildOperationTypeMap(allMethods, registry);
|
|
1705
1816
|
const clientName = `${namespaceName}Client`;
|
|
@@ -1735,6 +1846,8 @@ const generateKotlinClient = (api, config) => {
|
|
|
1735
1846
|
writer.line("import okhttp3.*");
|
|
1736
1847
|
writer.line("import okhttp3.MediaType.Companion.toMediaType");
|
|
1737
1848
|
writer.line("import okhttp3.RequestBody.Companion.toRequestBody");
|
|
1849
|
+
writer.line("import kotlin.time.Duration.Companion.milliseconds");
|
|
1850
|
+
writer.line("import kotlin.time.Duration.Companion.seconds");
|
|
1738
1851
|
writer.line("import okhttp3.HttpUrl.Companion.toHttpUrl");
|
|
1739
1852
|
writer.blank();
|
|
1740
1853
|
if (registry.all().some((type) => type.kind === "enum-class")) {
|
|
@@ -1784,13 +1897,43 @@ const generateKotlinClient = (api, config) => {
|
|
|
1784
1897
|
};
|
|
1785
1898
|
writer.block(`object ${namespaceName}`, () => {
|
|
1786
1899
|
emitTypes(writer, topLevelSharedTypes, ownedTypeMap, ownedTypeLookup, registry);
|
|
1900
|
+
for (const brand of registry.allBrands()) {
|
|
1901
|
+
writer.blank();
|
|
1902
|
+
emitBrand(writer, brand);
|
|
1903
|
+
}
|
|
1787
1904
|
});
|
|
1788
1905
|
emitClient(writer, { clientName }, partition, context, typesByOperation, registry);
|
|
1789
1906
|
for (const group of partition.groups) emitSubClientClass(writer, group, clientName, context);
|
|
1790
|
-
emitKizunaObject(writer, {
|
|
1907
|
+
emitKizunaObject(writer, {
|
|
1908
|
+
streaming: usesStreaming,
|
|
1909
|
+
clientName
|
|
1910
|
+
});
|
|
1791
1911
|
for (const warning of registry.warnings()) process.stderr.write(`[kizuna/kotlin] JsonElement fallback at ${warning}\n`);
|
|
1792
1912
|
return writer.toString();
|
|
1793
1913
|
};
|
|
1914
|
+
/**
|
|
1915
|
+
* The Kotlin client's walk: the routes into a partition, then the file.
|
|
1916
|
+
*/
|
|
1917
|
+
const kotlinClientWalk = (api, config) => {
|
|
1918
|
+
const registry = new TypeRegistry(config.camelCaseProperties ?? false, config.unknownEnumCase ?? false);
|
|
1919
|
+
const routes = kotlinRoutesWalk({
|
|
1920
|
+
namespaceName: config.namespaceName,
|
|
1921
|
+
registry
|
|
1922
|
+
});
|
|
1923
|
+
return {
|
|
1924
|
+
processRoute: routes.processRoute,
|
|
1925
|
+
finalize: () => renderKotlinClient(api, routes.finalize(), registry, config)
|
|
1926
|
+
};
|
|
1927
|
+
};
|
|
1928
|
+
/**
|
|
1929
|
+
* Generate a Kotlin API client from a Kizuna api.
|
|
1930
|
+
*
|
|
1931
|
+
* @param api - What `defineConfig` assembled.
|
|
1932
|
+
* @param config - Override the generated names:
|
|
1933
|
+
* - `namespaceName`: the object wrapping shared types.
|
|
1934
|
+
* - `packageName`: optional package declaration for the generated file.
|
|
1935
|
+
*/
|
|
1936
|
+
const generateKotlinClient = (api, config) => (0, kizunajs_generator.walkApi)(api, kotlinClientWalk(api, config), { skipHidden: true });
|
|
1794
1937
|
//#endregion
|
|
1795
1938
|
Object.defineProperty(exports, "generateKotlinClient", {
|
|
1796
1939
|
enumerable: true,
|
|
@@ -1798,3 +1941,9 @@ Object.defineProperty(exports, "generateKotlinClient", {
|
|
|
1798
1941
|
return generateKotlinClient;
|
|
1799
1942
|
}
|
|
1800
1943
|
});
|
|
1944
|
+
Object.defineProperty(exports, "kotlinClientWalk", {
|
|
1945
|
+
enumerable: true,
|
|
1946
|
+
get: function() {
|
|
1947
|
+
return kotlinClientWalk;
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isBinarySchema, isDiscriminatedUnionSchema, isFileSchema, isHintPrefix, isIntegerSchema, isNamedStream, isObjectSchema, isStreamResponse, isSuccessStatus, localTypeName, mergeHeaderFields, parsePath, readDef, readDefType, readDeprecation, readDiscriminatedUnion, readDiscriminatorStringLiteral, readMetaBrand, readMetaDescription, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, routeStreams, sanitizeFieldName, sanitizeIdentifier, shortTypeName, soleStreamResponse, statusToCamelCase, streamMode, toCamelCase, toPascalCase, unwrapOptionalWrappers, walkApi } from "kizunajs/generator";
|
|
2
2
|
//#region src/emit.ts
|
|
3
3
|
var KotlinWriter = class {
|
|
4
4
|
lines = [];
|
|
@@ -56,6 +56,7 @@ var TypeRegistry = class {
|
|
|
56
56
|
explicitIds = /* @__PURE__ */ new Set();
|
|
57
57
|
sealedVariantPayloads = /* @__PURE__ */ new Set();
|
|
58
58
|
sealedVariantPaths = /* @__PURE__ */ new Map();
|
|
59
|
+
brands = /* @__PURE__ */ new Map();
|
|
59
60
|
usesJsonElement = false;
|
|
60
61
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
61
62
|
this.camelCaseProperties = camelCaseProperties;
|
|
@@ -99,6 +100,15 @@ var TypeRegistry = class {
|
|
|
99
100
|
sealedVariantPath(name) {
|
|
100
101
|
return this.sealedVariantPaths.get(name);
|
|
101
102
|
}
|
|
103
|
+
addBrand(brand) {
|
|
104
|
+
const existing = this.brands.get(brand.name);
|
|
105
|
+
if (existing !== void 0 && existing.rawType !== brand.rawType) throw new Error(`@kizunajs/kotlin: brand ${stringify(brand.name)} wraps both ${existing.rawType} and ${brand.rawType}. Give each its own name.`);
|
|
106
|
+
if (this.types.has(brand.name)) throw new Error(`@kizunajs/kotlin: brand ${stringify(brand.name)} has the same name as a model. Give each its own name.`);
|
|
107
|
+
this.brands.set(brand.name, brand);
|
|
108
|
+
}
|
|
109
|
+
allBrands() {
|
|
110
|
+
return Array.from(this.brands.values());
|
|
111
|
+
}
|
|
102
112
|
warnJsonElement(hint, reason) {
|
|
103
113
|
this.usesJsonElement = true;
|
|
104
114
|
this.warningSet.add(`${hint}: ${reason}`);
|
|
@@ -117,8 +127,7 @@ const propertyName = (key, camelCase) => {
|
|
|
117
127
|
if (!camel) return "field";
|
|
118
128
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
119
129
|
};
|
|
120
|
-
const objectFields = (schema, registry, hint) => {
|
|
121
|
-
const shape = readObjectShape(schema) ?? {};
|
|
130
|
+
const objectFields = (schema, registry, hint, shape = readObjectShape(schema) ?? {}) => {
|
|
122
131
|
const fields = [];
|
|
123
132
|
const seen = /* @__PURE__ */ new Map();
|
|
124
133
|
for (const [key, value] of Object.entries(shape)) {
|
|
@@ -133,6 +142,7 @@ const objectFields = (schema, registry, hint) => {
|
|
|
133
142
|
wireName: key,
|
|
134
143
|
type: result.expression,
|
|
135
144
|
optional: result.optional,
|
|
145
|
+
requiredNullable: result.requiredNullable,
|
|
136
146
|
description: readMetaDescription(value),
|
|
137
147
|
isFile: result.isFile,
|
|
138
148
|
deprecated: deprecation !== void 0,
|
|
@@ -142,12 +152,38 @@ const objectFields = (schema, registry, hint) => {
|
|
|
142
152
|
return fields;
|
|
143
153
|
};
|
|
144
154
|
const stringify = (value) => JSON.stringify(value);
|
|
155
|
+
const BRANDABLE_TYPES = new Set([
|
|
156
|
+
"String",
|
|
157
|
+
"Int",
|
|
158
|
+
"Long",
|
|
159
|
+
"Double",
|
|
160
|
+
"Boolean",
|
|
161
|
+
"Instant"
|
|
162
|
+
]);
|
|
145
163
|
const mapType = (schema, registry, hint) => {
|
|
164
|
+
const result = mapUnbrandedType(schema, registry, hint);
|
|
165
|
+
const brand = readMetaBrand(schema);
|
|
166
|
+
if (brand === void 0 || !BRANDABLE_TYPES.has(result.expression)) return result;
|
|
167
|
+
const name = sanitizeIdentifier(brand);
|
|
168
|
+
registry.addBrand({
|
|
169
|
+
name,
|
|
170
|
+
rawType: result.expression
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
expression: name,
|
|
174
|
+
optional: false
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const mapUnbrandedType = (schema, registry, hint) => {
|
|
146
178
|
if (isFileSchema(schema)) return {
|
|
147
179
|
expression: "MultipartFile",
|
|
148
180
|
optional: false,
|
|
149
181
|
isFile: true
|
|
150
182
|
};
|
|
183
|
+
if (isBinarySchema(schema)) return {
|
|
184
|
+
expression: "ByteArray",
|
|
185
|
+
optional: false
|
|
186
|
+
};
|
|
151
187
|
const id = readMetaId(schema);
|
|
152
188
|
const def = readDef(schema);
|
|
153
189
|
const discriminated = readDiscriminatedUnion(schema);
|
|
@@ -223,12 +259,13 @@ const mapType = (schema, registry, hint) => {
|
|
|
223
259
|
optional: false
|
|
224
260
|
};
|
|
225
261
|
}
|
|
226
|
-
const { inner, optional } = unwrapOptionalWrappers(schema);
|
|
227
|
-
if (optional) {
|
|
262
|
+
const { inner, optional, nullable } = unwrapOptionalWrappers(schema);
|
|
263
|
+
if (optional || nullable) {
|
|
228
264
|
const innerResult = mapType(inner, registry, hint);
|
|
229
265
|
return {
|
|
230
266
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
231
267
|
optional: true,
|
|
268
|
+
requiredNullable: nullable && !optional,
|
|
232
269
|
isFile: innerResult.isFile
|
|
233
270
|
};
|
|
234
271
|
}
|
|
@@ -277,8 +314,9 @@ const mapType = (schema, registry, hint) => {
|
|
|
277
314
|
optional: false
|
|
278
315
|
};
|
|
279
316
|
}
|
|
317
|
+
const elementType = mapType(element, registry, `${hint}Item`).expression.replace(/\?$/, "");
|
|
280
318
|
return {
|
|
281
|
-
expression: `List<${
|
|
319
|
+
expression: `List<${unwrapOptionalWrappers(element).nullable ? `${elementType}?` : elementType}>`,
|
|
282
320
|
optional: false
|
|
283
321
|
};
|
|
284
322
|
}
|
|
@@ -412,6 +450,7 @@ const sanitizeCaseName = (literal) => {
|
|
|
412
450
|
const collectObjectFields = (schema, registry, hint) => {
|
|
413
451
|
return objectFields(schema, registry, hint);
|
|
414
452
|
};
|
|
453
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true;
|
|
415
454
|
const objectFieldCount = (schema) => {
|
|
416
455
|
return Object.keys(readObjectShape(schema) ?? {}).length;
|
|
417
456
|
};
|
|
@@ -576,7 +615,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
576
615
|
stream
|
|
577
616
|
};
|
|
578
617
|
};
|
|
579
|
-
const
|
|
618
|
+
const kotlinRoutesWalk = (options) => {
|
|
580
619
|
const flatMethods = [];
|
|
581
620
|
const groupMap = /* @__PURE__ */ new Map();
|
|
582
621
|
return {
|
|
@@ -608,7 +647,7 @@ const kotlinGenerator = createGenerator((options, _api) => {
|
|
|
608
647
|
};
|
|
609
648
|
}
|
|
610
649
|
};
|
|
611
|
-
}
|
|
650
|
+
};
|
|
612
651
|
const KOTLIN_KEYWORDS = new Set([
|
|
613
652
|
"as",
|
|
614
653
|
"break",
|
|
@@ -642,6 +681,10 @@ const KOTLIN_KEYWORDS = new Set([
|
|
|
642
681
|
const escapeKeyword = (name) => {
|
|
643
682
|
return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
644
683
|
};
|
|
684
|
+
const defaultArgument = (field) => {
|
|
685
|
+
if (!hasDefault(field)) return "";
|
|
686
|
+
return " = null";
|
|
687
|
+
};
|
|
645
688
|
const optionalize = (type, optional) => {
|
|
646
689
|
if (!optional) return type;
|
|
647
690
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -653,8 +696,13 @@ const KOTLIN_PRIMITIVE_TYPES = new Set([
|
|
|
653
696
|
"Long",
|
|
654
697
|
"Boolean",
|
|
655
698
|
"Instant",
|
|
656
|
-
"Unit"
|
|
699
|
+
"Unit",
|
|
700
|
+
"ByteArray"
|
|
657
701
|
]);
|
|
702
|
+
/**
|
|
703
|
+
* How a response body is read. Raw bytes are the body already.
|
|
704
|
+
*/
|
|
705
|
+
const decodeBody = (resolved) => resolved === "ByteArray" ? "data" : `json.decodeFromString<${resolved}>(data.decodeToString())`;
|
|
658
706
|
const resolveType = (typeName, currentOperation, context, scope = "client") => {
|
|
659
707
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
660
708
|
const optional = typeName.endsWith("?");
|
|
@@ -797,7 +845,7 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
797
845
|
if (!hasFile) writer.line("@Serializable");
|
|
798
846
|
const params = adjustedFields.map((field) => {
|
|
799
847
|
const typeExpression = optionalize(field.type, field.optional);
|
|
800
|
-
const defaultPart = field
|
|
848
|
+
const defaultPart = defaultArgument(field);
|
|
801
849
|
const serialNameAnnotation = needsSerialName && (field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name)) ? `@SerialName(${stringLiteral(field.wireName)}) ` : "";
|
|
802
850
|
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
803
851
|
});
|
|
@@ -890,7 +938,7 @@ const emitSealedClass = (writer, type, registry, ownedTypeMap, ownedTypeLookup)
|
|
|
890
938
|
const params = fields.map((field) => {
|
|
891
939
|
const variantPath = registry.sealedVariantPath(variant.payloadType) ?? variant.payloadType;
|
|
892
940
|
const typeExpression = optionalize(relativeOwnedPath(field.type, ownedTypeMap, variantPath, registry) ?? ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
893
|
-
const defaultPart = field
|
|
941
|
+
const defaultPart = defaultArgument(field);
|
|
894
942
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
895
943
|
});
|
|
896
944
|
if (params.length === 1) writer.line(`data class ${variant.caseName}(${params[0]}) : ${type.name}`);
|
|
@@ -927,6 +975,16 @@ const emitType = (writer, type, ownedTypeMap = /* @__PURE__ */ new Map(), ownedT
|
|
|
927
975
|
else if (type.kind === "enum-class") emitEnumClass(writer, type.name, type.cases, type.unknownCase, type.description);
|
|
928
976
|
else if (registry) emitSealedClass(writer, type, registry, ownedTypeMap, ownedTypeLookup);
|
|
929
977
|
};
|
|
978
|
+
/**
|
|
979
|
+
* A brand as a value class that serializes and prints as the bare value.
|
|
980
|
+
*/
|
|
981
|
+
const emitBrand = (writer, brand) => {
|
|
982
|
+
writer.line("@Serializable");
|
|
983
|
+
writer.line("@JvmInline");
|
|
984
|
+
writer.block(`value class ${brand.name}(val value: ${brand.rawType})`, () => {
|
|
985
|
+
writer.line(`override fun toString(): String = ${brand.rawType === "String" ? "value" : "value.toString()"}`);
|
|
986
|
+
});
|
|
987
|
+
};
|
|
930
988
|
const emitTypes = (writer, types, ownedTypeMap = /* @__PURE__ */ new Map(), ownedTypeLookup = /* @__PURE__ */ new Map(), registry) => {
|
|
931
989
|
for (const type of types) emitType(writer, type, ownedTypeMap, ownedTypeLookup, void 0, registry);
|
|
932
990
|
};
|
|
@@ -1002,7 +1060,7 @@ const emitOperationResultTypes = (writer, method, context) => {
|
|
|
1002
1060
|
writer.line(") {");
|
|
1003
1061
|
writer.indent(() => {
|
|
1004
1062
|
writer.line("/** How far along the call is. */");
|
|
1005
|
-
writer.line("enum class State { Running, Done, Failed }");
|
|
1063
|
+
writer.line("enum class State { Running, NeedsApproval, Done, Failed }");
|
|
1006
1064
|
});
|
|
1007
1065
|
writer.line("}");
|
|
1008
1066
|
writer.blank();
|
|
@@ -1012,7 +1070,12 @@ const emitOperationResultTypes = (writer, method, context) => {
|
|
|
1012
1070
|
writer.block("for (event in events)", () => {
|
|
1013
1071
|
writer.line("when (event) {");
|
|
1014
1072
|
writer.indent(() => {
|
|
1015
|
-
writer.line("is Event.ToolCall ->
|
|
1073
|
+
writer.line("is Event.ToolCall -> {");
|
|
1074
|
+
writer.indent(() => {
|
|
1075
|
+
writer.line("val tracked = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(call = event.data)");
|
|
1076
|
+
writer.line("calls[event.data.id] = if (event.data.needsApproval == true && tracked.state == ToolCallRecord.State.Running) tracked.copy(state = ToolCallRecord.State.NeedsApproval) else tracked");
|
|
1077
|
+
});
|
|
1078
|
+
writer.line("}");
|
|
1016
1079
|
writer.line("is Event.ToolResult -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name)).copy(state = ToolCallRecord.State.Done, result = event.data)");
|
|
1017
1080
|
if (eventNames.has("tool_error")) writer.line("is Event.ToolError -> calls[event.data.id] = (calls[event.data.id] ?: ToolCallRecord(event.data.id, event.data.name.wireValue)).copy(state = ToolCallRecord.State.Failed, message = event.data.message)");
|
|
1018
1081
|
writer.line("else -> Unit");
|
|
@@ -1074,7 +1137,7 @@ const groupTypeRef = (operationName, className, context) => `${context.clientNam
|
|
|
1074
1137
|
const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
|
|
1075
1138
|
const groupClassField = (field) => {
|
|
1076
1139
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1077
|
-
const defaultPart = field
|
|
1140
|
+
const defaultPart = defaultArgument(field);
|
|
1078
1141
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1079
1142
|
};
|
|
1080
1143
|
const emitGroupClass = (writer, className, fields, method, context) => {
|
|
@@ -1133,13 +1196,13 @@ const methodChannels = (method) => {
|
|
|
1133
1196
|
if (method.query.length > 0) channels.push({
|
|
1134
1197
|
varName: "query",
|
|
1135
1198
|
className: "Query",
|
|
1136
|
-
required: method.query.some((field) => !field
|
|
1199
|
+
required: method.query.some((field) => !hasDefault(field)),
|
|
1137
1200
|
fields: method.query
|
|
1138
1201
|
});
|
|
1139
1202
|
if (method.headers.length > 0) channels.push({
|
|
1140
1203
|
varName: "headers",
|
|
1141
1204
|
className: "Headers",
|
|
1142
|
-
required: method.headers.some((field) => !field
|
|
1205
|
+
required: method.headers.some((field) => !hasDefault(field)),
|
|
1143
1206
|
fields: method.headers
|
|
1144
1207
|
});
|
|
1145
1208
|
if (hasBodyGroup(method)) {
|
|
@@ -1188,7 +1251,7 @@ const emitArgsScaffolding = (writer, method, context) => {
|
|
|
1188
1251
|
};
|
|
1189
1252
|
const factorySignature = (channel) => channel.fields.map((field) => {
|
|
1190
1253
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1191
|
-
const defaultPart = field
|
|
1254
|
+
const defaultPart = defaultArgument(field);
|
|
1192
1255
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1193
1256
|
}).join(", ");
|
|
1194
1257
|
const factoryConstruction = (channel) => {
|
|
@@ -1245,10 +1308,16 @@ const emitBodyEncoding = (writer, method, context) => {
|
|
|
1245
1308
|
if (body.kind === "multipart") {
|
|
1246
1309
|
writer.line("val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)");
|
|
1247
1310
|
for (const field of body.multipartFields) {
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1311
|
+
const flattenedField = body.flattened.find((flatField) => flatField.name === field.name);
|
|
1312
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1313
|
+
const append = (accessor) => {
|
|
1314
|
+
if (isFile) writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.filename, ${accessor}.data.toRequestBody(${accessor}.mimeType.toMediaType()))`);
|
|
1315
|
+
else writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.toString())`);
|
|
1316
|
+
};
|
|
1317
|
+
if (flattenedField?.optional === true) writer.block(`if (${groupMemberAccessor("body", field)} != null)`, () => {
|
|
1318
|
+
append(groupMemberAccessor("body", field));
|
|
1319
|
+
});
|
|
1320
|
+
else append(groupMemberAccessor("body", field));
|
|
1252
1321
|
}
|
|
1253
1322
|
writer.line("requestBody = multipartBuilder.build()");
|
|
1254
1323
|
return;
|
|
@@ -1324,7 +1393,7 @@ const emitMethodBody = (writer, method, context) => {
|
|
|
1324
1393
|
else setHeader(accessor);
|
|
1325
1394
|
}
|
|
1326
1395
|
writer.line("requestInterceptor?.invoke(requestBuilder)");
|
|
1327
|
-
writer.line("val httpResponse = Kizuna.
|
|
1396
|
+
writer.line("val httpResponse = Kizuna.send(client, requestBuilder.build(), retry)");
|
|
1328
1397
|
if (method.stream) {
|
|
1329
1398
|
emitStreamMethodTail(writer, method, context);
|
|
1330
1399
|
return;
|
|
@@ -1344,7 +1413,7 @@ const emitMethodBody = (writer, method, context) => {
|
|
|
1344
1413
|
writer.indent(() => {
|
|
1345
1414
|
const resolved = resolveType(successResponse.type, method.operationName, context);
|
|
1346
1415
|
writer.block("try", () => {
|
|
1347
|
-
writer.line(`val payload =
|
|
1416
|
+
writer.line(`val payload = ${decodeBody(resolved)}`);
|
|
1348
1417
|
const bodyExpr = isMultiSuccess ? `${operationRef}.Success.Status${successResponse.status}(payload)` : "payload";
|
|
1349
1418
|
if (hasHeaders) {
|
|
1350
1419
|
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
@@ -1381,7 +1450,7 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1381
1450
|
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1382
1451
|
writer.line("val payload = try {");
|
|
1383
1452
|
writer.indent(() => {
|
|
1384
|
-
writer.line(
|
|
1453
|
+
writer.line(decodeBody(resolved));
|
|
1385
1454
|
});
|
|
1386
1455
|
writer.line(`} catch (error: Exception) { throw ${failureRef}.Decoding(error, statusCode, data) }`);
|
|
1387
1456
|
writer.line(`throw ${failureRef}.${caseName}(body = payload)`);
|
|
@@ -1394,7 +1463,7 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1394
1463
|
const resolved = resolveType(errorCase.type, method.operationName, context);
|
|
1395
1464
|
writer.line(`val ${errorCase.caseName} = try {`);
|
|
1396
1465
|
writer.indent(() => {
|
|
1397
|
-
writer.line(
|
|
1466
|
+
writer.line(decodeBody(resolved));
|
|
1398
1467
|
});
|
|
1399
1468
|
writer.line("} catch (_: Exception) { null }");
|
|
1400
1469
|
writer.line(`if (${errorCase.caseName} != null) throw ${failureRef}.${caseName}(body = ${errorCase.caseName})`);
|
|
@@ -1476,7 +1545,7 @@ const emitSubClientMethod = (writer, method, context) => {
|
|
|
1476
1545
|
};
|
|
1477
1546
|
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1478
1547
|
writer.blank();
|
|
1479
|
-
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?)`, () => {
|
|
1548
|
+
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?)`, () => {
|
|
1480
1549
|
for (const method of group.methods) {
|
|
1481
1550
|
writer.blank();
|
|
1482
1551
|
emitSubClientMethod(writer, method, context);
|
|
@@ -1581,6 +1650,47 @@ const emitKizunaObject = (writer, options) => {
|
|
|
1581
1650
|
writer.line("}");
|
|
1582
1651
|
});
|
|
1583
1652
|
writer.blank();
|
|
1653
|
+
writer.block(`suspend fun send(client: OkHttpClient, request: Request, retry: ${options.clientName}.RetryPolicy?): Response`, () => {
|
|
1654
|
+
writer.line("val idempotent = request.method in setOf(\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\")");
|
|
1655
|
+
writer.line("val policy = retry?.takeIf { idempotent && request.body?.isOneShot() != true }");
|
|
1656
|
+
writer.line("var attempt = 1");
|
|
1657
|
+
writer.block("while (true)", () => {
|
|
1658
|
+
writer.line("val response = try {");
|
|
1659
|
+
writer.line(" execute(client, request)");
|
|
1660
|
+
writer.line("} catch (error: java.io.IOException) {");
|
|
1661
|
+
writer.line(" val delay = policy?.let { retryDelay(it, attempt, null) } ?: throw error");
|
|
1662
|
+
writer.line(" kotlinx.coroutines.delay(delay)");
|
|
1663
|
+
writer.line(" attempt += 1");
|
|
1664
|
+
writer.line(" continue");
|
|
1665
|
+
writer.line("}");
|
|
1666
|
+
writer.line("val delay = policy?.let { retryDelay(it, attempt, response) } ?: return response");
|
|
1667
|
+
writer.line("response.close()");
|
|
1668
|
+
writer.line("kotlinx.coroutines.delay(delay)");
|
|
1669
|
+
writer.line("attempt += 1");
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
writer.blank();
|
|
1673
|
+
writer.block(`fun retryDelay(policy: ${options.clientName}.RetryPolicy, attempt: Int, response: Response?): Long?`, () => {
|
|
1674
|
+
writer.line("if (attempt >= policy.maximumAttempts) return null");
|
|
1675
|
+
writer.line("val ceiling = minOf(policy.maximumDelay.inWholeMilliseconds, policy.initialDelay.inWholeMilliseconds shl (attempt - 1))");
|
|
1676
|
+
writer.line("val backoff = (Math.random() * ceiling).toLong()");
|
|
1677
|
+
writer.line("if (response == null) return backoff");
|
|
1678
|
+
writer.line("if (response.code != 429 && response.code != 503) return null");
|
|
1679
|
+
writer.line("val asked = retryAfter(response.header(\"Retry-After\")) ?: return backoff");
|
|
1680
|
+
writer.line("return if (asked <= policy.maximumDelay.inWholeMilliseconds) asked else null");
|
|
1681
|
+
});
|
|
1682
|
+
writer.blank();
|
|
1683
|
+
writer.block("fun retryAfter(header: String?): Long?", () => {
|
|
1684
|
+
writer.line("val value = header?.trim() ?: return null");
|
|
1685
|
+
writer.line("value.toLongOrNull()?.let { return if (it >= 0) it * 1000 else null }");
|
|
1686
|
+
writer.line("val date = try {");
|
|
1687
|
+
writer.line(" java.time.ZonedDateTime.parse(value, java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME)");
|
|
1688
|
+
writer.line("} catch (_: java.time.format.DateTimeParseException) {");
|
|
1689
|
+
writer.line(" return null");
|
|
1690
|
+
writer.line("}");
|
|
1691
|
+
writer.line("return maxOf(0L, date.toInstant().toEpochMilli() - System.currentTimeMillis())");
|
|
1692
|
+
});
|
|
1693
|
+
writer.blank();
|
|
1584
1694
|
writer.block("suspend fun execute(client: OkHttpClient, request: Request): Response", () => {
|
|
1585
1695
|
writer.line("return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->");
|
|
1586
1696
|
writer.indent(() => {
|
|
@@ -1608,20 +1718,34 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1608
1718
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1609
1719
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1610
1720
|
const contextFields = context.requestContextFields;
|
|
1611
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1721
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1612
1722
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1613
1723
|
writer.blank();
|
|
1614
|
-
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null)`, () => {
|
|
1724
|
+
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null)`, () => {
|
|
1615
1725
|
if (contextFields.length > 0) {
|
|
1616
1726
|
writer.blank();
|
|
1617
1727
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
1618
|
-
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${field
|
|
1728
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`));
|
|
1619
1729
|
writer.blank();
|
|
1620
1730
|
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1621
1731
|
for (const field of contextFields) if (field.optional) writer.line(`if (requestContext.${escapeKeyword(field.name)} != null) put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)}!!)`);
|
|
1622
1732
|
else writer.line(`put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)})`);
|
|
1623
1733
|
});
|
|
1624
1734
|
}
|
|
1735
|
+
writer.blank();
|
|
1736
|
+
writer.line("/**");
|
|
1737
|
+
writer.line(" * Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or was answered 429 or 503,");
|
|
1738
|
+
writer.line(" * waiting what Retry-After asks or backing off.");
|
|
1739
|
+
writer.line(" *");
|
|
1740
|
+
writer.line(" * @property maximumAttempts Attempts in all, the first included.");
|
|
1741
|
+
writer.line(" * @property initialDelay What the first backoff waits at most.");
|
|
1742
|
+
writer.line(" * @property maximumDelay What any wait may take.");
|
|
1743
|
+
writer.line(" */");
|
|
1744
|
+
emitConstructorClass(writer, "data class RetryPolicy", [
|
|
1745
|
+
"val maximumAttempts: Int = 3",
|
|
1746
|
+
"val initialDelay: kotlin.time.Duration = 500.milliseconds",
|
|
1747
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1748
|
+
]);
|
|
1625
1749
|
if (usesMultipart) {
|
|
1626
1750
|
writer.blank();
|
|
1627
1751
|
emitConstructorClass(writer, "data class MultipartFile", [
|
|
@@ -1677,7 +1801,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1677
1801
|
}
|
|
1678
1802
|
if (groups.length > 0) for (const group of groups) {
|
|
1679
1803
|
writer.blank();
|
|
1680
|
-
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor)`);
|
|
1804
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry)`);
|
|
1681
1805
|
}
|
|
1682
1806
|
for (const method of flatMethods) {
|
|
1683
1807
|
writer.blank();
|
|
@@ -1685,21 +1809,8 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1685
1809
|
}
|
|
1686
1810
|
});
|
|
1687
1811
|
};
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
*
|
|
1691
|
-
* @param api - What `defineConfig` assembled.
|
|
1692
|
-
* @param config - Override the generated names:
|
|
1693
|
-
* - `namespaceName`: the object wrapping shared types.
|
|
1694
|
-
* - `packageName`: optional package declaration for the generated file.
|
|
1695
|
-
*/
|
|
1696
|
-
const generateKotlinClient = (api, config) => {
|
|
1697
|
-
const { namespaceName, packageName, camelCaseProperties = false, unknownEnumCase = false } = config;
|
|
1698
|
-
const registry = new TypeRegistry(camelCaseProperties, unknownEnumCase);
|
|
1699
|
-
const partition = kotlinGenerator(api, {
|
|
1700
|
-
namespaceName,
|
|
1701
|
-
registry
|
|
1702
|
-
});
|
|
1812
|
+
const renderKotlinClient = (api, partition, registry, config) => {
|
|
1813
|
+
const { namespaceName, packageName, camelCaseProperties = false } = config;
|
|
1703
1814
|
const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
|
|
1704
1815
|
const operationTypeMap = buildOperationTypeMap(allMethods, registry);
|
|
1705
1816
|
const clientName = `${namespaceName}Client`;
|
|
@@ -1735,6 +1846,8 @@ const generateKotlinClient = (api, config) => {
|
|
|
1735
1846
|
writer.line("import okhttp3.*");
|
|
1736
1847
|
writer.line("import okhttp3.MediaType.Companion.toMediaType");
|
|
1737
1848
|
writer.line("import okhttp3.RequestBody.Companion.toRequestBody");
|
|
1849
|
+
writer.line("import kotlin.time.Duration.Companion.milliseconds");
|
|
1850
|
+
writer.line("import kotlin.time.Duration.Companion.seconds");
|
|
1738
1851
|
writer.line("import okhttp3.HttpUrl.Companion.toHttpUrl");
|
|
1739
1852
|
writer.blank();
|
|
1740
1853
|
if (registry.all().some((type) => type.kind === "enum-class")) {
|
|
@@ -1784,12 +1897,42 @@ const generateKotlinClient = (api, config) => {
|
|
|
1784
1897
|
};
|
|
1785
1898
|
writer.block(`object ${namespaceName}`, () => {
|
|
1786
1899
|
emitTypes(writer, topLevelSharedTypes, ownedTypeMap, ownedTypeLookup, registry);
|
|
1900
|
+
for (const brand of registry.allBrands()) {
|
|
1901
|
+
writer.blank();
|
|
1902
|
+
emitBrand(writer, brand);
|
|
1903
|
+
}
|
|
1787
1904
|
});
|
|
1788
1905
|
emitClient(writer, { clientName }, partition, context, typesByOperation, registry);
|
|
1789
1906
|
for (const group of partition.groups) emitSubClientClass(writer, group, clientName, context);
|
|
1790
|
-
emitKizunaObject(writer, {
|
|
1907
|
+
emitKizunaObject(writer, {
|
|
1908
|
+
streaming: usesStreaming,
|
|
1909
|
+
clientName
|
|
1910
|
+
});
|
|
1791
1911
|
for (const warning of registry.warnings()) process.stderr.write(`[kizuna/kotlin] JsonElement fallback at ${warning}\n`);
|
|
1792
1912
|
return writer.toString();
|
|
1793
1913
|
};
|
|
1914
|
+
/**
|
|
1915
|
+
* The Kotlin client's walk: the routes into a partition, then the file.
|
|
1916
|
+
*/
|
|
1917
|
+
const kotlinClientWalk = (api, config) => {
|
|
1918
|
+
const registry = new TypeRegistry(config.camelCaseProperties ?? false, config.unknownEnumCase ?? false);
|
|
1919
|
+
const routes = kotlinRoutesWalk({
|
|
1920
|
+
namespaceName: config.namespaceName,
|
|
1921
|
+
registry
|
|
1922
|
+
});
|
|
1923
|
+
return {
|
|
1924
|
+
processRoute: routes.processRoute,
|
|
1925
|
+
finalize: () => renderKotlinClient(api, routes.finalize(), registry, config)
|
|
1926
|
+
};
|
|
1927
|
+
};
|
|
1928
|
+
/**
|
|
1929
|
+
* Generate a Kotlin API client from a Kizuna api.
|
|
1930
|
+
*
|
|
1931
|
+
* @param api - What `defineConfig` assembled.
|
|
1932
|
+
* @param config - Override the generated names:
|
|
1933
|
+
* - `namespaceName`: the object wrapping shared types.
|
|
1934
|
+
* - `packageName`: optional package declaration for the generated file.
|
|
1935
|
+
*/
|
|
1936
|
+
const generateKotlinClient = (api, config) => walkApi(api, kotlinClientWalk(api, config), { skipHidden: true });
|
|
1794
1937
|
//#endregion
|
|
1795
|
-
export { generateKotlinClient as t };
|
|
1938
|
+
export { kotlinClientWalk as n, generateKotlinClient as t };
|
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_generator = require("./generator-
|
|
2
|
+
const require_generator = require("./generator-Bg4HlRb3.cjs");
|
|
3
|
+
let kizunajs_generator = require("kizunajs/generator");
|
|
4
|
+
let zod = require("zod");
|
|
3
5
|
//#region src/client-target.ts
|
|
6
|
+
const KotlinClientOptionsSchema = zod.z.object({
|
|
7
|
+
/**
|
|
8
|
+
* Name of the generated namespace object.
|
|
9
|
+
*
|
|
10
|
+
* @default 'API'
|
|
11
|
+
*/
|
|
12
|
+
namespace: zod.z.string().default("API"),
|
|
13
|
+
/**
|
|
14
|
+
* Package declaration for the generated file, e.g. `com.example.api`.
|
|
15
|
+
*/
|
|
16
|
+
package: zod.z.string().optional(),
|
|
17
|
+
/**
|
|
18
|
+
* Convert wire field names to camelCase properties, mapping the wire name
|
|
19
|
+
* back via `@SerialName`.
|
|
20
|
+
*
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
camelCaseProperties: zod.z.boolean().optional(),
|
|
24
|
+
/**
|
|
25
|
+
* Give every enum an `unknown` case, so a value the client has not been
|
|
26
|
+
* generated for decodes instead of throwing.
|
|
27
|
+
*
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
unknownEnumCase: zod.z.boolean().optional()
|
|
31
|
+
});
|
|
4
32
|
/**
|
|
5
|
-
* A Kotlin client
|
|
33
|
+
* A Kotlin client for `kizuna.config.ts`.
|
|
6
34
|
*
|
|
7
35
|
* @example
|
|
8
|
-
* kotlinClient({
|
|
36
|
+
* kotlinClient({
|
|
37
|
+
* output: './api/APIClient.kt',
|
|
38
|
+
* package: 'com.example.api',
|
|
39
|
+
* });
|
|
9
40
|
*/
|
|
10
|
-
const kotlinClient = (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
generate: (
|
|
14
|
-
namespaceName: options.namespace
|
|
41
|
+
const kotlinClient = (0, kizunajs_generator.defineClient)({
|
|
42
|
+
target: "kotlin",
|
|
43
|
+
options: KotlinClientOptionsSchema,
|
|
44
|
+
generate: ({ options, api }) => require_generator.kotlinClientWalk(api, {
|
|
45
|
+
namespaceName: options.namespace,
|
|
15
46
|
packageName: options.package,
|
|
16
47
|
camelCaseProperties: options.camelCaseProperties,
|
|
17
48
|
unknownEnumCase: options.unknownEnumCase
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GeneratedFileOptions } from "kizunajs/generator";
|
|
2
|
+
import * as _$kizunajs from "kizunajs";
|
|
3
|
+
import { ApiDefinition } from "kizunajs";
|
|
4
|
+
import { z } from "zod";
|
|
2
5
|
|
|
3
6
|
//#region src/generator.d.ts
|
|
4
7
|
interface KotlinConfig {
|
|
@@ -38,45 +41,30 @@ interface KotlinConfig {
|
|
|
38
41
|
declare const generateKotlinClient: (api: ApiDefinition, config: KotlinConfig) => string;
|
|
39
42
|
//#endregion
|
|
40
43
|
//#region src/client-target.d.ts
|
|
44
|
+
declare const KotlinClientOptionsSchema: z.ZodObject<{
|
|
45
|
+
namespace: z.ZodDefault<z.ZodString>;
|
|
46
|
+
package: z.ZodOptional<z.ZodString>;
|
|
47
|
+
camelCaseProperties: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
unknownEnumCase: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
}, z.core.$strip>;
|
|
41
50
|
/**
|
|
42
51
|
* Where a generated Kotlin client is written, and what it is called there.
|
|
43
52
|
*/
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Path the generated file is written to.
|
|
47
|
-
*/
|
|
48
|
-
output: string;
|
|
49
|
-
/**
|
|
50
|
-
* Name of the generated namespace object.
|
|
51
|
-
*
|
|
52
|
-
* @default 'API'
|
|
53
|
-
*/
|
|
54
|
-
namespace?: string;
|
|
55
|
-
/**
|
|
56
|
-
* Package declaration for the generated file, e.g. `com.example.api`.
|
|
57
|
-
*/
|
|
58
|
-
package?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Convert wire field names to camelCase properties, mapping the wire name
|
|
61
|
-
* back via `@SerialName`.
|
|
62
|
-
*
|
|
63
|
-
* @default false
|
|
64
|
-
*/
|
|
65
|
-
camelCaseProperties?: boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Give every enum an `unknown` case, so a value the client has not been
|
|
68
|
-
* generated for decodes instead of throwing.
|
|
69
|
-
*
|
|
70
|
-
* @default false
|
|
71
|
-
*/
|
|
72
|
-
unknownEnumCase?: boolean;
|
|
73
|
-
}
|
|
53
|
+
type KotlinClientOptions = z.input<typeof KotlinClientOptionsSchema> & GeneratedFileOptions;
|
|
74
54
|
/**
|
|
75
|
-
* A Kotlin client
|
|
55
|
+
* A Kotlin client for `kizuna.config.ts`.
|
|
76
56
|
*
|
|
77
57
|
* @example
|
|
78
|
-
* kotlinClient({
|
|
58
|
+
* kotlinClient({
|
|
59
|
+
* output: './api/APIClient.kt',
|
|
60
|
+
* package: 'com.example.api',
|
|
61
|
+
* });
|
|
79
62
|
*/
|
|
80
|
-
declare const kotlinClient: (options:
|
|
63
|
+
declare const kotlinClient: (options: {
|
|
64
|
+
namespace?: string | undefined;
|
|
65
|
+
package?: string | undefined;
|
|
66
|
+
camelCaseProperties?: boolean | undefined;
|
|
67
|
+
unknownEnumCase?: boolean | undefined;
|
|
68
|
+
} & GeneratedFileOptions) => _$kizunajs.GeneratedFile;
|
|
81
69
|
//#endregion
|
|
82
70
|
export { type KotlinClientOptions, type KotlinConfig, generateKotlinClient, kotlinClient };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GeneratedFileOptions } from "kizunajs/generator";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as _$kizunajs from "kizunajs";
|
|
4
|
+
import { ApiDefinition } from "kizunajs";
|
|
2
5
|
|
|
3
6
|
//#region src/generator.d.ts
|
|
4
7
|
interface KotlinConfig {
|
|
@@ -38,45 +41,30 @@ interface KotlinConfig {
|
|
|
38
41
|
declare const generateKotlinClient: (api: ApiDefinition, config: KotlinConfig) => string;
|
|
39
42
|
//#endregion
|
|
40
43
|
//#region src/client-target.d.ts
|
|
44
|
+
declare const KotlinClientOptionsSchema: z.ZodObject<{
|
|
45
|
+
namespace: z.ZodDefault<z.ZodString>;
|
|
46
|
+
package: z.ZodOptional<z.ZodString>;
|
|
47
|
+
camelCaseProperties: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
unknownEnumCase: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
}, z.core.$strip>;
|
|
41
50
|
/**
|
|
42
51
|
* Where a generated Kotlin client is written, and what it is called there.
|
|
43
52
|
*/
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Path the generated file is written to.
|
|
47
|
-
*/
|
|
48
|
-
output: string;
|
|
49
|
-
/**
|
|
50
|
-
* Name of the generated namespace object.
|
|
51
|
-
*
|
|
52
|
-
* @default 'API'
|
|
53
|
-
*/
|
|
54
|
-
namespace?: string;
|
|
55
|
-
/**
|
|
56
|
-
* Package declaration for the generated file, e.g. `com.example.api`.
|
|
57
|
-
*/
|
|
58
|
-
package?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Convert wire field names to camelCase properties, mapping the wire name
|
|
61
|
-
* back via `@SerialName`.
|
|
62
|
-
*
|
|
63
|
-
* @default false
|
|
64
|
-
*/
|
|
65
|
-
camelCaseProperties?: boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Give every enum an `unknown` case, so a value the client has not been
|
|
68
|
-
* generated for decodes instead of throwing.
|
|
69
|
-
*
|
|
70
|
-
* @default false
|
|
71
|
-
*/
|
|
72
|
-
unknownEnumCase?: boolean;
|
|
73
|
-
}
|
|
53
|
+
type KotlinClientOptions = z.input<typeof KotlinClientOptionsSchema> & GeneratedFileOptions;
|
|
74
54
|
/**
|
|
75
|
-
* A Kotlin client
|
|
55
|
+
* A Kotlin client for `kizuna.config.ts`.
|
|
76
56
|
*
|
|
77
57
|
* @example
|
|
78
|
-
* kotlinClient({
|
|
58
|
+
* kotlinClient({
|
|
59
|
+
* output: './api/APIClient.kt',
|
|
60
|
+
* package: 'com.example.api',
|
|
61
|
+
* });
|
|
79
62
|
*/
|
|
80
|
-
declare const kotlinClient: (options:
|
|
63
|
+
declare const kotlinClient: (options: {
|
|
64
|
+
namespace?: string | undefined;
|
|
65
|
+
package?: string | undefined;
|
|
66
|
+
camelCaseProperties?: boolean | undefined;
|
|
67
|
+
unknownEnumCase?: boolean | undefined;
|
|
68
|
+
} & GeneratedFileOptions) => _$kizunajs.GeneratedFile;
|
|
81
69
|
//#endregion
|
|
82
70
|
export { type KotlinClientOptions, type KotlinConfig, generateKotlinClient, kotlinClient };
|
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
|
-
import { t as generateKotlinClient } from "./generator-
|
|
2
|
-
|
|
1
|
+
import { n as kotlinClientWalk, t as generateKotlinClient } from "./generator-D4GvH1-a.mjs";
|
|
2
|
+
import { defineClient } from "kizunajs/generator";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
/**
|
|
4
|
-
* A Kotlin client
|
|
5
|
+
* A Kotlin client for `kizuna.config.ts`.
|
|
5
6
|
*
|
|
6
7
|
* @example
|
|
7
|
-
* kotlinClient({
|
|
8
|
+
* kotlinClient({
|
|
9
|
+
* output: './api/APIClient.kt',
|
|
10
|
+
* package: 'com.example.api',
|
|
11
|
+
* });
|
|
8
12
|
*/
|
|
9
|
-
const kotlinClient = (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const kotlinClient = defineClient({
|
|
14
|
+
target: "kotlin",
|
|
15
|
+
options: z.object({
|
|
16
|
+
/**
|
|
17
|
+
* Name of the generated namespace object.
|
|
18
|
+
*
|
|
19
|
+
* @default 'API'
|
|
20
|
+
*/
|
|
21
|
+
namespace: z.string().default("API"),
|
|
22
|
+
/**
|
|
23
|
+
* Package declaration for the generated file, e.g. `com.example.api`.
|
|
24
|
+
*/
|
|
25
|
+
package: z.string().optional(),
|
|
26
|
+
/**
|
|
27
|
+
* Convert wire field names to camelCase properties, mapping the wire name
|
|
28
|
+
* back via `@SerialName`.
|
|
29
|
+
*
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
camelCaseProperties: z.boolean().optional(),
|
|
33
|
+
/**
|
|
34
|
+
* Give every enum an `unknown` case, so a value the client has not been
|
|
35
|
+
* generated for decodes instead of throwing.
|
|
36
|
+
*
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
unknownEnumCase: z.boolean().optional()
|
|
40
|
+
}),
|
|
41
|
+
generate: ({ options, api }) => kotlinClientWalk(api, {
|
|
42
|
+
namespaceName: options.namespace,
|
|
14
43
|
packageName: options.package,
|
|
15
44
|
camelCaseProperties: options.camelCaseProperties,
|
|
16
45
|
unknownEnumCase: options.unknownEnumCase
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/kotlin",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.20",
|
|
4
4
|
"description": "Kotlin client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -60,18 +60,18 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@kizunajs/cli": "2.0.0-beta.
|
|
63
|
+
"@kizunajs/cli": "2.0.0-beta.20"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
66
|
+
"kizunajs": "2.0.0-beta.20",
|
|
67
|
+
"zod": "^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
+
"kizunajs": "2.0.0-beta.20",
|
|
70
71
|
"@types/node": "^22.9.0",
|
|
71
72
|
"tsdown": "^0.21.0",
|
|
72
73
|
"typescript": "^6.0.3",
|
|
73
|
-
"zod": "^4.0.0"
|
|
74
|
-
"kizunajs": "2.0.0-beta.2"
|
|
74
|
+
"zod": "^4.0.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "tsdown src/index.ts src/cli.ts --format esm,cjs --dts --clean --external kizunajs --external @kizunajs/cli --external zod",
|