@kizunajs/kotlin 2.0.0-beta.16 → 2.0.0-beta.18
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/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-D2GCSZwz.mjs";
|
|
3
3
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -58,6 +58,7 @@ var TypeRegistry = class {
|
|
|
58
58
|
sealedVariantPaths = /* @__PURE__ */ new Map();
|
|
59
59
|
brands = /* @__PURE__ */ new Map();
|
|
60
60
|
usesJsonElement = false;
|
|
61
|
+
usesNullish = false;
|
|
61
62
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
62
63
|
this.camelCaseProperties = camelCaseProperties;
|
|
63
64
|
this.unknownEnumCase = unknownEnumCase;
|
|
@@ -127,11 +128,13 @@ const propertyName = (key, camelCase) => {
|
|
|
127
128
|
if (!camel) return "field";
|
|
128
129
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
129
130
|
};
|
|
130
|
-
const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
|
|
131
|
+
const objectFields = (schema, registry, hint, nullishTriState = true, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
|
|
131
132
|
const fields = [];
|
|
132
133
|
const seen = /* @__PURE__ */ new Map();
|
|
133
134
|
for (const [key, value] of Object.entries(shape)) {
|
|
134
135
|
const result = mapType(value, registry, `${hint}${(0, kizunajs_generator.toPascalCase)(key)}`);
|
|
136
|
+
const nullish = nullishTriState && result.nullish === true;
|
|
137
|
+
if (nullish) registry.usesNullish = true;
|
|
135
138
|
const deprecation = (0, kizunajs_generator.readDeprecation)(value);
|
|
136
139
|
const kotlinName = propertyName(key, registry.camelCaseProperties);
|
|
137
140
|
const previousWireName = seen.get(kotlinName);
|
|
@@ -140,8 +143,10 @@ const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.rea
|
|
|
140
143
|
fields.push({
|
|
141
144
|
name: kotlinName,
|
|
142
145
|
wireName: key,
|
|
143
|
-
type: result.expression,
|
|
144
|
-
optional: result.optional,
|
|
146
|
+
type: nullish ? `Nullish<${result.expression.replace(/\?$/, "")}>` : result.expression,
|
|
147
|
+
optional: result.optional && !nullish,
|
|
148
|
+
requiredNullable: result.requiredNullable,
|
|
149
|
+
nullish,
|
|
145
150
|
description: (0, kizunajs_generator.readMetaDescription)(value),
|
|
146
151
|
isFile: result.isFile,
|
|
147
152
|
deprecated: deprecation !== void 0,
|
|
@@ -264,6 +269,8 @@ const mapUnbrandedType = (schema, registry, hint) => {
|
|
|
264
269
|
return {
|
|
265
270
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
266
271
|
optional: true,
|
|
272
|
+
requiredNullable: nullable && !optional,
|
|
273
|
+
nullish: nullable && optional,
|
|
267
274
|
isFile: innerResult.isFile
|
|
268
275
|
};
|
|
269
276
|
}
|
|
@@ -445,9 +452,13 @@ const sanitizeCaseName = (literal) => {
|
|
|
445
452
|
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
446
453
|
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
447
454
|
};
|
|
448
|
-
|
|
449
|
-
|
|
455
|
+
/**
|
|
456
|
+
* A JSON body keeps `.nullish()` as `Nullish`. A query, path, header or form field has no null, so it takes an optional.
|
|
457
|
+
*/
|
|
458
|
+
const collectObjectFields = (schema, registry, hint, json = false) => {
|
|
459
|
+
return objectFields(schema, registry, hint, json);
|
|
450
460
|
};
|
|
461
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true || field.nullish === true;
|
|
451
462
|
const objectFieldCount = (schema) => {
|
|
452
463
|
return Object.keys((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
|
|
453
464
|
};
|
|
@@ -512,7 +523,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
512
523
|
else bodyDescriptor = {
|
|
513
524
|
kind: "json-flat",
|
|
514
525
|
structName,
|
|
515
|
-
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
526
|
+
flattened: collectObjectFields(route.body, registry, bodyHint, true),
|
|
516
527
|
multipartFields: []
|
|
517
528
|
};
|
|
518
529
|
}
|
|
@@ -678,6 +689,49 @@ const KOTLIN_KEYWORDS = new Set([
|
|
|
678
689
|
const escapeKeyword = (name) => {
|
|
679
690
|
return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
680
691
|
};
|
|
692
|
+
const NULLISH_TYPE = /^Nullish<(.+)>$/;
|
|
693
|
+
const emitNullish = (writer) => {
|
|
694
|
+
writer.line("/** A `.nullish()` field: left out, sent as `null`, or sent with a value. */");
|
|
695
|
+
writer.line("@Serializable(with = NullishSerializer::class)");
|
|
696
|
+
writer.block("sealed interface Nullish<out T>", () => {
|
|
697
|
+
writer.line("val value: T?");
|
|
698
|
+
writer.blank();
|
|
699
|
+
writer.block("data object Absent : Nullish<Nothing>", () => {
|
|
700
|
+
writer.line("override val value: Nothing? get() = null");
|
|
701
|
+
});
|
|
702
|
+
writer.blank();
|
|
703
|
+
writer.block("data object Null : Nullish<Nothing>", () => {
|
|
704
|
+
writer.line("override val value: Nothing? get() = null");
|
|
705
|
+
});
|
|
706
|
+
writer.blank();
|
|
707
|
+
writer.line("data class Some<out T>(override val value: T) : Nullish<T>");
|
|
708
|
+
});
|
|
709
|
+
writer.blank();
|
|
710
|
+
writer.block("class NullishSerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<Nullish<T>>", () => {
|
|
711
|
+
writer.line("override val descriptor: SerialDescriptor = valueSerializer.descriptor.nullable");
|
|
712
|
+
writer.blank();
|
|
713
|
+
writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
714
|
+
writer.block("override fun serialize(encoder: Encoder, value: Nullish<T>)", () => {
|
|
715
|
+
writer.line("when (value) {");
|
|
716
|
+
writer.line(" is Nullish.Some -> encoder.encodeSerializableValue(valueSerializer, value.value)");
|
|
717
|
+
writer.line(" Nullish.Null -> encoder.encodeNull()");
|
|
718
|
+
writer.line(" Nullish.Absent -> throw SerializationException(\"An absent field has no value to write, so its key is left out\")");
|
|
719
|
+
writer.line("}");
|
|
720
|
+
});
|
|
721
|
+
writer.blank();
|
|
722
|
+
writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
723
|
+
writer.block("override fun deserialize(decoder: Decoder): Nullish<T>", () => {
|
|
724
|
+
writer.line("if (decoder.decodeNotNullMark()) return Nullish.Some(decoder.decodeSerializableValue(valueSerializer))");
|
|
725
|
+
writer.line("decoder.decodeNull()");
|
|
726
|
+
writer.line("return Nullish.Null");
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
writer.blank();
|
|
730
|
+
};
|
|
731
|
+
const defaultArgument = (field) => {
|
|
732
|
+
if (!hasDefault(field)) return "";
|
|
733
|
+
return field.nullish ? " = Nullish.Absent" : " = null";
|
|
734
|
+
};
|
|
681
735
|
const optionalize = (type, optional) => {
|
|
682
736
|
if (!optional) return type;
|
|
683
737
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -698,6 +752,8 @@ const KOTLIN_PRIMITIVE_TYPES = new Set([
|
|
|
698
752
|
const decodeBody = (resolved) => resolved === "ByteArray" ? "data" : `json.decodeFromString<${resolved}>(data.decodeToString())`;
|
|
699
753
|
const resolveType = (typeName, currentOperation, context, scope = "client") => {
|
|
700
754
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
755
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
756
|
+
if (nullish) return `Nullish<${resolveType(nullish[1], currentOperation, context, scope)}>`;
|
|
701
757
|
const optional = typeName.endsWith("?");
|
|
702
758
|
const base = optional ? typeName.slice(0, -1) : typeName;
|
|
703
759
|
if (KOTLIN_PRIMITIVE_TYPES.has(base)) return typeName;
|
|
@@ -820,6 +876,8 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
820
876
|
const baseFields = type.discriminatorWireName ? type.fields.filter((field) => field.wireName !== type.discriminatorWireName) : type.fields;
|
|
821
877
|
const hasFile = baseFields.some((field) => field.isFile);
|
|
822
878
|
const resolveOwnedType = (raw) => {
|
|
879
|
+
const nullish = NULLISH_TYPE.exec(raw);
|
|
880
|
+
if (nullish) return `Nullish<${resolveOwnedType(nullish[1])}>`;
|
|
823
881
|
const optional = raw.endsWith("?");
|
|
824
882
|
const stripped = optional ? raw.slice(0, -1) : raw;
|
|
825
883
|
const isList = stripped.startsWith("List<") && stripped.endsWith(">");
|
|
@@ -835,12 +893,15 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
835
893
|
}));
|
|
836
894
|
const needsSerialName = !hasFile && baseFields.some((field) => field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name));
|
|
837
895
|
if (type.serialName !== void 0) writer.line(`@SerialName(${stringLiteral(type.serialName)})`);
|
|
838
|
-
if (!hasFile)
|
|
896
|
+
if (!hasFile) {
|
|
897
|
+
if (adjustedFields.some((field) => field.nullish)) writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
898
|
+
writer.line("@Serializable");
|
|
899
|
+
}
|
|
839
900
|
const params = adjustedFields.map((field) => {
|
|
840
901
|
const typeExpression = optionalize(field.type, field.optional);
|
|
841
|
-
const defaultPart = field
|
|
902
|
+
const defaultPart = defaultArgument(field);
|
|
842
903
|
const serialNameAnnotation = needsSerialName && (field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name)) ? `@SerialName(${stringLiteral(field.wireName)}) ` : "";
|
|
843
|
-
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
904
|
+
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}${field.nullish ? "@EncodeDefault(EncodeDefault.Mode.NEVER) " : ""}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
844
905
|
});
|
|
845
906
|
const hasOwnedTypes = Array.from(ownedTypeMap.entries()).some(([, owner]) => owner === lookupName);
|
|
846
907
|
if (params.length === 0) writer.line(`data class ${type.name}(private val unused: Unit = Unit)`);
|
|
@@ -882,6 +943,8 @@ const relativeOwnedPath = (typeName, ownedTypeMap, enclosingPath, registry) => {
|
|
|
882
943
|
return path.startsWith(prefix) ? path.slice(prefix.length) : void 0;
|
|
883
944
|
};
|
|
884
945
|
const ownedTypePath = (typeName, ownedTypeMap, registry) => {
|
|
946
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
947
|
+
if (nullish) return `Nullish<${ownedTypePath(nullish[1], ownedTypeMap, registry)}>`;
|
|
885
948
|
const sealedPath = registry?.sealedVariantPath(typeName);
|
|
886
949
|
if (sealedPath !== void 0) return sealedPath;
|
|
887
950
|
const owningClass = ownedTypeMap.get(typeName);
|
|
@@ -931,7 +994,7 @@ const emitSealedClass = (writer, type, registry, ownedTypeMap, ownedTypeLookup)
|
|
|
931
994
|
const params = fields.map((field) => {
|
|
932
995
|
const variantPath = registry.sealedVariantPath(variant.payloadType) ?? variant.payloadType;
|
|
933
996
|
const typeExpression = optionalize(relativeOwnedPath(field.type, ownedTypeMap, variantPath, registry) ?? ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
934
|
-
const defaultPart = field
|
|
997
|
+
const defaultPart = defaultArgument(field);
|
|
935
998
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
936
999
|
});
|
|
937
1000
|
if (params.length === 1) writer.line(`data class ${variant.caseName}(${params[0]}) : ${type.name}`);
|
|
@@ -1130,7 +1193,7 @@ const groupTypeRef = (operationName, className, context) => `${context.clientNam
|
|
|
1130
1193
|
const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
|
|
1131
1194
|
const groupClassField = (field) => {
|
|
1132
1195
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1133
|
-
const defaultPart = field
|
|
1196
|
+
const defaultPart = defaultArgument(field);
|
|
1134
1197
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1135
1198
|
};
|
|
1136
1199
|
const emitGroupClass = (writer, className, fields, method, context) => {
|
|
@@ -1189,13 +1252,13 @@ const methodChannels = (method) => {
|
|
|
1189
1252
|
if (method.query.length > 0) channels.push({
|
|
1190
1253
|
varName: "query",
|
|
1191
1254
|
className: "Query",
|
|
1192
|
-
required: method.query.some((field) => !field
|
|
1255
|
+
required: method.query.some((field) => !hasDefault(field)),
|
|
1193
1256
|
fields: method.query
|
|
1194
1257
|
});
|
|
1195
1258
|
if (method.headers.length > 0) channels.push({
|
|
1196
1259
|
varName: "headers",
|
|
1197
1260
|
className: "Headers",
|
|
1198
|
-
required: method.headers.some((field) => !field
|
|
1261
|
+
required: method.headers.some((field) => !hasDefault(field)),
|
|
1199
1262
|
fields: method.headers
|
|
1200
1263
|
});
|
|
1201
1264
|
if (hasBodyGroup(method)) {
|
|
@@ -1244,7 +1307,7 @@ const emitArgsScaffolding = (writer, method, context) => {
|
|
|
1244
1307
|
};
|
|
1245
1308
|
const factorySignature = (channel) => channel.fields.map((field) => {
|
|
1246
1309
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1247
|
-
const defaultPart = field
|
|
1310
|
+
const defaultPart = defaultArgument(field);
|
|
1248
1311
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1249
1312
|
}).join(", ");
|
|
1250
1313
|
const factoryConstruction = (channel) => {
|
|
@@ -1301,10 +1364,16 @@ const emitBodyEncoding = (writer, method, context) => {
|
|
|
1301
1364
|
if (body.kind === "multipart") {
|
|
1302
1365
|
writer.line("val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)");
|
|
1303
1366
|
for (const field of body.multipartFields) {
|
|
1304
|
-
const
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1367
|
+
const flattenedField = body.flattened.find((flatField) => flatField.name === field.name);
|
|
1368
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1369
|
+
const append = (accessor) => {
|
|
1370
|
+
if (isFile) writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.filename, ${accessor}.data.toRequestBody(${accessor}.mimeType.toMediaType()))`);
|
|
1371
|
+
else writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.toString())`);
|
|
1372
|
+
};
|
|
1373
|
+
if (flattenedField?.optional === true) writer.block(`if (${groupMemberAccessor("body", field)} != null)`, () => {
|
|
1374
|
+
append(groupMemberAccessor("body", field));
|
|
1375
|
+
});
|
|
1376
|
+
else append(groupMemberAccessor("body", field));
|
|
1308
1377
|
}
|
|
1309
1378
|
writer.line("requestBody = multipartBuilder.build()");
|
|
1310
1379
|
return;
|
|
@@ -1664,14 +1733,14 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1664
1733
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1665
1734
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1666
1735
|
const contextFields = context.requestContextFields;
|
|
1667
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1736
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1668
1737
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1669
1738
|
writer.blank();
|
|
1670
1739
|
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)`, () => {
|
|
1671
1740
|
if (contextFields.length > 0) {
|
|
1672
1741
|
writer.blank();
|
|
1673
1742
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
1674
|
-
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${field
|
|
1743
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`));
|
|
1675
1744
|
writer.blank();
|
|
1676
1745
|
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1677
1746
|
for (const field of contextFields) if (field.optional) writer.line(`if (requestContext.${escapeKeyword(field.name)} != null) put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)}!!)`);
|
|
@@ -1768,7 +1837,7 @@ const renderKotlinClient = (api, partition, registry, config) => {
|
|
|
1768
1837
|
}
|
|
1769
1838
|
writer.line("import kotlinx.serialization.*");
|
|
1770
1839
|
writer.line("import kotlinx.serialization.json.*");
|
|
1771
|
-
if (registry.all().some((type) => type.kind === "enum-class" && type.unknownCase)) {
|
|
1840
|
+
if (registry.all().some((type) => type.kind === "enum-class" && type.unknownCase) || registry.usesNullish) {
|
|
1772
1841
|
writer.line("import kotlinx.serialization.descriptors.*");
|
|
1773
1842
|
writer.line("import kotlinx.serialization.encoding.*");
|
|
1774
1843
|
}
|
|
@@ -1786,6 +1855,7 @@ const renderKotlinClient = (api, partition, registry, config) => {
|
|
|
1786
1855
|
});
|
|
1787
1856
|
writer.blank();
|
|
1788
1857
|
}
|
|
1858
|
+
if (registry.usesNullish) emitNullish(writer);
|
|
1789
1859
|
const sharedTypes = [];
|
|
1790
1860
|
const typesByOperation = /* @__PURE__ */ new Map();
|
|
1791
1861
|
for (const type of registry.all()) {
|
|
@@ -58,6 +58,7 @@ var TypeRegistry = class {
|
|
|
58
58
|
sealedVariantPaths = /* @__PURE__ */ new Map();
|
|
59
59
|
brands = /* @__PURE__ */ new Map();
|
|
60
60
|
usesJsonElement = false;
|
|
61
|
+
usesNullish = false;
|
|
61
62
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
62
63
|
this.camelCaseProperties = camelCaseProperties;
|
|
63
64
|
this.unknownEnumCase = unknownEnumCase;
|
|
@@ -127,11 +128,13 @@ const propertyName = (key, camelCase) => {
|
|
|
127
128
|
if (!camel) return "field";
|
|
128
129
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
129
130
|
};
|
|
130
|
-
const objectFields = (schema, registry, hint, shape = readObjectShape(schema) ?? {}) => {
|
|
131
|
+
const objectFields = (schema, registry, hint, nullishTriState = true, shape = readObjectShape(schema) ?? {}) => {
|
|
131
132
|
const fields = [];
|
|
132
133
|
const seen = /* @__PURE__ */ new Map();
|
|
133
134
|
for (const [key, value] of Object.entries(shape)) {
|
|
134
135
|
const result = mapType(value, registry, `${hint}${toPascalCase(key)}`);
|
|
136
|
+
const nullish = nullishTriState && result.nullish === true;
|
|
137
|
+
if (nullish) registry.usesNullish = true;
|
|
135
138
|
const deprecation = readDeprecation(value);
|
|
136
139
|
const kotlinName = propertyName(key, registry.camelCaseProperties);
|
|
137
140
|
const previousWireName = seen.get(kotlinName);
|
|
@@ -140,8 +143,10 @@ const objectFields = (schema, registry, hint, shape = readObjectShape(schema) ??
|
|
|
140
143
|
fields.push({
|
|
141
144
|
name: kotlinName,
|
|
142
145
|
wireName: key,
|
|
143
|
-
type: result.expression,
|
|
144
|
-
optional: result.optional,
|
|
146
|
+
type: nullish ? `Nullish<${result.expression.replace(/\?$/, "")}>` : result.expression,
|
|
147
|
+
optional: result.optional && !nullish,
|
|
148
|
+
requiredNullable: result.requiredNullable,
|
|
149
|
+
nullish,
|
|
145
150
|
description: readMetaDescription(value),
|
|
146
151
|
isFile: result.isFile,
|
|
147
152
|
deprecated: deprecation !== void 0,
|
|
@@ -264,6 +269,8 @@ const mapUnbrandedType = (schema, registry, hint) => {
|
|
|
264
269
|
return {
|
|
265
270
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
266
271
|
optional: true,
|
|
272
|
+
requiredNullable: nullable && !optional,
|
|
273
|
+
nullish: nullable && optional,
|
|
267
274
|
isFile: innerResult.isFile
|
|
268
275
|
};
|
|
269
276
|
}
|
|
@@ -445,9 +452,13 @@ const sanitizeCaseName = (literal) => {
|
|
|
445
452
|
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
446
453
|
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
447
454
|
};
|
|
448
|
-
|
|
449
|
-
|
|
455
|
+
/**
|
|
456
|
+
* A JSON body keeps `.nullish()` as `Nullish`. A query, path, header or form field has no null, so it takes an optional.
|
|
457
|
+
*/
|
|
458
|
+
const collectObjectFields = (schema, registry, hint, json = false) => {
|
|
459
|
+
return objectFields(schema, registry, hint, json);
|
|
450
460
|
};
|
|
461
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true || field.nullish === true;
|
|
451
462
|
const objectFieldCount = (schema) => {
|
|
452
463
|
return Object.keys(readObjectShape(schema) ?? {}).length;
|
|
453
464
|
};
|
|
@@ -512,7 +523,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
512
523
|
else bodyDescriptor = {
|
|
513
524
|
kind: "json-flat",
|
|
514
525
|
structName,
|
|
515
|
-
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
526
|
+
flattened: collectObjectFields(route.body, registry, bodyHint, true),
|
|
516
527
|
multipartFields: []
|
|
517
528
|
};
|
|
518
529
|
}
|
|
@@ -678,6 +689,49 @@ const KOTLIN_KEYWORDS = new Set([
|
|
|
678
689
|
const escapeKeyword = (name) => {
|
|
679
690
|
return KOTLIN_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
680
691
|
};
|
|
692
|
+
const NULLISH_TYPE = /^Nullish<(.+)>$/;
|
|
693
|
+
const emitNullish = (writer) => {
|
|
694
|
+
writer.line("/** A `.nullish()` field: left out, sent as `null`, or sent with a value. */");
|
|
695
|
+
writer.line("@Serializable(with = NullishSerializer::class)");
|
|
696
|
+
writer.block("sealed interface Nullish<out T>", () => {
|
|
697
|
+
writer.line("val value: T?");
|
|
698
|
+
writer.blank();
|
|
699
|
+
writer.block("data object Absent : Nullish<Nothing>", () => {
|
|
700
|
+
writer.line("override val value: Nothing? get() = null");
|
|
701
|
+
});
|
|
702
|
+
writer.blank();
|
|
703
|
+
writer.block("data object Null : Nullish<Nothing>", () => {
|
|
704
|
+
writer.line("override val value: Nothing? get() = null");
|
|
705
|
+
});
|
|
706
|
+
writer.blank();
|
|
707
|
+
writer.line("data class Some<out T>(override val value: T) : Nullish<T>");
|
|
708
|
+
});
|
|
709
|
+
writer.blank();
|
|
710
|
+
writer.block("class NullishSerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<Nullish<T>>", () => {
|
|
711
|
+
writer.line("override val descriptor: SerialDescriptor = valueSerializer.descriptor.nullable");
|
|
712
|
+
writer.blank();
|
|
713
|
+
writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
714
|
+
writer.block("override fun serialize(encoder: Encoder, value: Nullish<T>)", () => {
|
|
715
|
+
writer.line("when (value) {");
|
|
716
|
+
writer.line(" is Nullish.Some -> encoder.encodeSerializableValue(valueSerializer, value.value)");
|
|
717
|
+
writer.line(" Nullish.Null -> encoder.encodeNull()");
|
|
718
|
+
writer.line(" Nullish.Absent -> throw SerializationException(\"An absent field has no value to write, so its key is left out\")");
|
|
719
|
+
writer.line("}");
|
|
720
|
+
});
|
|
721
|
+
writer.blank();
|
|
722
|
+
writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
723
|
+
writer.block("override fun deserialize(decoder: Decoder): Nullish<T>", () => {
|
|
724
|
+
writer.line("if (decoder.decodeNotNullMark()) return Nullish.Some(decoder.decodeSerializableValue(valueSerializer))");
|
|
725
|
+
writer.line("decoder.decodeNull()");
|
|
726
|
+
writer.line("return Nullish.Null");
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
writer.blank();
|
|
730
|
+
};
|
|
731
|
+
const defaultArgument = (field) => {
|
|
732
|
+
if (!hasDefault(field)) return "";
|
|
733
|
+
return field.nullish ? " = Nullish.Absent" : " = null";
|
|
734
|
+
};
|
|
681
735
|
const optionalize = (type, optional) => {
|
|
682
736
|
if (!optional) return type;
|
|
683
737
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -698,6 +752,8 @@ const KOTLIN_PRIMITIVE_TYPES = new Set([
|
|
|
698
752
|
const decodeBody = (resolved) => resolved === "ByteArray" ? "data" : `json.decodeFromString<${resolved}>(data.decodeToString())`;
|
|
699
753
|
const resolveType = (typeName, currentOperation, context, scope = "client") => {
|
|
700
754
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
755
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
756
|
+
if (nullish) return `Nullish<${resolveType(nullish[1], currentOperation, context, scope)}>`;
|
|
701
757
|
const optional = typeName.endsWith("?");
|
|
702
758
|
const base = optional ? typeName.slice(0, -1) : typeName;
|
|
703
759
|
if (KOTLIN_PRIMITIVE_TYPES.has(base)) return typeName;
|
|
@@ -820,6 +876,8 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
820
876
|
const baseFields = type.discriminatorWireName ? type.fields.filter((field) => field.wireName !== type.discriminatorWireName) : type.fields;
|
|
821
877
|
const hasFile = baseFields.some((field) => field.isFile);
|
|
822
878
|
const resolveOwnedType = (raw) => {
|
|
879
|
+
const nullish = NULLISH_TYPE.exec(raw);
|
|
880
|
+
if (nullish) return `Nullish<${resolveOwnedType(nullish[1])}>`;
|
|
823
881
|
const optional = raw.endsWith("?");
|
|
824
882
|
const stripped = optional ? raw.slice(0, -1) : raw;
|
|
825
883
|
const isList = stripped.startsWith("List<") && stripped.endsWith(">");
|
|
@@ -835,12 +893,15 @@ const emitDataClass = (writer, type, ownedTypeMap, ownedTypeLookup, registryName
|
|
|
835
893
|
}));
|
|
836
894
|
const needsSerialName = !hasFile && baseFields.some((field) => field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name));
|
|
837
895
|
if (type.serialName !== void 0) writer.line(`@SerialName(${stringLiteral(type.serialName)})`);
|
|
838
|
-
if (!hasFile)
|
|
896
|
+
if (!hasFile) {
|
|
897
|
+
if (adjustedFields.some((field) => field.nullish)) writer.line("@OptIn(ExperimentalSerializationApi::class)");
|
|
898
|
+
writer.line("@Serializable");
|
|
899
|
+
}
|
|
839
900
|
const params = adjustedFields.map((field) => {
|
|
840
901
|
const typeExpression = optionalize(field.type, field.optional);
|
|
841
|
-
const defaultPart = field
|
|
902
|
+
const defaultPart = defaultArgument(field);
|
|
842
903
|
const serialNameAnnotation = needsSerialName && (field.name !== field.wireName || KOTLIN_KEYWORDS.has(field.name)) ? `@SerialName(${stringLiteral(field.wireName)}) ` : "";
|
|
843
|
-
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
904
|
+
return `${field.deprecated ? `${deprecatedAnnotation(field.deprecationMessage)} ` : ""}${serialNameAnnotation}${field.nullish ? "@EncodeDefault(EncodeDefault.Mode.NEVER) " : ""}val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
844
905
|
});
|
|
845
906
|
const hasOwnedTypes = Array.from(ownedTypeMap.entries()).some(([, owner]) => owner === lookupName);
|
|
846
907
|
if (params.length === 0) writer.line(`data class ${type.name}(private val unused: Unit = Unit)`);
|
|
@@ -882,6 +943,8 @@ const relativeOwnedPath = (typeName, ownedTypeMap, enclosingPath, registry) => {
|
|
|
882
943
|
return path.startsWith(prefix) ? path.slice(prefix.length) : void 0;
|
|
883
944
|
};
|
|
884
945
|
const ownedTypePath = (typeName, ownedTypeMap, registry) => {
|
|
946
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
947
|
+
if (nullish) return `Nullish<${ownedTypePath(nullish[1], ownedTypeMap, registry)}>`;
|
|
885
948
|
const sealedPath = registry?.sealedVariantPath(typeName);
|
|
886
949
|
if (sealedPath !== void 0) return sealedPath;
|
|
887
950
|
const owningClass = ownedTypeMap.get(typeName);
|
|
@@ -931,7 +994,7 @@ const emitSealedClass = (writer, type, registry, ownedTypeMap, ownedTypeLookup)
|
|
|
931
994
|
const params = fields.map((field) => {
|
|
932
995
|
const variantPath = registry.sealedVariantPath(variant.payloadType) ?? variant.payloadType;
|
|
933
996
|
const typeExpression = optionalize(relativeOwnedPath(field.type, ownedTypeMap, variantPath, registry) ?? ownedTypePath(field.type, ownedTypeMap, registry), field.optional);
|
|
934
|
-
const defaultPart = field
|
|
997
|
+
const defaultPart = defaultArgument(field);
|
|
935
998
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
936
999
|
});
|
|
937
1000
|
if (params.length === 1) writer.line(`data class ${variant.caseName}(${params[0]}) : ${type.name}`);
|
|
@@ -1130,7 +1193,7 @@ const groupTypeRef = (operationName, className, context) => `${context.clientNam
|
|
|
1130
1193
|
const groupMemberAccessor = (groupLabel, field) => `${groupLabel}.${escapeKeyword(field.name)}`;
|
|
1131
1194
|
const groupClassField = (field) => {
|
|
1132
1195
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1133
|
-
const defaultPart = field
|
|
1196
|
+
const defaultPart = defaultArgument(field);
|
|
1134
1197
|
return `val ${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1135
1198
|
};
|
|
1136
1199
|
const emitGroupClass = (writer, className, fields, method, context) => {
|
|
@@ -1189,13 +1252,13 @@ const methodChannels = (method) => {
|
|
|
1189
1252
|
if (method.query.length > 0) channels.push({
|
|
1190
1253
|
varName: "query",
|
|
1191
1254
|
className: "Query",
|
|
1192
|
-
required: method.query.some((field) => !field
|
|
1255
|
+
required: method.query.some((field) => !hasDefault(field)),
|
|
1193
1256
|
fields: method.query
|
|
1194
1257
|
});
|
|
1195
1258
|
if (method.headers.length > 0) channels.push({
|
|
1196
1259
|
varName: "headers",
|
|
1197
1260
|
className: "Headers",
|
|
1198
|
-
required: method.headers.some((field) => !field
|
|
1261
|
+
required: method.headers.some((field) => !hasDefault(field)),
|
|
1199
1262
|
fields: method.headers
|
|
1200
1263
|
});
|
|
1201
1264
|
if (hasBodyGroup(method)) {
|
|
@@ -1244,7 +1307,7 @@ const emitArgsScaffolding = (writer, method, context) => {
|
|
|
1244
1307
|
};
|
|
1245
1308
|
const factorySignature = (channel) => channel.fields.map((field) => {
|
|
1246
1309
|
const typeExpression = optionalize(field.type, field.optional);
|
|
1247
|
-
const defaultPart = field
|
|
1310
|
+
const defaultPart = defaultArgument(field);
|
|
1248
1311
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
1249
1312
|
}).join(", ");
|
|
1250
1313
|
const factoryConstruction = (channel) => {
|
|
@@ -1301,10 +1364,16 @@ const emitBodyEncoding = (writer, method, context) => {
|
|
|
1301
1364
|
if (body.kind === "multipart") {
|
|
1302
1365
|
writer.line("val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)");
|
|
1303
1366
|
for (const field of body.multipartFields) {
|
|
1304
|
-
const
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1367
|
+
const flattenedField = body.flattened.find((flatField) => flatField.name === field.name);
|
|
1368
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1369
|
+
const append = (accessor) => {
|
|
1370
|
+
if (isFile) writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.filename, ${accessor}.data.toRequestBody(${accessor}.mimeType.toMediaType()))`);
|
|
1371
|
+
else writer.line(`multipartBuilder.addFormDataPart(${stringLiteral(field.wireName)}, ${accessor}.toString())`);
|
|
1372
|
+
};
|
|
1373
|
+
if (flattenedField?.optional === true) writer.block(`if (${groupMemberAccessor("body", field)} != null)`, () => {
|
|
1374
|
+
append(groupMemberAccessor("body", field));
|
|
1375
|
+
});
|
|
1376
|
+
else append(groupMemberAccessor("body", field));
|
|
1308
1377
|
}
|
|
1309
1378
|
writer.line("requestBody = multipartBuilder.build()");
|
|
1310
1379
|
return;
|
|
@@ -1664,14 +1733,14 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1664
1733
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1665
1734
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1666
1735
|
const contextFields = context.requestContextFields;
|
|
1667
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1736
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1668
1737
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1669
1738
|
writer.blank();
|
|
1670
1739
|
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)`, () => {
|
|
1671
1740
|
if (contextFields.length > 0) {
|
|
1672
1741
|
writer.blank();
|
|
1673
1742
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
1674
|
-
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${field
|
|
1743
|
+
emitConstructorClass(writer, "data class RequestContext", contextFields.map((field) => `val ${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`));
|
|
1675
1744
|
writer.blank();
|
|
1676
1745
|
writer.block("private val requestContextHeaders: Map<String, String> = buildMap", () => {
|
|
1677
1746
|
for (const field of contextFields) if (field.optional) writer.line(`if (requestContext.${escapeKeyword(field.name)} != null) put(${stringLiteral(field.wireName)}, requestContext.${escapeKeyword(field.name)}!!)`);
|
|
@@ -1768,7 +1837,7 @@ const renderKotlinClient = (api, partition, registry, config) => {
|
|
|
1768
1837
|
}
|
|
1769
1838
|
writer.line("import kotlinx.serialization.*");
|
|
1770
1839
|
writer.line("import kotlinx.serialization.json.*");
|
|
1771
|
-
if (registry.all().some((type) => type.kind === "enum-class" && type.unknownCase)) {
|
|
1840
|
+
if (registry.all().some((type) => type.kind === "enum-class" && type.unknownCase) || registry.usesNullish) {
|
|
1772
1841
|
writer.line("import kotlinx.serialization.descriptors.*");
|
|
1773
1842
|
writer.line("import kotlinx.serialization.encoding.*");
|
|
1774
1843
|
}
|
|
@@ -1786,6 +1855,7 @@ const renderKotlinClient = (api, partition, registry, config) => {
|
|
|
1786
1855
|
});
|
|
1787
1856
|
writer.blank();
|
|
1788
1857
|
}
|
|
1858
|
+
if (registry.usesNullish) emitNullish(writer);
|
|
1789
1859
|
const sharedTypes = [];
|
|
1790
1860
|
const typesByOperation = /* @__PURE__ */ new Map();
|
|
1791
1861
|
for (const type of registry.all()) {
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_generator = require("./generator-
|
|
2
|
+
const require_generator = require("./generator-CeUS3C_6.cjs");
|
|
3
3
|
let kizunajs_generator = require("kizunajs/generator");
|
|
4
4
|
let zod = require("zod");
|
|
5
5
|
//#region src/client-target.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as kotlinClientWalk, t as generateKotlinClient } from "./generator-
|
|
1
|
+
import { n as kotlinClientWalk, t as generateKotlinClient } from "./generator-D2GCSZwz.mjs";
|
|
2
2
|
import { defineClient } from "kizunajs/generator";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
/**
|
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.18",
|
|
4
4
|
"description": "Kotlin client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@kizunajs/cli": "2.0.0-beta.
|
|
63
|
+
"@kizunajs/cli": "2.0.0-beta.18"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"kizunajs": "2.0.0-beta.
|
|
66
|
+
"kizunajs": "2.0.0-beta.18",
|
|
67
67
|
"zod": "^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"kizunajs": "2.0.0-beta.
|
|
70
|
+
"kizunajs": "2.0.0-beta.18",
|
|
71
71
|
"@types/node": "^22.9.0",
|
|
72
72
|
"tsdown": "^0.21.0",
|
|
73
73
|
"typescript": "^6.0.3",
|