From ddfd63df1d52f720f5e9c12a28f8cd7933151618 Mon Sep 17 00:00:00 2001 From: Brandon Bloom Date: Sun, 31 Mar 2024 17:25:34 -0500 Subject: [PATCH] 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( """