@kizunajs/kotlin 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{generator-BvNpSarY.mjs → generator-DHi8XSWb.mjs} +306 -78
- package/dist/{generator-s2pn7vqP.cjs → generator-Dft2DL-3.cjs} +310 -76
- 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
|
@@ -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})`);
|
|
@@ -1407,6 +1476,35 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1407
1476
|
}
|
|
1408
1477
|
writer.line(`else -> throw ${failureRef}.Unexpected(statusCode = statusCode, data = data)`);
|
|
1409
1478
|
};
|
|
1479
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
1480
|
+
"GET",
|
|
1481
|
+
"HEAD",
|
|
1482
|
+
"OPTIONS",
|
|
1483
|
+
"PUT",
|
|
1484
|
+
"DELETE"
|
|
1485
|
+
]);
|
|
1486
|
+
/**
|
|
1487
|
+
* Writes the reconnection an event stream reopens through and returns the argument handing it to `Kizuna.events`, or
|
|
1488
|
+
* nothing for a method a client may not resend.
|
|
1489
|
+
*/
|
|
1490
|
+
const emitReopen = (writer, method, operationRef, status) => {
|
|
1491
|
+
if (!IDEMPOTENT_METHODS.has(method.method)) return "";
|
|
1492
|
+
closure(writer, "val reconnection = reconnect?.let { policy ->", () => {
|
|
1493
|
+
closure(writer, "Kizuna.Reconnection(policy) { lastEventId ->", () => {
|
|
1494
|
+
writer.line("val again = requestBuilder.build().newBuilder()");
|
|
1495
|
+
writer.line("if (lastEventId.isNotEmpty()) again.header(\"Last-Event-ID\", lastEventId)");
|
|
1496
|
+
writer.line("requestInterceptor?.invoke(again)");
|
|
1497
|
+
writer.line("val reopened = Kizuna.send(client, again.build(), retry)");
|
|
1498
|
+
writer.line("val eventStream = reopened.header(\"Content-Type\")?.lowercase()?.startsWith(\"text/event-stream\") == true");
|
|
1499
|
+
writer.block("when", () => {
|
|
1500
|
+
writer.line("reopened.code == 204 -> { reopened.close(); null }");
|
|
1501
|
+
writer.line(`reopened.code == ${status} && eventStream -> reopened`);
|
|
1502
|
+
writer.line(`else -> { reopened.close(); throw ${operationRef}.Failure.Unexpected(statusCode = reopened.code, data = ByteArray(0)) }`);
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
return ", reconnection";
|
|
1507
|
+
};
|
|
1410
1508
|
const emitStreamMethodTail = (writer, method, context) => {
|
|
1411
1509
|
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1412
1510
|
const stream = method.stream;
|
|
@@ -1419,7 +1517,7 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1419
1517
|
else if (stream.events) {
|
|
1420
1518
|
const events = stream.events;
|
|
1421
1519
|
const elementType = streamElementType(method, context, "client");
|
|
1422
|
-
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse) { event ->`, () => {
|
|
1520
|
+
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse${emitReopen(writer, method, operationRef, stream.status)}) { event ->`, () => {
|
|
1423
1521
|
writer.block("when (event.event)", () => {
|
|
1424
1522
|
for (const event of events) {
|
|
1425
1523
|
const payloadType = resolveType(event.type, method.operationName, context);
|
|
@@ -1430,7 +1528,8 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1430
1528
|
});
|
|
1431
1529
|
} else {
|
|
1432
1530
|
const elementType = streamElementType(method, context, "client");
|
|
1433
|
-
|
|
1531
|
+
const reconnection = emitReopen(writer, method, operationRef, stream.status);
|
|
1532
|
+
writer.line(`val body = Kizuna.events<${elementType}>(httpResponse${reconnection}) { event -> json.decodeFromString<${elementType}>(event.data) }`);
|
|
1434
1533
|
}
|
|
1435
1534
|
if (hasHeaders) {
|
|
1436
1535
|
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
@@ -1476,7 +1575,7 @@ const emitSubClientMethod = (writer, method, context) => {
|
|
|
1476
1575
|
};
|
|
1477
1576
|
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1478
1577
|
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)?)`, () => {
|
|
1578
|
+
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?, private val reconnect: ${clientName}.ReconnectPolicy?)`, () => {
|
|
1480
1579
|
for (const method of group.methods) {
|
|
1481
1580
|
writer.blank();
|
|
1482
1581
|
emitSubClientMethod(writer, method, context);
|
|
@@ -1488,10 +1587,12 @@ const closure = (writer, opener, body) => {
|
|
|
1488
1587
|
writer.indent(body);
|
|
1489
1588
|
writer.line("}");
|
|
1490
1589
|
};
|
|
1491
|
-
const emitKizunaStreaming = (writer) => {
|
|
1590
|
+
const emitKizunaStreaming = (writer, clientName) => {
|
|
1492
1591
|
writer.blank();
|
|
1493
1592
|
writer.line("data class ServerSentEvent(val event: String?, val data: String, val id: String?, val retry: Int?)");
|
|
1494
1593
|
writer.blank();
|
|
1594
|
+
writer.line(`class Reconnection(val policy: ${clientName}.ReconnectPolicy, val reopen: suspend (String) -> Response?)`);
|
|
1595
|
+
writer.blank();
|
|
1495
1596
|
writer.block("private suspend fun readLine(source: okio.BufferedSource): String?", () => {
|
|
1496
1597
|
writer.line("return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { source.readUtf8Line() }");
|
|
1497
1598
|
});
|
|
@@ -1518,44 +1619,83 @@ const emitKizunaStreaming = (writer) => {
|
|
|
1518
1619
|
});
|
|
1519
1620
|
});
|
|
1520
1621
|
writer.blank();
|
|
1521
|
-
writer.
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
writer.line("val
|
|
1526
|
-
writer.
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
writer.
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1622
|
+
writer.line("private val LINE_ENDINGS = okio.ByteString.of('\\r'.code.toByte(), '\\n'.code.toByte())");
|
|
1623
|
+
writer.blank();
|
|
1624
|
+
writer.block("private suspend fun readEventLine(source: okio.BufferedSource): String?", () => {
|
|
1625
|
+
closure(writer, "return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {", () => {
|
|
1626
|
+
writer.line("val end = source.indexOfElement(LINE_ENDINGS)");
|
|
1627
|
+
writer.block("if (end == -1L)", () => {
|
|
1628
|
+
writer.line("if (source.exhausted()) null else source.readUtf8()");
|
|
1629
|
+
});
|
|
1630
|
+
writer.block("else", () => {
|
|
1631
|
+
writer.line("val line = source.readUtf8(end)");
|
|
1632
|
+
writer.line("val carriageReturn = source.readByte() == '\\r'.code.toByte()");
|
|
1633
|
+
writer.line("if (carriageReturn && source.request(1) && source.buffer[0] == '\\n'.code.toByte()) source.skip(1)");
|
|
1634
|
+
writer.line("line");
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
writer.blank();
|
|
1639
|
+
writer.block("fun serverSentEvents(response: Response, reconnect: Reconnection?): Flow<ServerSentEvent> = flow", () => {
|
|
1640
|
+
writer.line("var lastEventId = \"\"");
|
|
1641
|
+
writer.line("var reconnectionTime = reconnect?.policy?.reconnectionTime ?: 3.seconds");
|
|
1642
|
+
writer.line("var current: Response? = response");
|
|
1643
|
+
writer.line("var attempts = 0");
|
|
1644
|
+
writer.line("var failure: Throwable? = null");
|
|
1645
|
+
writer.block("while (true)", () => {
|
|
1646
|
+
closure(writer, "current?.use { open ->", () => {
|
|
1647
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1648
|
+
writer.line("var eventType = \"\"");
|
|
1649
|
+
writer.line("val dataLines = mutableListOf<String>()");
|
|
1650
|
+
writer.line("var pendingId = lastEventId");
|
|
1651
|
+
writer.line("var retry: Int? = null");
|
|
1652
|
+
writer.line("var firstLine = true");
|
|
1653
|
+
writer.block("while (true)", () => {
|
|
1654
|
+
writer.line("val received = try { readEventLine(source) } catch (error: java.io.IOException) { failure = error; null } ?: break");
|
|
1655
|
+
writer.line("val line = if (firstLine) received.removePrefix(\"\\uFEFF\") else received");
|
|
1656
|
+
writer.line("firstLine = false");
|
|
1657
|
+
writer.block("if (line.isEmpty())", () => {
|
|
1658
|
+
writer.line("lastEventId = pendingId");
|
|
1659
|
+
writer.block("if (dataLines.isNotEmpty())", () => {
|
|
1660
|
+
writer.line("emit(ServerSentEvent(eventType.ifEmpty { null }, dataLines.joinToString(\"\\n\"), lastEventId.ifEmpty { null }, retry))");
|
|
1661
|
+
writer.line("attempts = 0");
|
|
1662
|
+
writer.line("failure = null");
|
|
1663
|
+
writer.line("dataLines.clear()");
|
|
1664
|
+
writer.line("retry = null");
|
|
1665
|
+
});
|
|
1666
|
+
writer.line("eventType = \"\"");
|
|
1667
|
+
writer.line("continue");
|
|
1668
|
+
});
|
|
1669
|
+
writer.line("if (line.startsWith(\":\")) continue");
|
|
1670
|
+
writer.line("val separator = line.indexOf(':')");
|
|
1671
|
+
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1672
|
+
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1673
|
+
writer.block("when (field)", () => {
|
|
1674
|
+
writer.line("\"event\" -> eventType = value");
|
|
1675
|
+
writer.line("\"data\" -> dataLines.add(value)");
|
|
1676
|
+
writer.line("\"id\" -> if (!value.contains('\\u0000')) pendingId = value");
|
|
1677
|
+
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { character -> character in '0'..'9' }) value.toIntOrNull()?.let { milliseconds -> retry = milliseconds; reconnectionTime = minOf(milliseconds.milliseconds, 3_600_000.milliseconds) }");
|
|
1535
1678
|
});
|
|
1536
|
-
writer.line("eventType = \"\"");
|
|
1537
|
-
writer.line("continue");
|
|
1538
|
-
});
|
|
1539
|
-
writer.line("if (line.startsWith(\":\")) continue");
|
|
1540
|
-
writer.line("val separator = line.indexOf(':')");
|
|
1541
|
-
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1542
|
-
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1543
|
-
writer.block("when (field)", () => {
|
|
1544
|
-
writer.line("\"event\" -> eventType = value");
|
|
1545
|
-
writer.line("\"data\" -> dataLines.add(value)");
|
|
1546
|
-
writer.line("\"id\" -> if (!value.contains('\\u0000')) lastEventId = value");
|
|
1547
|
-
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { it.isDigit() }) retry = value.toIntOrNull()");
|
|
1548
1679
|
});
|
|
1549
1680
|
});
|
|
1681
|
+
writer.block("if (reconnect == null || attempts >= reconnect.policy.maximumAttempts)", () => {
|
|
1682
|
+
writer.line("failure?.let { throw it }");
|
|
1683
|
+
writer.line("return@flow");
|
|
1684
|
+
});
|
|
1685
|
+
writer.line("attempts += 1");
|
|
1686
|
+
writer.line("val ceiling = minOf(reconnect.policy.maximumDelay, reconnectionTime * Math.pow(2.0, (attempts - 1).toDouble()))");
|
|
1687
|
+
writer.line("val extra = (ceiling - reconnectionTime).coerceAtLeast(kotlin.time.Duration.ZERO)");
|
|
1688
|
+
writer.line("kotlinx.coroutines.delay(reconnectionTime + extra * Math.random())");
|
|
1689
|
+
writer.line("current = try { reconnect.reopen(lastEventId) ?: return@flow } catch (error: java.io.IOException) { failure = error; null }");
|
|
1550
1690
|
});
|
|
1551
1691
|
});
|
|
1552
1692
|
writer.blank();
|
|
1553
|
-
writer.line("fun <Event : Any> events(response: Response, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response).mapNotNull(decode)");
|
|
1693
|
+
writer.line("fun <Event : Any> events(response: Response, reconnect: Reconnection? = null, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response, reconnect).mapNotNull(decode)");
|
|
1554
1694
|
};
|
|
1555
1695
|
const emitKizunaObject = (writer, options) => {
|
|
1556
1696
|
writer.blank();
|
|
1557
1697
|
writer.block("private object Kizuna", () => {
|
|
1558
|
-
if (options.streaming) emitKizunaStreaming(writer);
|
|
1698
|
+
if (options.streaming) emitKizunaStreaming(writer, options.clientName);
|
|
1559
1699
|
writer.block("fun resolveUrl(baseUrl: String, path: String): HttpUrl.Builder", () => {
|
|
1560
1700
|
writer.line("val base = baseUrl.toHttpUrl()");
|
|
1561
1701
|
writer.line("val trimmedPath = path.trimStart('/')");
|
|
@@ -1581,6 +1721,47 @@ const emitKizunaObject = (writer, options) => {
|
|
|
1581
1721
|
writer.line("}");
|
|
1582
1722
|
});
|
|
1583
1723
|
writer.blank();
|
|
1724
|
+
writer.block(`suspend fun send(client: OkHttpClient, request: Request, retry: ${options.clientName}.RetryPolicy?): Response`, () => {
|
|
1725
|
+
writer.line("val idempotent = request.method in setOf(\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\")");
|
|
1726
|
+
writer.line("val policy = retry?.takeIf { idempotent && request.body?.isOneShot() != true }");
|
|
1727
|
+
writer.line("var attempt = 1");
|
|
1728
|
+
writer.block("while (true)", () => {
|
|
1729
|
+
writer.line("val response = try {");
|
|
1730
|
+
writer.line(" execute(client, request)");
|
|
1731
|
+
writer.line("} catch (error: java.io.IOException) {");
|
|
1732
|
+
writer.line(" val delay = policy?.let { retryDelay(it, attempt, null) } ?: throw error");
|
|
1733
|
+
writer.line(" kotlinx.coroutines.delay(delay)");
|
|
1734
|
+
writer.line(" attempt += 1");
|
|
1735
|
+
writer.line(" continue");
|
|
1736
|
+
writer.line("}");
|
|
1737
|
+
writer.line("val delay = policy?.let { retryDelay(it, attempt, response) } ?: return response");
|
|
1738
|
+
writer.line("response.close()");
|
|
1739
|
+
writer.line("kotlinx.coroutines.delay(delay)");
|
|
1740
|
+
writer.line("attempt += 1");
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
writer.blank();
|
|
1744
|
+
writer.block(`fun retryDelay(policy: ${options.clientName}.RetryPolicy, attempt: Int, response: Response?): Long?`, () => {
|
|
1745
|
+
writer.line("if (attempt >= policy.maximumAttempts) return null");
|
|
1746
|
+
writer.line("val ceiling = minOf(policy.maximumDelay.inWholeMilliseconds, policy.initialDelay.inWholeMilliseconds shl (attempt - 1))");
|
|
1747
|
+
writer.line("val backoff = (Math.random() * ceiling).toLong()");
|
|
1748
|
+
writer.line("if (response == null) return backoff");
|
|
1749
|
+
writer.line("if (response.code != 429 && response.code != 503) return null");
|
|
1750
|
+
writer.line("val asked = retryAfter(response.header(\"Retry-After\")) ?: return backoff");
|
|
1751
|
+
writer.line("return if (asked <= policy.maximumDelay.inWholeMilliseconds) asked else null");
|
|
1752
|
+
});
|
|
1753
|
+
writer.blank();
|
|
1754
|
+
writer.block("fun retryAfter(header: String?): Long?", () => {
|
|
1755
|
+
writer.line("val value = header?.trim() ?: return null");
|
|
1756
|
+
writer.line("value.toLongOrNull()?.let { return if (it >= 0) it * 1000 else null }");
|
|
1757
|
+
writer.line("val date = try {");
|
|
1758
|
+
writer.line(" java.time.ZonedDateTime.parse(value, java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME)");
|
|
1759
|
+
writer.line("} catch (_: java.time.format.DateTimeParseException) {");
|
|
1760
|
+
writer.line(" return null");
|
|
1761
|
+
writer.line("}");
|
|
1762
|
+
writer.line("return maxOf(0L, date.toInstant().toEpochMilli() - System.currentTimeMillis())");
|
|
1763
|
+
});
|
|
1764
|
+
writer.blank();
|
|
1584
1765
|
writer.block("suspend fun execute(client: OkHttpClient, request: Request): Response", () => {
|
|
1585
1766
|
writer.line("return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->");
|
|
1586
1767
|
writer.indent(() => {
|
|
@@ -1608,20 +1789,48 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1608
1789
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1609
1790
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1610
1791
|
const contextFields = context.requestContextFields;
|
|
1611
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1792
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1612
1793
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1613
1794
|
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)`, () => {
|
|
1795
|
+
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, private val reconnect: ReconnectPolicy? = null)`, () => {
|
|
1615
1796
|
if (contextFields.length > 0) {
|
|
1616
1797
|
writer.blank();
|
|
1617
1798
|
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
|
|
1799
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`));
|
|
1619
1800
|
writer.blank();
|
|
1620
1801
|
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1621
1802
|
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
1803
|
else writer.line(`put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)})`);
|
|
1623
1804
|
});
|
|
1624
1805
|
}
|
|
1806
|
+
writer.blank();
|
|
1807
|
+
writer.line("/**");
|
|
1808
|
+
writer.line(" * Retries a GET, HEAD, OPTIONS, PUT or DELETE that failed on the network or was answered 429 or 503,");
|
|
1809
|
+
writer.line(" * waiting what Retry-After asks or backing off.");
|
|
1810
|
+
writer.line(" *");
|
|
1811
|
+
writer.line(" * @property maximumAttempts Attempts in all, the first included.");
|
|
1812
|
+
writer.line(" * @property initialDelay What the first backoff waits at most.");
|
|
1813
|
+
writer.line(" * @property maximumDelay What any wait may take.");
|
|
1814
|
+
writer.line(" */");
|
|
1815
|
+
emitConstructorClass(writer, "data class RetryPolicy", [
|
|
1816
|
+
"val maximumAttempts: Int = 3",
|
|
1817
|
+
"val initialDelay: kotlin.time.Duration = 500.milliseconds",
|
|
1818
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1819
|
+
]);
|
|
1820
|
+
writer.blank();
|
|
1821
|
+
writer.line("/**");
|
|
1822
|
+
writer.line(" * Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops,");
|
|
1823
|
+
writer.line(" * sending the id of the last event as Last-Event-ID.");
|
|
1824
|
+
writer.line(" *");
|
|
1825
|
+
writer.line(" * @property maximumAttempts Reconnections in a row before the stream fails. An event received resets the count.");
|
|
1826
|
+
writer.line(" * @property reconnectionTime What to wait before reconnecting, until the server sends a retry field.");
|
|
1827
|
+
writer.line(" * @property maximumDelay What the wait may grow to after reconnections in a row that fail.");
|
|
1828
|
+
writer.line(" */");
|
|
1829
|
+
emitConstructorClass(writer, "data class ReconnectPolicy", [
|
|
1830
|
+
"val maximumAttempts: Int = 5",
|
|
1831
|
+
"val reconnectionTime: kotlin.time.Duration = 3.seconds",
|
|
1832
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1833
|
+
]);
|
|
1625
1834
|
if (usesMultipart) {
|
|
1626
1835
|
writer.blank();
|
|
1627
1836
|
emitConstructorClass(writer, "data class MultipartFile", [
|
|
@@ -1677,7 +1886,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1677
1886
|
}
|
|
1678
1887
|
if (groups.length > 0) for (const group of groups) {
|
|
1679
1888
|
writer.blank();
|
|
1680
|
-
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor)`);
|
|
1889
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry, reconnect)`);
|
|
1681
1890
|
}
|
|
1682
1891
|
for (const method of flatMethods) {
|
|
1683
1892
|
writer.blank();
|
|
@@ -1685,21 +1894,8 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1685
1894
|
}
|
|
1686
1895
|
});
|
|
1687
1896
|
};
|
|
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
|
-
});
|
|
1897
|
+
const renderKotlinClient = (api, partition, registry, config) => {
|
|
1898
|
+
const { namespaceName, packageName, camelCaseProperties = false } = config;
|
|
1703
1899
|
const allMethods = [...partition.flatMethods, ...partition.groups.flatMap((group) => group.methods)];
|
|
1704
1900
|
const operationTypeMap = buildOperationTypeMap(allMethods, registry);
|
|
1705
1901
|
const clientName = `${namespaceName}Client`;
|
|
@@ -1735,6 +1931,8 @@ const generateKotlinClient = (api, config) => {
|
|
|
1735
1931
|
writer.line("import okhttp3.*");
|
|
1736
1932
|
writer.line("import okhttp3.MediaType.Companion.toMediaType");
|
|
1737
1933
|
writer.line("import okhttp3.RequestBody.Companion.toRequestBody");
|
|
1934
|
+
writer.line("import kotlin.time.Duration.Companion.milliseconds");
|
|
1935
|
+
writer.line("import kotlin.time.Duration.Companion.seconds");
|
|
1738
1936
|
writer.line("import okhttp3.HttpUrl.Companion.toHttpUrl");
|
|
1739
1937
|
writer.blank();
|
|
1740
1938
|
if (registry.all().some((type) => type.kind === "enum-class")) {
|
|
@@ -1784,12 +1982,42 @@ const generateKotlinClient = (api, config) => {
|
|
|
1784
1982
|
};
|
|
1785
1983
|
writer.block(`object ${namespaceName}`, () => {
|
|
1786
1984
|
emitTypes(writer, topLevelSharedTypes, ownedTypeMap, ownedTypeLookup, registry);
|
|
1985
|
+
for (const brand of registry.allBrands()) {
|
|
1986
|
+
writer.blank();
|
|
1987
|
+
emitBrand(writer, brand);
|
|
1988
|
+
}
|
|
1787
1989
|
});
|
|
1788
1990
|
emitClient(writer, { clientName }, partition, context, typesByOperation, registry);
|
|
1789
1991
|
for (const group of partition.groups) emitSubClientClass(writer, group, clientName, context);
|
|
1790
|
-
emitKizunaObject(writer, {
|
|
1992
|
+
emitKizunaObject(writer, {
|
|
1993
|
+
streaming: usesStreaming,
|
|
1994
|
+
clientName
|
|
1995
|
+
});
|
|
1791
1996
|
for (const warning of registry.warnings()) process.stderr.write(`[kizuna/kotlin] JsonElement fallback at ${warning}\n`);
|
|
1792
1997
|
return writer.toString();
|
|
1793
1998
|
};
|
|
1999
|
+
/**
|
|
2000
|
+
* The Kotlin client's walk: the routes into a partition, then the file.
|
|
2001
|
+
*/
|
|
2002
|
+
const kotlinClientWalk = (api, config) => {
|
|
2003
|
+
const registry = new TypeRegistry(config.camelCaseProperties ?? false, config.unknownEnumCase ?? false);
|
|
2004
|
+
const routes = kotlinRoutesWalk({
|
|
2005
|
+
namespaceName: config.namespaceName,
|
|
2006
|
+
registry
|
|
2007
|
+
});
|
|
2008
|
+
return {
|
|
2009
|
+
processRoute: routes.processRoute,
|
|
2010
|
+
finalize: () => renderKotlinClient(api, routes.finalize(), registry, config)
|
|
2011
|
+
};
|
|
2012
|
+
};
|
|
2013
|
+
/**
|
|
2014
|
+
* Generate a Kotlin API client from a Kizuna api.
|
|
2015
|
+
*
|
|
2016
|
+
* @param api - What `defineConfig` assembled.
|
|
2017
|
+
* @param config - Override the generated names:
|
|
2018
|
+
* - `namespaceName`: the object wrapping shared types.
|
|
2019
|
+
* - `packageName`: optional package declaration for the generated file.
|
|
2020
|
+
*/
|
|
2021
|
+
const generateKotlinClient = (api, config) => walkApi(api, kotlinClientWalk(api, config), { skipHidden: true });
|
|
1794
2022
|
//#endregion
|
|
1795
|
-
export { generateKotlinClient as t };
|
|
2023
|
+
export { kotlinClientWalk as n, generateKotlinClient as t };
|