From 86cff2f16189488ba84f4b3d7dcb7000183a016e Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Sun, 31 Mar 2024 16:45:34 -0500 Subject: [PATCH 1/6] Support `type: 'null'` as OpenAPIValueContainer --- Package.swift | 2 +- .../TypeAssignment/TypeMatcher.swift | 3 ++- .../TypeAssignment/isSchemaSupported.swift | 4 ++-- .../SnippetBasedReferenceTests.swift | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 7b90240a..47166359 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ let package = Package( // Read OpenAPI documents .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.1.2"), - .package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"6.0.0"), + .package(url: "https://github.com/jpsim/Yams", "5.1.0"..<"6.0.0"), // CLI Tool .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 08ac4a1e..5395c501 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -289,6 +289,7 @@ struct TypeMatcher { private static func _tryMatchBuiltinNonRecursive(for schema: JSONSchema.Schema) -> TypeUsage? { let typeName: TypeName switch schema { + case .null(_): typeName = TypeName.valueContainer case .boolean(_): typeName = .swift("Bool") case .number(let core, _): switch core.format { @@ -331,7 +332,7 @@ struct TypeMatcher { // arrays are already recursed-into by _tryMatchTypeRecursive // so just return nil here return nil - case .reference, .not, .all, .any, .one, .null: + case .reference, .not, .all, .any, .one: // never built-in return nil } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift index e4735b0b..e1fb6cc0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift @@ -132,7 +132,7 @@ extension FileTranslator { func isSchemaSupported(_ schema: JSONSchema, referenceStack: inout ReferenceStack) throws -> IsSchemaSupportedResult { switch schema.value { - case .string, .integer, .number, .boolean, + case .null, .string, .integer, .number, .boolean, // We mark any object as supported, even if it // has unsupported properties. // The code responsible for emitting an object is @@ -173,7 +173,7 @@ extension FileTranslator { schemas.filter(\.isReference), referenceStack: &referenceStack ) - case .not, .null: return .unsupported(reason: .schemaType, schema: schema) + case .not: return .unsupported(reason: .schemaType, schema: schema) } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 52ad3b82..dbe67a16 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -224,6 +224,25 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasNull() throws { + try self.assertSchemasTranslation( + """ + schemas: + Null: + type: "null" + NullArray: + type: array + items: + $ref: "#/components/schemas/Null" + """, + """ + public enum Schemas { + public typealias Null = OpenAPIRuntime.OpenAPIValueContainer + public typealias NullArray = [Components.Schemas.Null] + } + """) + } + func testComponentsSchemasNullableStringProperty() throws { try self.assertSchemasTranslation( """ From 5ed951fa4f7e3b2706e9b463f945ca0fa633ca4d Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Sun, 31 Mar 2024 21:39:04 -0500 Subject: [PATCH 2/6] yaml: null != 'null' --- .../Development/Handling-nullable-schemas.md | 4 +-- .../Resources/Docs/petstore.yaml | 4 +-- .../SnippetBasedReferenceTests.swift | 30 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md b/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md index 1fe8b4ea..0d0aa806 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md @@ -37,7 +37,7 @@ For example: ```yaml MyOptionalString: - type: [string, null] + type: [string, 'null'] ``` > The rule can be summarized as: `schema is optional := schema is nullable`, where being `nullable` is represented slightly differently between the two JSON Schema versions. @@ -75,7 +75,7 @@ MyPerson: name: type: string age: - type: [integer, null] + type: [integer, 'null'] required: - name - age # even though required, the nullability of the schema "wins" diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index ac8a417d..ecfe6c37 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -403,9 +403,9 @@ components: type: object properties: foo: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] CodeError: type: object properties: diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index dbe67a16..4b308362 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -192,7 +192,7 @@ final class SnippetBasedReferenceTests: XCTestCase { StringArrayNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] """, """ public enum Schemas { @@ -213,7 +213,7 @@ final class SnippetBasedReferenceTests: XCTestCase { items: $ref: '#/components/schemas/NullableString' NullableString: - type: [string, null] + type: [string, 'null'] """, """ public enum Schemas { @@ -255,9 +255,9 @@ final class SnippetBasedReferenceTests: XCTestCase { fooRequired: type: string fooOptionalNullable: - type: [string, null] + type: [string, 'null'] fooRequiredNullable: - type: [string, null] + type: [string, 'null'] fooOptionalArray: type: array @@ -268,30 +268,30 @@ final class SnippetBasedReferenceTests: XCTestCase { items: type: string fooOptionalNullableArray: - type: [array, null] + type: [array, 'null'] items: type: string fooRequiredNullableArray: - type: [array, null] + type: [array, 'null'] items: type: string fooOptionalArrayOfNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] fooRequiredArrayOfNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] fooOptionalNullableArrayOfNullableItems: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] fooRequiredNullableArrayOfNullableItems: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] required: - fooRequired - fooRequiredNullable @@ -499,7 +499,7 @@ final class SnippetBasedReferenceTests: XCTestCase { MyRequiredString: type: string MyNullableString: - type: [string, null] + type: [string, 'null'] MyObject: type: object properties: @@ -2692,7 +2692,7 @@ final class SnippetBasedReferenceTests: XCTestCase { content: application/json: schema: - type: [string, null] + type: [string, 'null'] responses: default: description: Response @@ -2848,7 +2848,7 @@ final class SnippetBasedReferenceTests: XCTestCase { content: application/json: schema: - type: [string, null] + type: [string, 'null'] responses: default: description: Response From f285dab334556aa19e783ff1a9cd19ba1d486c63 Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Mon, 1 Apr 2024 11:31:19 -0500 Subject: [PATCH 3/6] support references in TypeMatcher isOptional --- .../TypeAssignment/TypeMatcher.swift | 71 +++++++++++++++++-- .../TypeAssignment/Test_TypeMatcher.swift | 18 +++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 5395c501..18707774 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -247,10 +247,29 @@ struct TypeMatcher { /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is optional, `false` otherwise. func isOptional(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(schema, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the schema is optional. + /// - Parameters: + /// - schema: The schema to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ schema: JSONSchema, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { if schema.nullable || !schema.required { return true } - guard case .reference(let ref, _) = schema.value else { return false } - let targetSchema = try components.lookup(ref) - return try isOptional(targetSchema, components: components) + switch schema.value { + case .null(_): + return true + case .reference(let ref, _): + return try isOptional(ref, components: components, cache: &cache) + case .one(of: let schemas, core: _): + return try schemas.contains(where: { try isOptional($0, components: components, cache: &cache) }) + default: + return schema.nullable + } } /// Returns a Boolean value indicating whether the schema is optional. @@ -260,16 +279,56 @@ struct TypeMatcher { /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is optional, `false` otherwise. func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(schema, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the schema is optional. + /// - Parameters: + /// - schema: The schema to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { guard let schema else { // A nil unresolved schema represents a non-optional fragment. return false } switch schema { case .a(let ref): - let targetSchema = try components.lookup(ref) - return try isOptional(targetSchema, components: components) - case .b(let schema): return try isOptional(schema, components: components) + return try isOptional(ref.jsonReference, components: components, cache: &cache) + case .b(let schema): return try isOptional(schema, components: components, cache: &cache) + } + } + + /// Returns a Boolean value indicating whether the referenced schema is optional. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ ref: JSONReference, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(ref, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the referenced schema is optional. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ ref: JSONReference, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { + if let result = cache[ref] { + return result } + let targetSchema = try components.lookup(ref) + cache[ref] = false // Pre-cache to treat directly recursive types as non-nullable. + let result = try isOptional(targetSchema, components: components, cache: &cache) + cache[ref] = result + return result } // MARK: - Private diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index a7e0b8e3..3d937c9c 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -216,6 +216,8 @@ final class Test_TypeMatcher: Test_Core { } static let optionalTestCases: [(JSONSchema, Bool)] = [ + // Explicit null. + (.null(), true), // A required string. (.string, false), (.string(required: true, nullable: false), false), @@ -227,10 +229,26 @@ final class Test_TypeMatcher: Test_Core { // A reference pointing to a required schema. (.reference(.component(named: "RequiredString")), false), (.reference(.component(named: "NullableString")), true), + + // Unknown type. + (.fragment(), false), + (.fragment(nullable: true), true), + + // References. + (.reference(.component(named: "List")), true), + (.reference(.component(named: "Loop")), false), ] func testOptionalSchemas() throws { let components = OpenAPI.Components(schemas: [ "RequiredString": .string, "NullableString": .string(nullable: true), + // Singlely linked list where null is an empty list. + "List": .one(of: [ + .null(), + .object(properties: ["next": .reference(.component(named: "List"), + required: true)])]), + // A non-empty circular linked list. + "Loop": .object(properties: ["next": .reference(.component(named: "Loop"), + required: true)]), ]) for (schema, expectedIsOptional) in Self.optionalTestCases { let actualIsOptional = try typeMatcher.isOptional(schema, components: components) From d01e68a5a048b3bd056d59c7cb92e6da1dac109a Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Mon, 1 Apr 2024 11:41:43 -0500 Subject: [PATCH 4/6] Add testTypeNameForReferenceProperties --- .../TypeAssignment/Test_TypeAssigner.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift index c76ec4c5..c67e34d9 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift @@ -104,6 +104,27 @@ class Test_TypeAssigner: Test_Core { } } + func testTypeNameForReferenceProperties() throws { + let parent = TypeName(swiftKeyPath: ["MyType"]) + let components: OpenAPI.Components = .init(schemas: [ + "SomeString": .string(), + "MaybeString": .one(of: [.reference(.component(named: "SomeString")), .null()]), + ]) + func assertTypeName(_ property: String, _ schema: JSONSchema, _ typeName: String, file: StaticString = #file, line: UInt = #line) throws { + let actual = try typeAssigner.typeUsage( + forObjectPropertyNamed: property, + withSchema: schema, + components: components, + inParent: parent + ).fullyQualifiedSwiftName + XCTAssertEqual(typeName, actual, file: file, line: line) + } + try assertTypeName("someString", .reference(.component(named: "SomeString")), "Components.Schemas.SomeString") + try assertTypeName("maybeString", .reference(.component(named: "MaybeString")), "Components.Schemas.MaybeString") + try assertTypeName("optionalSomeString", .reference(.component(named: "SomeString"), required: false), "Components.Schemas.SomeString?") + try assertTypeName("optionalMaybeString", .reference(.component(named: "MaybeString"), required: false), "Components.Schemas.MaybeString") + } + func testContentSwiftName() throws { let nameMaker = makeTranslator().typeAssigner.contentSwiftName let cases: [(String, String)] = [ From 1ed3e18ac16e095b5c84cfbe92d11a4a4d3be41b Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Mon, 1 Apr 2024 16:18:39 -0500 Subject: [PATCH 5/6] Improve handling of Optional referenced types. --- .../translateTypealias.swift | 2 +- .../Parameters/translateParameter.swift | 2 +- .../Responses/translateResponseHeader.swift | 2 +- .../TypeAssignment/TypeAssigner.swift | 2 +- .../TypeAssignment/TypeMatcher.swift | 21 ++++++++++++++++++- .../SnippetBasedReferenceTests.swift | 8 +++---- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift index f6c2d48a..63670507 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift @@ -28,7 +28,7 @@ extension FileTranslator { let typealiasDescription = TypealiasDescription( accessModifier: config.access, name: typeName.shortSwiftName, - existingType: .init(existingTypeUsage.withOptional(false)) + existingType: .init(existingTypeUsage) ) let typealiasComment: Comment? = typeName.docCommentWithUserDescription(userDescription) return .commentable(typealiasComment, .typealias(typealiasDescription)) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index c422bec0..dfc40f98 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -70,7 +70,7 @@ extension TypesFileTranslator { let decl = try translateSchema( typeName: typeName, schema: parameter.schema, - overrides: .init(isOptional: !parameter.required, userDescription: parameter.parameter.description) + overrides: .init(userDescription: parameter.parameter.description) ) return decl } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 229d575a..707761d6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -114,7 +114,7 @@ extension TypesFileTranslator { let decl = try translateSchema( typeName: typeName, schema: header.schema, - overrides: .init(isOptional: header.isOptional, userDescription: header.header.description) + overrides: .init(userDescription: header.header.description) ) return decl } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift index 72e18f8a..5fc3a8ec 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift @@ -345,7 +345,7 @@ struct TypeAssigner { swiftComponent: asSwiftSafeName(originalName) + suffix, jsonComponent: jsonReferenceComponentOverride ?? originalName ) - .asUsage.withOptional(try typeMatcher.isOptional(schema, components: components)) + .asUsage.withOptional(try typeMatcher.isOptionalRoot(schema, components: components)) } /// Returns a type name for a reusable component. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 18707774..0671774a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -78,7 +78,7 @@ struct TypeMatcher { }, genericArrayHandler: { TypeName.arrayContainer.asUsage } )? - .withOptional(isOptional(schema, components: components)) + .withOptional(isOptionalRoot(schema, components: components)) } /// Returns a Boolean value that indicates whether the schema @@ -331,6 +331,25 @@ struct TypeMatcher { return result } + /// Returns a Boolean value indicating whether the schema is optional at the root of any references. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is an optional root, `false` otherwise. + func isOptionalRoot(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool { + let directlyOptional = schema.nullable || !schema.required + switch schema.value { + case .null(_): + return true + case .reference(let ref, _): + let indirectlyOptional = try isOptional(ref, components: components) + return directlyOptional && !indirectlyOptional + default: + return directlyOptional + } + } + // MARK: - Private /// Returns the type name of a built-in type that matches the specified diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 4b308362..da4af56d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -237,7 +237,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """, """ public enum Schemas { - public typealias Null = OpenAPIRuntime.OpenAPIValueContainer + public typealias Null = OpenAPIRuntime.OpenAPIValueContainer? public typealias NullArray = [Components.Schemas.Null] } """) @@ -520,17 +520,17 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public enum Schemas { public typealias MyRequiredString = Swift.String - public typealias MyNullableString = Swift.String + public typealias MyNullableString = Swift.String? public struct MyObject: Codable, Hashable, Sendable { public var id: Swift.Int64 public var alias: Swift.String? public var requiredString: Components.Schemas.MyRequiredString - public var nullableString: Components.Schemas.MyNullableString? + public var nullableString: Components.Schemas.MyNullableString public init( id: Swift.Int64, alias: Swift.String? = nil, requiredString: Components.Schemas.MyRequiredString, - nullableString: Components.Schemas.MyNullableString? = nil + nullableString: Components.Schemas.MyNullableString ) { self.id = id self.alias = alias From ddfd63df1d52f720f5e9c12a28f8cd7933151618 Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Sun, 31 Mar 2024 17:25:34 -0500 Subject: [PATCH 6/6] Support {oneOf: [{type: 'null'}, ...]} as optional Fixes #513 --- .../translateAllAnyOneOf.swift | 18 +++++++++---- .../CommonTranslations/translateSchema.swift | 9 +++---- .../TypeAssignment/TypeMatcher.swift | 15 +++++++++++ .../SnippetBasedReferenceTests.swift | 25 +++++++++++++++++++ 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index ad7a1b63..9b91c460 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -38,7 +38,7 @@ extension TypesFileTranslator { /// - Throws: An error if there is an issue during translation. /// - Returns: A declaration representing the translated allOf/anyOf structure. func translateAllOrAnyOf(typeName: TypeName, openAPIDescription: String?, type: AllOrAnyOf, schemas: [JSONSchema]) - throws -> Declaration + throws -> [Declaration] { let properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)] = try schemas.enumerated() .map { index, schema in @@ -107,7 +107,7 @@ extension TypesFileTranslator { properties: propertyValues ) ) - return structDecl + return [structDecl] } /// Returns a declaration for a oneOf schema. @@ -128,7 +128,7 @@ extension TypesFileTranslator { openAPIDescription: String?, discriminator: OpenAPI.Discriminator?, schemas: [JSONSchema] - ) throws -> Declaration { + ) throws -> [Declaration] { let cases: [(String, [String]?, Bool, Comment?, TypeUsage, [Declaration])] if let discriminator { // > When using the discriminator, inline schemas will not be considered. @@ -148,7 +148,15 @@ extension TypesFileTranslator { return (caseName, mappedType.rawNames, true, comment, mappedType.typeName.asUsage, []) } } else { - cases = try schemas.enumerated() + let (schemas, nullSchemas) = schemas.partitioned(by: { typeMatcher.isNull($0) }) + if schemas.count == 1, nullSchemas.count > 0, let schema = schemas.first { + return try translateSchema( + typeName: typeName, + schema: schema, + overrides: .init(isOptional: true)) + } + cases = try schemas + .enumerated() .map { index, schema in let key = "case\(index+1)" let childType = try typeAssigner.typeUsage( @@ -242,6 +250,6 @@ extension TypesFileTranslator { conformances: Constants.ObjectStruct.conformances, members: caseDecls + codingKeysDecls + [decoder, encoder] ) - return .commentable(comment, enumDecl) + return [.commentable(comment, enumDecl)] } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index f7668e4f..a9f69ef4 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -141,29 +141,26 @@ extension TypesFileTranslator { arrayContext: arrayContext ) case let .all(of: schemas, core: coreContext): - let allOfDecl = try translateAllOrAnyOf( + return try translateAllOrAnyOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, type: .allOf, schemas: schemas ) - return [allOfDecl] case let .any(of: schemas, core: coreContext): - let anyOfDecl = try translateAllOrAnyOf( + return try translateAllOrAnyOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, type: .anyOf, schemas: schemas ) - return [anyOfDecl] case let .one(of: schemas, core: coreContext): - let oneOfDecl = try translateOneOf( + return try translateOneOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, discriminator: coreContext.discriminator, schemas: schemas ) - return [oneOfDecl] default: return [] } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 0671774a..f4ef6f4b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -350,6 +350,21 @@ struct TypeMatcher { } } + /// Returns a Boolean value indicating whether the schema admits only explicit null values. + /// - Parameters: + /// - schema: The schema to check. + /// - Returns: `true` if the schema admits only explicit null values, `false` otherwise. + func isNull(_ schema: JSONSchema) -> Bool { + switch schema.value { + case .null(_): + return true + case let .fragment(core): + return core.format.jsonType == .null + default: + return false + } + } + // MARK: - Private /// Returns the type name of a built-in type that matches the specified diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index da4af56d..02f8be7c 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1761,6 +1761,31 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testOneOfRefOrNull() throws { + try self.assertSchemasTranslation( + """ + schemas: + SomeString: + type: string + NullableRef: + oneOf: + - $ref: '#/components/schemas/SomeString' + - type: 'null' + ArrayOfNullableRefs: + type: array + items: + $ref: '#/components/schemas/NullableRef' + """, + """ + public enum Schemas { + public typealias SomeString = Swift.String + public typealias NullableRef = Components.Schemas.SomeString? + public typealias ArrayOfNullableRefs = [Components.Schemas.NullableRef] + } + """ + ) + } + func testComponentsResponsesResponseNoBody() throws { try self.assertResponsesTranslation( """