@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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_generator = require("./generator-E0WQFf92.cjs");
2
+ const require_generator = require("./generator-SM_Zrp0p.cjs");
3
3
  let node_fs = require("node:fs");
4
4
  let node_path = require("node:path");
5
5
  let node_util = require("node:util");
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as generateSwiftClient } from "./generator-CxUzLmbK.mjs";
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
- const collectObjectFields = (schema, registry, hint) => {
421
- return objectFields(schema, registry, hint);
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.optional ? " = nil" : "";
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.optional ? " = nil" : "";
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 needsCodingKeys = !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
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, (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name));
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.optional ? " = nil" : "";
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.optional)
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.optional)
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 accessor = `body.${escapeKeyword(field.name)}`;
1878
- if (field.isFile || flattenedField?.isFile === true) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
1879
- else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
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.optional);
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.optional ? " = nil" : ""}`).join(", ");
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
- const collectObjectFields = (schema, registry, hint) => {
421
- return objectFields(schema, registry, hint);
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.optional ? " = nil" : "";
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.optional ? " = nil" : "";
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 needsCodingKeys = !hasFile && type.fields.some((field) => field.name !== field.wireName || SWIFT_KEYWORDS.has(field.name) || field.deprecated === true);
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, (field) => field.deprecated ? deprecatedStorageName(field, type.fields) : escapeKeyword(field.name));
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.optional ? " = nil" : "";
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.optional)
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.optional)
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 accessor = `body.${escapeKeyword(field.name)}`;
1878
- if (field.isFile || flattenedField?.isFile === true) writer.line(`multipart.appendFile(name: ${stringLiteral(field.wireName)}, file: ${accessor})`);
1879
- else writer.line(`multipart.appendField(name: ${stringLiteral(field.wireName)}, value: String(describing: ${accessor}))`);
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.optional);
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.optional ? " = nil" : ""}`).join(", ");
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-E0WQFf92.cjs");
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
@@ -1,4 +1,4 @@
1
- import { n as swiftClientWalk, t as generateSwiftClient } from "./generator-CxUzLmbK.mjs";
1
+ import { n as swiftClientWalk, t as generateSwiftClient } from "./generator-CZr33FgN.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/swift",
3
- "version": "2.0.0-beta.17",
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.17"
62
+ "@kizunajs/cli": "2.0.0-beta.18"
63
63
  },
64
64
  "peerDependencies": {
65
- "kizunajs": "2.0.0-beta.17",
65
+ "kizunajs": "2.0.0-beta.18",
66
66
  "zod": "^4.0.0"
67
67
  },
68
68
  "devDependencies": {
69
- "kizunajs": "2.0.0-beta.17",
69
+ "kizunajs": "2.0.0-beta.18",
70
70
  "@types/node": "^24",
71
71
  "tsdown": "^0.21.0",
72
72
  "typescript": "^6.0.3",