diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 00f88483..08ac4a1e 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -44,7 +44,9 @@ struct TypeMatcher { Self._tryMatchRecursive( for: schema, test: { schema in Self._tryMatchBuiltinNonRecursive(for: schema) }, - matchedArrayHandler: { elementType in elementType.asArray }, + matchedArrayHandler: { elementType, nullableItems in + nullableItems ? elementType.asOptional.asArray : elementType.asArray + }, genericArrayHandler: { TypeName.arrayContainer.asUsage } ) } @@ -71,7 +73,9 @@ struct TypeMatcher { guard case let .reference(ref, _) = schema else { return nil } return try TypeAssigner(asSwiftSafeName: asSwiftSafeName).typeName(for: ref).asUsage }, - matchedArrayHandler: { elementType in elementType.asArray }, + matchedArrayHandler: { elementType, nullableItems in + nullableItems ? elementType.asOptional.asArray : elementType.asArray + }, genericArrayHandler: { TypeName.arrayContainer.asUsage } )? .withOptional(isOptional(schema, components: components)) @@ -94,7 +98,7 @@ struct TypeMatcher { guard case .reference = schema else { return false } return true }, - matchedArrayHandler: { elementIsReferenceable in elementIsReferenceable }, + matchedArrayHandler: { elementIsReferenceable, _ in elementIsReferenceable }, genericArrayHandler: { true } ) ?? false } @@ -351,7 +355,7 @@ struct TypeMatcher { private static func _tryMatchRecursive( for schema: JSONSchema.Schema, test: (JSONSchema.Schema) throws -> R?, - matchedArrayHandler: (R) -> R, + matchedArrayHandler: (R, _ nullableItems: Bool) -> R, genericArrayHandler: () -> R ) rethrows -> R? { switch schema { @@ -365,7 +369,7 @@ struct TypeMatcher { genericArrayHandler: genericArrayHandler ) else { return nil } - return matchedArrayHandler(itemsResult) + return matchedArrayHandler(itemsResult, items.nullable) default: return try test(schema) } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index 3d17469d..ac8a417d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -399,6 +399,13 @@ components: type: string additionalProperties: type: integer + ObjectWithOptionalNullableArrayOfNullableItems: + type: object + properties: + foo: + type: [array, null] + items: + type: [string, null] CodeError: type: object properties: diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 66b5e4c9..c3880143 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -619,6 +619,21 @@ public enum Components { try encoder.encodeAdditionalProperties(additionalProperties) } } + /// - Remark: Generated from `#/components/schemas/ObjectWithOptionalNullableArrayOfNullableItems`. + public struct ObjectWithOptionalNullableArrayOfNullableItems: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ObjectWithOptionalNullableArrayOfNullableItems/foo`. + public var foo: [Swift.String?]? + /// Creates a new `ObjectWithOptionalNullableArrayOfNullableItems`. + /// + /// - Parameters: + /// - foo: + public init(foo: [Swift.String?]? = nil) { + self.foo = foo + } + public enum CodingKeys: String, CodingKey { + case foo + } + } /// - Remark: Generated from `#/components/schemas/CodeError`. public struct CodeError: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/CodeError/code`. diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index b64d058a..52ad3b82 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -164,6 +164,66 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasNullableString() throws { + try self.assertSchemasTranslation( + """ + schemas: + MyString: + type: string + """, + // NOTE: We don't generate a typealias to an optional; instead nullable is considered at point of use. + """ + public enum Schemas { + public typealias MyString = Swift.String + } + """ + ) + } + + func testComponentsSchemasArrayWithNullableItems() throws { + try self.assertSchemasTranslation( + """ + schemas: + StringArray: + type: array + items: + type: string + + StringArrayNullableItems: + type: array + items: + type: [string, null] + """, + """ + public enum Schemas { + public typealias StringArray = [Swift.String] + public typealias StringArrayNullableItems = [Swift.String?] + } + """ + ) + } + + func testComponentsSchemasArrayOfRefsOfNullableItems() throws { + try XCTSkipIf(true, "TODO: Still need to propagate nullability through reference at time of use") + try self.assertSchemasTranslation( + """ + schemas: + ArrayOfRefsToNullableItems: + type: array + items: + $ref: '#/components/schemas/NullableString' + NullableString: + type: [string, null] + """, + """ + public enum Schemas { + public typealias ArrayOfRefsToNullableItems = [Components.Schemas.NullableString?] + public typealias NullableString = Swift.String + } + """ + ) + } + func testComponentsSchemasNullableStringProperty() throws { try self.assertSchemasTranslation( """ @@ -179,9 +239,47 @@ final class SnippetBasedReferenceTests: XCTestCase { type: [string, null] fooRequiredNullable: type: [string, null] + + fooOptionalArray: + type: array + items: + type: string + fooRequiredArray: + type: array + items: + type: string + fooOptionalNullableArray: + type: [array, null] + items: + type: string + fooRequiredNullableArray: + type: [array, null] + items: + type: string + + fooOptionalArrayOfNullableItems: + type: array + items: + type: [string, null] + fooRequiredArrayOfNullableItems: + type: array + items: + type: [string, null] + fooOptionalNullableArrayOfNullableItems: + type: [array, null] + items: + type: [string, null] + fooRequiredNullableArrayOfNullableItems: + type: [array, null] + items: + type: [string, null] required: - fooRequired - fooRequiredNullable + - fooRequiredArray + - fooRequiredNullableArray + - fooRequiredArrayOfNullableItems + - fooRequiredNullableArrayOfNullableItems """, """ public enum Schemas { @@ -190,22 +288,54 @@ final class SnippetBasedReferenceTests: XCTestCase { public var fooRequired: Swift.String public var fooOptionalNullable: Swift.String? public var fooRequiredNullable: Swift.String? + public var fooOptionalArray: [Swift.String]? + public var fooRequiredArray: [Swift.String] + public var fooOptionalNullableArray: [Swift.String]? + public var fooRequiredNullableArray: [Swift.String]? + public var fooOptionalArrayOfNullableItems: [Swift.String?]? + public var fooRequiredArrayOfNullableItems: [Swift.String?] + public var fooOptionalNullableArrayOfNullableItems: [Swift.String?]? + public var fooRequiredNullableArrayOfNullableItems: [Swift.String?]? public init( fooOptional: Swift.String? = nil, fooRequired: Swift.String, fooOptionalNullable: Swift.String? = nil, - fooRequiredNullable: Swift.String? = nil + fooRequiredNullable: Swift.String? = nil, + fooOptionalArray: [Swift.String]? = nil, + fooRequiredArray: [Swift.String], + fooOptionalNullableArray: [Swift.String]? = nil, + fooRequiredNullableArray: [Swift.String]? = nil, + fooOptionalArrayOfNullableItems: [Swift.String?]? = nil, + fooRequiredArrayOfNullableItems: [Swift.String?], + fooOptionalNullableArrayOfNullableItems: [Swift.String?]? = nil, + fooRequiredNullableArrayOfNullableItems: [Swift.String?]? = nil ) { self.fooOptional = fooOptional self.fooRequired = fooRequired self.fooOptionalNullable = fooOptionalNullable self.fooRequiredNullable = fooRequiredNullable + self.fooOptionalArray = fooOptionalArray + self.fooRequiredArray = fooRequiredArray + self.fooOptionalNullableArray = fooOptionalNullableArray + self.fooRequiredNullableArray = fooRequiredNullableArray + self.fooOptionalArrayOfNullableItems = fooOptionalArrayOfNullableItems + self.fooRequiredArrayOfNullableItems = fooRequiredArrayOfNullableItems + self.fooOptionalNullableArrayOfNullableItems = fooOptionalNullableArrayOfNullableItems + self.fooRequiredNullableArrayOfNullableItems = fooRequiredNullableArrayOfNullableItems } public enum CodingKeys: String, CodingKey { case fooOptional case fooRequired case fooOptionalNullable case fooRequiredNullable + case fooOptionalArray + case fooRequiredArray + case fooOptionalNullableArray + case fooRequiredNullableArray + case fooOptionalArrayOfNullableItems + case fooRequiredArrayOfNullableItems + case fooOptionalNullableArrayOfNullableItems + case fooRequiredNullableArrayOfNullableItems } } } @@ -213,6 +343,25 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testEncodingDecodingArrayWithNullableItems() throws { + struct MyObject: Codable, Equatable { + let myArray: [String?]? + + var json: String { get throws { try String(data: JSONEncoder().encode(self), encoding: .utf8)! } } + + static func from(json: String) throws -> Self { try JSONDecoder().decode(Self.self, from: Data(json.utf8)) } + } + + for (value, encoding) in [ + (MyObject(myArray: nil), #"{}"#), (MyObject(myArray: []), #"{"myArray":[]}"#), + (MyObject(myArray: ["a"]), #"{"myArray":["a"]}"#), (MyObject(myArray: [nil]), #"{"myArray":[null]}"#), + (MyObject(myArray: ["a", nil]), #"{"myArray":["a",null]}"#), + ] { + XCTAssertEqual(try value.json, encoding) + XCTAssertEqual(try MyObject.from(json: value.json), value) + } + } + func testComponentsSchemasObjectWithInferredProperty() throws { try self.assertSchemasTranslation( ignoredDiagnosticMessages: [