@kizunajs/swift 2.0.0-beta.17 → 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 generateSwiftClient } from "./generator-
|
|
2
|
+
import { t as generateSwiftClient } from "./generator-CZr33FgN.mjs";
|
|
3
3
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -47,6 +47,7 @@ var TypeRegistry = class {
|
|
|
47
47
|
unionVariantOwners = /* @__PURE__ */ new Map();
|
|
48
48
|
brands = /* @__PURE__ */ new Map();
|
|
49
49
|
usesAnyCodable = false;
|
|
50
|
+
usesNullish = false;
|
|
50
51
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
51
52
|
this.camelCaseProperties = camelCaseProperties;
|
|
52
53
|
this.unknownEnumCase = unknownEnumCase;
|
|
@@ -110,11 +111,13 @@ const propertyName = (key, camelCase) => {
|
|
|
110
111
|
if (!camel) return "field";
|
|
111
112
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
112
113
|
};
|
|
113
|
-
const objectFields = (schema, registry, hint, shape = readObjectShape(schema) ?? {}) => {
|
|
114
|
+
const objectFields = (schema, registry, hint, nullishTriState = true, shape = readObjectShape(schema) ?? {}) => {
|
|
114
115
|
const fields = [];
|
|
115
116
|
const seen = /* @__PURE__ */ new Map();
|
|
116
117
|
for (const [key, value] of Object.entries(shape)) {
|
|
117
118
|
const result = mapType(value, registry, `${hint}${toPascalCase(key)}`);
|
|
119
|
+
const nullish = nullishTriState && result.nullish === true;
|
|
120
|
+
if (nullish) registry.usesNullish = true;
|
|
118
121
|
const deprecation = readDeprecation(value);
|
|
119
122
|
const swiftName = propertyName(key, registry.camelCaseProperties);
|
|
120
123
|
const previousWireName = seen.get(swiftName);
|
|
@@ -123,8 +126,10 @@ const objectFields = (schema, registry, hint, shape = readObjectShape(schema) ??
|
|
|
123
126
|
fields.push({
|
|
124
127
|
name: swiftName,
|
|
125
128
|
wireName: key,
|
|
126
|
-
type: result.expression,
|
|
127
|
-
optional: result.optional,
|
|
129
|
+
type: nullish ? `Nullish<${result.expression.replace(/\?$/, "")}>` : result.expression,
|
|
130
|
+
optional: result.optional && !nullish,
|
|
131
|
+
requiredNullable: result.requiredNullable,
|
|
132
|
+
nullish,
|
|
128
133
|
description: readMetaDescription(value),
|
|
129
134
|
isFile: result.isFile,
|
|
130
135
|
deprecated: deprecation !== void 0,
|
|
@@ -236,6 +241,8 @@ const mapUnbrandedType = (schema, registry, hint) => {
|
|
|
236
241
|
return {
|
|
237
242
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
238
243
|
optional: true,
|
|
244
|
+
requiredNullable: nullable && !optional,
|
|
245
|
+
nullish: nullable && optional,
|
|
239
246
|
isFile: innerResult.isFile
|
|
240
247
|
};
|
|
241
248
|
}
|
|
@@ -417,9 +424,13 @@ const sanitizeCaseName = (literal) => {
|
|
|
417
424
|
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
418
425
|
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
419
426
|
};
|
|
420
|
-
|
|
421
|
-
|
|
427
|
+
/**
|
|
428
|
+
* A JSON body keeps `.nullish()` as `Nullish`. A query, path, header or form field has no null, so it takes an optional.
|
|
429
|
+
*/
|
|
430
|
+
const collectObjectFields = (schema, registry, hint, json = false) => {
|
|
431
|
+
return objectFields(schema, registry, hint, json);
|
|
422
432
|
};
|
|
433
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true || field.nullish === true;
|
|
423
434
|
const objectFieldCount = (schema) => {
|
|
424
435
|
return Object.keys(readObjectShape(schema) ?? {}).length;
|
|
425
436
|
};
|
|
@@ -476,7 +487,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
476
487
|
else bodyDescriptor = {
|
|
477
488
|
kind: "json-flat",
|
|
478
489
|
structName,
|
|
479
|
-
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
490
|
+
flattened: collectObjectFields(route.body, registry, bodyHint, true),
|
|
480
491
|
multipartFields: []
|
|
481
492
|
};
|
|
482
493
|
}
|
|
@@ -696,6 +707,63 @@ const sanitizeEnumCaseName = (value) => {
|
|
|
696
707
|
if (!camel) return "_";
|
|
697
708
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
698
709
|
};
|
|
710
|
+
const NULLISH_TYPE = /^Nullish<(.+)>$/;
|
|
711
|
+
const emitNullish = (writer) => {
|
|
712
|
+
writer.blank();
|
|
713
|
+
writer.docComment("A `.nullish()` field: left out, sent as `null`, or sent with a value.");
|
|
714
|
+
writer.block("public enum Nullish<Wrapped>", () => {
|
|
715
|
+
writer.line("case absent");
|
|
716
|
+
writer.line("case null");
|
|
717
|
+
writer.line("case some(Wrapped)");
|
|
718
|
+
writer.blank();
|
|
719
|
+
writer.block("public var value: Wrapped?", () => {
|
|
720
|
+
writer.line("if case .some(let value) = self { return value }");
|
|
721
|
+
writer.line("return nil");
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
writer.blank();
|
|
725
|
+
writer.line("extension Nullish: Sendable where Wrapped: Sendable {}");
|
|
726
|
+
writer.line("extension Nullish: Equatable where Wrapped: Equatable {}");
|
|
727
|
+
writer.line("extension Nullish: Hashable where Wrapped: Hashable {}");
|
|
728
|
+
writer.blank();
|
|
729
|
+
writer.block("extension Nullish: Encodable where Wrapped: Encodable", () => {
|
|
730
|
+
writer.block("public func encode(to encoder: Encoder) throws", () => {
|
|
731
|
+
writer.line("var container = encoder.singleValueContainer()");
|
|
732
|
+
writer.line("switch self {");
|
|
733
|
+
writer.line("case .absent, .null: try container.encodeNil()");
|
|
734
|
+
writer.line("case .some(let value): try container.encode(value)");
|
|
735
|
+
writer.line("}");
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
writer.blank();
|
|
739
|
+
writer.block("extension Nullish: Decodable where Wrapped: Decodable", () => {
|
|
740
|
+
writer.block("public init(from decoder: Decoder) throws", () => {
|
|
741
|
+
writer.line("let container = try decoder.singleValueContainer()");
|
|
742
|
+
writer.line("self = container.decodeNil() ? .null : .some(try container.decode(Wrapped.self))");
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
writer.blank();
|
|
746
|
+
writer.block("extension KeyedEncodingContainer", () => {
|
|
747
|
+
writer.block("public mutating func encode<Wrapped: Encodable>(_ value: Nullish<Wrapped>, forKey key: Key) throws", () => {
|
|
748
|
+
writer.line("switch value {");
|
|
749
|
+
writer.line("case .absent: return");
|
|
750
|
+
writer.line("case .null: try encodeNil(forKey: key)");
|
|
751
|
+
writer.line("case .some(let wrapped): try encode(wrapped, forKey: key)");
|
|
752
|
+
writer.line("}");
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
writer.blank();
|
|
756
|
+
writer.block("extension KeyedDecodingContainer", () => {
|
|
757
|
+
writer.block("public func decode<Wrapped: Decodable>(_ type: Nullish<Wrapped>.Type, forKey key: Key) throws -> Nullish<Wrapped>", () => {
|
|
758
|
+
writer.line("guard contains(key) else { return .absent }");
|
|
759
|
+
writer.line("return try decodeNil(forKey: key) ? .null : .some(try decode(Wrapped.self, forKey: key))");
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
const defaultArgument = (field) => {
|
|
764
|
+
if (!hasDefault(field)) return "";
|
|
765
|
+
return field.nullish ? " = .absent" : " = nil";
|
|
766
|
+
};
|
|
699
767
|
const optionalize = (type, optional) => {
|
|
700
768
|
if (!optional) return type;
|
|
701
769
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -710,7 +778,7 @@ const emitNamedFactory = (writer, factoryName, fields, buildExpression) => {
|
|
|
710
778
|
}
|
|
711
779
|
const params = fields.map((field) => {
|
|
712
780
|
const typeExpression = optionalize(field.type, field.optional);
|
|
713
|
-
const defaultPart = field
|
|
781
|
+
const defaultPart = defaultArgument(field);
|
|
714
782
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
715
783
|
});
|
|
716
784
|
const args = fields.map((field) => `${field.name}: ${escapeKeyword(field.name)}`).join(", ");
|
|
@@ -733,7 +801,7 @@ const emitMemberwiseInit = (writer, fields, assignmentTarget = (field) => escape
|
|
|
733
801
|
}
|
|
734
802
|
const params = fields.map((field) => {
|
|
735
803
|
const typeExpression = optionalize(field.type, field.optional);
|
|
736
|
-
const defaultPart = field
|
|
804
|
+
const defaultPart = defaultArgument(field);
|
|
737
805
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
738
806
|
});
|
|
739
807
|
if (params.length === 1) {
|
|
@@ -851,6 +919,8 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
851
919
|
const hasFile = type.fields.some((field) => field.isFile);
|
|
852
920
|
const conformances = hasFile ? "Sendable, Equatable" : "Codable, Sendable, Equatable";
|
|
853
921
|
const resolveOwnedType = (raw) => {
|
|
922
|
+
const nullish = NULLISH_TYPE.exec(raw);
|
|
923
|
+
if (nullish) return `Nullish<${resolveOwnedType(nullish[1])}>`;
|
|
854
924
|
const optional = raw.endsWith("?");
|
|
855
925
|
const stripped = optional ? raw.slice(0, -1) : raw;
|
|
856
926
|
const isArray = stripped.startsWith("[") && stripped.endsWith("]");
|
|
@@ -867,7 +937,9 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
867
937
|
...field,
|
|
868
938
|
type: resolveOwnedType(field.type)
|
|
869
939
|
}));
|
|
870
|
-
const
|
|
940
|
+
const needsEncode = !hasFile && type.fields.some((field) => field.requiredNullable === true);
|
|
941
|
+
const needsCodingKeys = needsEncode || !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
|
|
942
|
+
const storageName = (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name);
|
|
871
943
|
writer.block(`public struct ${type.name}: ${conformances}`, () => {
|
|
872
944
|
emitOwnedTypes(writer, lookupName, context, ownedTypeMap, ownedTypeLookup);
|
|
873
945
|
for (const field of type.fields) {
|
|
@@ -890,8 +962,18 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
890
962
|
else writer.line(`case ${escapeKeyword(field.name)} = ${stringLiteral(field.wireName)}`);
|
|
891
963
|
});
|
|
892
964
|
}
|
|
965
|
+
if (needsEncode) {
|
|
966
|
+
writer.blank();
|
|
967
|
+
writer.block("public func encode(to encoder: Encoder) throws", () => {
|
|
968
|
+
writer.line("var container = encoder.container(keyedBy: CodingKeys.self)");
|
|
969
|
+
for (const field of type.fields) {
|
|
970
|
+
const encodeCall = field.optional && field.requiredNullable !== true ? "encodeIfPresent" : "encode";
|
|
971
|
+
writer.line(`try container.${encodeCall}(${storageName(field)}, forKey: .${storageName(field)})`);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
893
975
|
writer.blank();
|
|
894
|
-
emitMemberwiseInit(writer, adjustedFields,
|
|
976
|
+
emitMemberwiseInit(writer, adjustedFields, storageName);
|
|
895
977
|
});
|
|
896
978
|
};
|
|
897
979
|
/**
|
|
@@ -929,7 +1011,7 @@ const emitDiscriminatedEnum = (writer, type, context, registryName, ownedTypeMap
|
|
|
929
1011
|
if (!payloadStruct) continue;
|
|
930
1012
|
const isDiscriminator = (field) => field.wireName === type.discriminator || field.name === type.discriminator;
|
|
931
1013
|
const factoryParams = payloadStruct.fields.filter((field) => !isDiscriminator(field)).map((field) => {
|
|
932
|
-
const defaultPart = field
|
|
1014
|
+
const defaultPart = defaultArgument(field);
|
|
933
1015
|
const fieldType = (ownedTypeMap && relativeOwnedPath(field.type, ownedTypeMap, lookupName)) ?? resolveType(field.type, void 0, context);
|
|
934
1016
|
return `${escapeKeyword(field.name)}: ${optionalize(fieldType, field.optional)}${defaultPart}`;
|
|
935
1017
|
}).join(", ");
|
|
@@ -1027,6 +1109,8 @@ const dictionarySeparator = (inner) => {
|
|
|
1027
1109
|
};
|
|
1028
1110
|
const resolveType = (typeName, currentOperation, context, scope = "actor") => {
|
|
1029
1111
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
1112
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
1113
|
+
if (nullish) return `Nullish<${resolveType(nullish[1], currentOperation, context, scope)}>`;
|
|
1030
1114
|
const optional = typeName.endsWith("?");
|
|
1031
1115
|
const base = optional ? typeName.slice(0, -1) : typeName;
|
|
1032
1116
|
if (SWIFT_PRIMITIVE_TYPES.has(base)) return typeName;
|
|
@@ -1652,13 +1736,13 @@ const methodGroups = (method, context) => {
|
|
|
1652
1736
|
varName: "query",
|
|
1653
1737
|
type: groupTypeRef(operationName, "Query", context),
|
|
1654
1738
|
factory: "query",
|
|
1655
|
-
required: method.query.some((field) => !field
|
|
1739
|
+
required: method.query.some((field) => !hasDefault(field))
|
|
1656
1740
|
});
|
|
1657
1741
|
if (method.headers.length > 0) groups.push({
|
|
1658
1742
|
varName: "headers",
|
|
1659
1743
|
type: groupTypeRef(operationName, "Headers", context),
|
|
1660
1744
|
factory: "headers",
|
|
1661
|
-
required: method.headers.some((field) => !field
|
|
1745
|
+
required: method.headers.some((field) => !hasDefault(field))
|
|
1662
1746
|
});
|
|
1663
1747
|
return [...groups.filter((group) => group.required), ...groups.filter((group) => !group.required)];
|
|
1664
1748
|
};
|
|
@@ -1874,9 +1958,15 @@ const emitBodyEncoding = (writer, method, context, receiver = "") => {
|
|
|
1874
1958
|
writer.line("var multipart = Kizuna.MultipartBuilder()");
|
|
1875
1959
|
for (const field of body.multipartFields) {
|
|
1876
1960
|
const flattenedField = body.flattened.find((candidate) => candidate.name === field.name);
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1961
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1962
|
+
const append = (accessor) => {
|
|
1963
|
+
if (isFile) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
|
|
1964
|
+
else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
|
|
1965
|
+
};
|
|
1966
|
+
if (flattenedField?.optional === true) writer.block(`if let value = body.${escapeKeyword(field.name)}`, () => {
|
|
1967
|
+
append("value");
|
|
1968
|
+
});
|
|
1969
|
+
else append(`body.${escapeKeyword(field.name)}`);
|
|
1880
1970
|
}
|
|
1881
1971
|
writer.line("request.httpBody = multipart.finalize()");
|
|
1882
1972
|
writer.line("request.setValue(multipart.contentType, forHTTPHeaderField: \"Content-Type\")");
|
|
@@ -1895,7 +1985,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
1895
1985
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1896
1986
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1897
1987
|
const contextFields = context.requestContextFields;
|
|
1898
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1988
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1899
1989
|
writer.blank();
|
|
1900
1990
|
writer.block(`public final class ${clientName}: Sendable`, () => {
|
|
1901
1991
|
if (contextFields.length > 0) {
|
|
@@ -1904,7 +1994,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
1904
1994
|
writer.block("public struct RequestContext: Sendable, Equatable", () => {
|
|
1905
1995
|
for (const field of contextFields) writer.line(`public var ${escapeKeyword(field.name)}: ${field.type}`);
|
|
1906
1996
|
writer.blank();
|
|
1907
|
-
const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${field
|
|
1997
|
+
const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`).join(", ");
|
|
1908
1998
|
writer.block(`public init(${initParams})`, () => {
|
|
1909
1999
|
for (const field of contextFields) writer.line(`self.${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`);
|
|
1910
2000
|
});
|
|
@@ -2125,6 +2215,7 @@ const renderSwiftClient = (api, partition, registry, options) => {
|
|
|
2125
2215
|
anyCodable: registry.usesAnyCodable
|
|
2126
2216
|
}, partition, context, typesByOperation);
|
|
2127
2217
|
for (const group of partition.groups) emitSubClientStruct(writer, group, clientName, context);
|
|
2218
|
+
if (registry.usesNullish) emitNullish(writer);
|
|
2128
2219
|
emitKizunaFailureProtocols(writer);
|
|
2129
2220
|
emitKizunaNamespace(writer, {
|
|
2130
2221
|
multipart: usesMultipart,
|
|
@@ -47,6 +47,7 @@ var TypeRegistry = class {
|
|
|
47
47
|
unionVariantOwners = /* @__PURE__ */ new Map();
|
|
48
48
|
brands = /* @__PURE__ */ new Map();
|
|
49
49
|
usesAnyCodable = false;
|
|
50
|
+
usesNullish = false;
|
|
50
51
|
constructor(camelCaseProperties = false, unknownEnumCase = false) {
|
|
51
52
|
this.camelCaseProperties = camelCaseProperties;
|
|
52
53
|
this.unknownEnumCase = unknownEnumCase;
|
|
@@ -110,11 +111,13 @@ const propertyName = (key, camelCase) => {
|
|
|
110
111
|
if (!camel) return "field";
|
|
111
112
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
112
113
|
};
|
|
113
|
-
const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
|
|
114
|
+
const objectFields = (schema, registry, hint, nullishTriState = true, shape = (0, kizunajs_generator.readObjectShape)(schema) ?? {}) => {
|
|
114
115
|
const fields = [];
|
|
115
116
|
const seen = /* @__PURE__ */ new Map();
|
|
116
117
|
for (const [key, value] of Object.entries(shape)) {
|
|
117
118
|
const result = mapType(value, registry, `${hint}${(0, kizunajs_generator.toPascalCase)(key)}`);
|
|
119
|
+
const nullish = nullishTriState && result.nullish === true;
|
|
120
|
+
if (nullish) registry.usesNullish = true;
|
|
118
121
|
const deprecation = (0, kizunajs_generator.readDeprecation)(value);
|
|
119
122
|
const swiftName = propertyName(key, registry.camelCaseProperties);
|
|
120
123
|
const previousWireName = seen.get(swiftName);
|
|
@@ -123,8 +126,10 @@ const objectFields = (schema, registry, hint, shape = (0, kizunajs_generator.rea
|
|
|
123
126
|
fields.push({
|
|
124
127
|
name: swiftName,
|
|
125
128
|
wireName: key,
|
|
126
|
-
type: result.expression,
|
|
127
|
-
optional: result.optional,
|
|
129
|
+
type: nullish ? `Nullish<${result.expression.replace(/\?$/, "")}>` : result.expression,
|
|
130
|
+
optional: result.optional && !nullish,
|
|
131
|
+
requiredNullable: result.requiredNullable,
|
|
132
|
+
nullish,
|
|
128
133
|
description: (0, kizunajs_generator.readMetaDescription)(value),
|
|
129
134
|
isFile: result.isFile,
|
|
130
135
|
deprecated: deprecation !== void 0,
|
|
@@ -236,6 +241,8 @@ const mapUnbrandedType = (schema, registry, hint) => {
|
|
|
236
241
|
return {
|
|
237
242
|
expression: innerResult.expression.endsWith("?") ? innerResult.expression : `${innerResult.expression}?`,
|
|
238
243
|
optional: true,
|
|
244
|
+
requiredNullable: nullable && !optional,
|
|
245
|
+
nullish: nullable && optional,
|
|
239
246
|
isFile: innerResult.isFile
|
|
240
247
|
};
|
|
241
248
|
}
|
|
@@ -417,9 +424,13 @@ const sanitizeCaseName = (literal) => {
|
|
|
417
424
|
if (/^[0-9]/.test(cleaned)) return `_${cleaned}`;
|
|
418
425
|
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
419
426
|
};
|
|
420
|
-
|
|
421
|
-
|
|
427
|
+
/**
|
|
428
|
+
* A JSON body keeps `.nullish()` as `Nullish`. A query, path, header or form field has no null, so it takes an optional.
|
|
429
|
+
*/
|
|
430
|
+
const collectObjectFields = (schema, registry, hint, json = false) => {
|
|
431
|
+
return objectFields(schema, registry, hint, json);
|
|
422
432
|
};
|
|
433
|
+
const hasDefault = (field) => field.optional && field.requiredNullable !== true || field.nullish === true;
|
|
423
434
|
const objectFieldCount = (schema) => {
|
|
424
435
|
return Object.keys((0, kizunajs_generator.readObjectShape)(schema) ?? {}).length;
|
|
425
436
|
};
|
|
@@ -476,7 +487,7 @@ const buildRouteMethod = (routeKey, route, registry, deprecated, deprecationMess
|
|
|
476
487
|
else bodyDescriptor = {
|
|
477
488
|
kind: "json-flat",
|
|
478
489
|
structName,
|
|
479
|
-
flattened: collectObjectFields(route.body, registry, bodyHint),
|
|
490
|
+
flattened: collectObjectFields(route.body, registry, bodyHint, true),
|
|
480
491
|
multipartFields: []
|
|
481
492
|
};
|
|
482
493
|
}
|
|
@@ -696,6 +707,63 @@ const sanitizeEnumCaseName = (value) => {
|
|
|
696
707
|
if (!camel) return "_";
|
|
697
708
|
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
|
|
698
709
|
};
|
|
710
|
+
const NULLISH_TYPE = /^Nullish<(.+)>$/;
|
|
711
|
+
const emitNullish = (writer) => {
|
|
712
|
+
writer.blank();
|
|
713
|
+
writer.docComment("A `.nullish()` field: left out, sent as `null`, or sent with a value.");
|
|
714
|
+
writer.block("public enum Nullish<Wrapped>", () => {
|
|
715
|
+
writer.line("case absent");
|
|
716
|
+
writer.line("case null");
|
|
717
|
+
writer.line("case some(Wrapped)");
|
|
718
|
+
writer.blank();
|
|
719
|
+
writer.block("public var value: Wrapped?", () => {
|
|
720
|
+
writer.line("if case .some(let value) = self { return value }");
|
|
721
|
+
writer.line("return nil");
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
writer.blank();
|
|
725
|
+
writer.line("extension Nullish: Sendable where Wrapped: Sendable {}");
|
|
726
|
+
writer.line("extension Nullish: Equatable where Wrapped: Equatable {}");
|
|
727
|
+
writer.line("extension Nullish: Hashable where Wrapped: Hashable {}");
|
|
728
|
+
writer.blank();
|
|
729
|
+
writer.block("extension Nullish: Encodable where Wrapped: Encodable", () => {
|
|
730
|
+
writer.block("public func encode(to encoder: Encoder) throws", () => {
|
|
731
|
+
writer.line("var container = encoder.singleValueContainer()");
|
|
732
|
+
writer.line("switch self {");
|
|
733
|
+
writer.line("case .absent, .null: try container.encodeNil()");
|
|
734
|
+
writer.line("case .some(let value): try container.encode(value)");
|
|
735
|
+
writer.line("}");
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
writer.blank();
|
|
739
|
+
writer.block("extension Nullish: Decodable where Wrapped: Decodable", () => {
|
|
740
|
+
writer.block("public init(from decoder: Decoder) throws", () => {
|
|
741
|
+
writer.line("let container = try decoder.singleValueContainer()");
|
|
742
|
+
writer.line("self = container.decodeNil() ? .null : .some(try container.decode(Wrapped.self))");
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
writer.blank();
|
|
746
|
+
writer.block("extension KeyedEncodingContainer", () => {
|
|
747
|
+
writer.block("public mutating func encode<Wrapped: Encodable>(_ value: Nullish<Wrapped>, forKey key: Key) throws", () => {
|
|
748
|
+
writer.line("switch value {");
|
|
749
|
+
writer.line("case .absent: return");
|
|
750
|
+
writer.line("case .null: try encodeNil(forKey: key)");
|
|
751
|
+
writer.line("case .some(let wrapped): try encode(wrapped, forKey: key)");
|
|
752
|
+
writer.line("}");
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
writer.blank();
|
|
756
|
+
writer.block("extension KeyedDecodingContainer", () => {
|
|
757
|
+
writer.block("public func decode<Wrapped: Decodable>(_ type: Nullish<Wrapped>.Type, forKey key: Key) throws -> Nullish<Wrapped>", () => {
|
|
758
|
+
writer.line("guard contains(key) else { return .absent }");
|
|
759
|
+
writer.line("return try decodeNil(forKey: key) ? .null : .some(try decode(Wrapped.self, forKey: key))");
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
const defaultArgument = (field) => {
|
|
764
|
+
if (!hasDefault(field)) return "";
|
|
765
|
+
return field.nullish ? " = .absent" : " = nil";
|
|
766
|
+
};
|
|
699
767
|
const optionalize = (type, optional) => {
|
|
700
768
|
if (!optional) return type;
|
|
701
769
|
return type.endsWith("?") ? type : `${type}?`;
|
|
@@ -710,7 +778,7 @@ const emitNamedFactory = (writer, factoryName, fields, buildExpression) => {
|
|
|
710
778
|
}
|
|
711
779
|
const params = fields.map((field) => {
|
|
712
780
|
const typeExpression = optionalize(field.type, field.optional);
|
|
713
|
-
const defaultPart = field
|
|
781
|
+
const defaultPart = defaultArgument(field);
|
|
714
782
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
715
783
|
});
|
|
716
784
|
const args = fields.map((field) => `${field.name}: ${escapeKeyword(field.name)}`).join(", ");
|
|
@@ -733,7 +801,7 @@ const emitMemberwiseInit = (writer, fields, assignmentTarget = (field) => escape
|
|
|
733
801
|
}
|
|
734
802
|
const params = fields.map((field) => {
|
|
735
803
|
const typeExpression = optionalize(field.type, field.optional);
|
|
736
|
-
const defaultPart = field
|
|
804
|
+
const defaultPart = defaultArgument(field);
|
|
737
805
|
return `${escapeKeyword(field.name)}: ${typeExpression}${defaultPart}`;
|
|
738
806
|
});
|
|
739
807
|
if (params.length === 1) {
|
|
@@ -851,6 +919,8 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
851
919
|
const hasFile = type.fields.some((field) => field.isFile);
|
|
852
920
|
const conformances = hasFile ? "Sendable, Equatable" : "Codable, Sendable, Equatable";
|
|
853
921
|
const resolveOwnedType = (raw) => {
|
|
922
|
+
const nullish = NULLISH_TYPE.exec(raw);
|
|
923
|
+
if (nullish) return `Nullish<${resolveOwnedType(nullish[1])}>`;
|
|
854
924
|
const optional = raw.endsWith("?");
|
|
855
925
|
const stripped = optional ? raw.slice(0, -1) : raw;
|
|
856
926
|
const isArray = stripped.startsWith("[") && stripped.endsWith("]");
|
|
@@ -867,7 +937,9 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
867
937
|
...field,
|
|
868
938
|
type: resolveOwnedType(field.type)
|
|
869
939
|
}));
|
|
870
|
-
const
|
|
940
|
+
const needsEncode = !hasFile && type.fields.some((field) => field.requiredNullable === true);
|
|
941
|
+
const needsCodingKeys = needsEncode || !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
|
|
942
|
+
const storageName = (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name);
|
|
871
943
|
writer.block(`public struct ${type.name}: ${conformances}`, () => {
|
|
872
944
|
emitOwnedTypes(writer, lookupName, context, ownedTypeMap, ownedTypeLookup);
|
|
873
945
|
for (const field of type.fields) {
|
|
@@ -890,8 +962,18 @@ const emitStruct = (writer, type, context, ownedTypeMap, ownedTypeLookup, regist
|
|
|
890
962
|
else writer.line(`case ${escapeKeyword(field.name)} = ${stringLiteral(field.wireName)}`);
|
|
891
963
|
});
|
|
892
964
|
}
|
|
965
|
+
if (needsEncode) {
|
|
966
|
+
writer.blank();
|
|
967
|
+
writer.block("public func encode(to encoder: Encoder) throws", () => {
|
|
968
|
+
writer.line("var container = encoder.container(keyedBy: CodingKeys.self)");
|
|
969
|
+
for (const field of type.fields) {
|
|
970
|
+
const encodeCall = field.optional && field.requiredNullable !== true ? "encodeIfPresent" : "encode";
|
|
971
|
+
writer.line(`try container.${encodeCall}(${storageName(field)}, forKey: .${storageName(field)})`);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
893
975
|
writer.blank();
|
|
894
|
-
emitMemberwiseInit(writer, adjustedFields,
|
|
976
|
+
emitMemberwiseInit(writer, adjustedFields, storageName);
|
|
895
977
|
});
|
|
896
978
|
};
|
|
897
979
|
/**
|
|
@@ -929,7 +1011,7 @@ const emitDiscriminatedEnum = (writer, type, context, registryName, ownedTypeMap
|
|
|
929
1011
|
if (!payloadStruct) continue;
|
|
930
1012
|
const isDiscriminator = (field) => field.wireName === type.discriminator || field.name === type.discriminator;
|
|
931
1013
|
const factoryParams = payloadStruct.fields.filter((field) => !isDiscriminator(field)).map((field) => {
|
|
932
|
-
const defaultPart = field
|
|
1014
|
+
const defaultPart = defaultArgument(field);
|
|
933
1015
|
const fieldType = (ownedTypeMap && relativeOwnedPath(field.type, ownedTypeMap, lookupName)) ?? resolveType(field.type, void 0, context);
|
|
934
1016
|
return `${escapeKeyword(field.name)}: ${optionalize(fieldType, field.optional)}${defaultPart}`;
|
|
935
1017
|
}).join(", ");
|
|
@@ -1027,6 +1109,8 @@ const dictionarySeparator = (inner) => {
|
|
|
1027
1109
|
};
|
|
1028
1110
|
const resolveType = (typeName, currentOperation, context, scope = "actor") => {
|
|
1029
1111
|
const { operationTypeMap, namespaceName, clientName, fileLevelTypeNames } = context;
|
|
1112
|
+
const nullish = NULLISH_TYPE.exec(typeName);
|
|
1113
|
+
if (nullish) return `Nullish<${resolveType(nullish[1], currentOperation, context, scope)}>`;
|
|
1030
1114
|
const optional = typeName.endsWith("?");
|
|
1031
1115
|
const base = optional ? typeName.slice(0, -1) : typeName;
|
|
1032
1116
|
if (SWIFT_PRIMITIVE_TYPES.has(base)) return typeName;
|
|
@@ -1652,13 +1736,13 @@ const methodGroups = (method, context) => {
|
|
|
1652
1736
|
varName: "query",
|
|
1653
1737
|
type: groupTypeRef(operationName, "Query", context),
|
|
1654
1738
|
factory: "query",
|
|
1655
|
-
required: method.query.some((field) => !field
|
|
1739
|
+
required: method.query.some((field) => !hasDefault(field))
|
|
1656
1740
|
});
|
|
1657
1741
|
if (method.headers.length > 0) groups.push({
|
|
1658
1742
|
varName: "headers",
|
|
1659
1743
|
type: groupTypeRef(operationName, "Headers", context),
|
|
1660
1744
|
factory: "headers",
|
|
1661
|
-
required: method.headers.some((field) => !field
|
|
1745
|
+
required: method.headers.some((field) => !hasDefault(field))
|
|
1662
1746
|
});
|
|
1663
1747
|
return [...groups.filter((group) => group.required), ...groups.filter((group) => !group.required)];
|
|
1664
1748
|
};
|
|
@@ -1874,9 +1958,15 @@ const emitBodyEncoding = (writer, method, context, receiver = "") => {
|
|
|
1874
1958
|
writer.line("var multipart = Kizuna.MultipartBuilder()");
|
|
1875
1959
|
for (const field of body.multipartFields) {
|
|
1876
1960
|
const flattenedField = body.flattened.find((candidate) => candidate.name === field.name);
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1961
|
+
const isFile = field.isFile || flattenedField?.isFile === true;
|
|
1962
|
+
const append = (accessor) => {
|
|
1963
|
+
if (isFile) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
|
|
1964
|
+
else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
|
|
1965
|
+
};
|
|
1966
|
+
if (flattenedField?.optional === true) writer.block(`if let value = body.${escapeKeyword(field.name)}`, () => {
|
|
1967
|
+
append("value");
|
|
1968
|
+
});
|
|
1969
|
+
else append(`body.${escapeKeyword(field.name)}`);
|
|
1880
1970
|
}
|
|
1881
1971
|
writer.line("request.httpBody = multipart.finalize()");
|
|
1882
1972
|
writer.line("request.setValue(multipart.contentType, forHTTPHeaderField: \"Content-Type\")");
|
|
@@ -1895,7 +1985,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
1895
1985
|
const allMethods = [...flatMethods, ...groups.flatMap((group) => group.methods)];
|
|
1896
1986
|
const usesMultipart = allMethods.some((method) => method.body?.kind === "multipart");
|
|
1897
1987
|
const contextFields = context.requestContextFields;
|
|
1898
|
-
const contextRequired = contextFields.some((field) => !field
|
|
1988
|
+
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1899
1989
|
writer.blank();
|
|
1900
1990
|
writer.block(`public final class ${clientName}: Sendable`, () => {
|
|
1901
1991
|
if (contextFields.length > 0) {
|
|
@@ -1904,7 +1994,7 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
1904
1994
|
writer.block("public struct RequestContext: Sendable, Equatable", () => {
|
|
1905
1995
|
for (const field of contextFields) writer.line(`public var ${escapeKeyword(field.name)}: ${field.type}`);
|
|
1906
1996
|
writer.blank();
|
|
1907
|
-
const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${field
|
|
1997
|
+
const initParams = contextFields.map((field) => `${escapeKeyword(field.name)}: ${field.type}${defaultArgument(field)}`).join(", ");
|
|
1908
1998
|
writer.block(`public init(${initParams})`, () => {
|
|
1909
1999
|
for (const field of contextFields) writer.line(`self.${escapeKeyword(field.name)} = ${escapeKeyword(field.name)}`);
|
|
1910
2000
|
});
|
|
@@ -2125,6 +2215,7 @@ const renderSwiftClient = (api, partition, registry, options) => {
|
|
|
2125
2215
|
anyCodable: registry.usesAnyCodable
|
|
2126
2216
|
}, partition, context, typesByOperation);
|
|
2127
2217
|
for (const group of partition.groups) emitSubClientStruct(writer, group, clientName, context);
|
|
2218
|
+
if (registry.usesNullish) emitNullish(writer);
|
|
2128
2219
|
emitKizunaFailureProtocols(writer);
|
|
2129
2220
|
emitKizunaNamespace(writer, {
|
|
2130
2221
|
multipart: usesMultipart,
|
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-SM_Zrp0p.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/swift",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.18",
|
|
4
4
|
"description": "Swift client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@kizunajs/cli": "2.0.0-beta.
|
|
62
|
+
"@kizunajs/cli": "2.0.0-beta.18"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"kizunajs": "2.0.0-beta.
|
|
65
|
+
"kizunajs": "2.0.0-beta.18",
|
|
66
66
|
"zod": "^4.0.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"kizunajs": "2.0.0-beta.
|
|
69
|
+
"kizunajs": "2.0.0-beta.18",
|
|
70
70
|
"@types/node": "^24",
|
|
71
71
|
"tsdown": "^0.21.0",
|
|
72
72
|
"typescript": "^6.0.3",
|