diff --git a/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift b/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift index 9f233ffb..f80c952c 100644 --- a/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift +++ b/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift @@ -27,9 +27,11 @@ /// 1.0 is released.) public enum FeatureFlag: String, Hashable, Codable, CaseIterable { - /// Has to be here until we add more feature flags, otherwise the enum - /// doesn't compile. - case empty + /// Support for `nullable` schemas. + /// + /// A dedicated field in OpenAPI 3.0, a `null` value present in + /// the `types` array in OpenAPI 3.1. + case nullableSchemas } /// A set of enabled feature flags. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift index 4527bbe9..b64a9385 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift @@ -15,4 +15,10 @@ import OpenAPIKit extension FileTranslator { // Add helpers for reading feature flags below. + + /// A Boolean value indicating whether the `nullable` field on schemas + /// should be taken into account. + var supportNullableSchemas: Bool { + config.featureFlags.contains(.nullableSchemas) + } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift index 1743885e..03f2d13a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift @@ -45,6 +45,10 @@ struct TypeAssigner { /// safe to be used as a Swift identifier. var asSwiftSafeName: (String) -> String + /// A Boolean value indicating whether the `nullable` field on schemas + /// should be taken into account. + var supportNullableSchemas: Bool + /// Returns a type name for an OpenAPI-named component type. /// /// A component type is any type in `#/components` in the OpenAPI document. @@ -256,7 +260,10 @@ struct TypeAssigner { // Check if this type can be simply referenced without // creating a new inline type. if let referenceableType = - try TypeMatcher(asSwiftSafeName: asSwiftSafeName) + try TypeMatcher( + asSwiftSafeName: asSwiftSafeName, + supportNullableSchemas: supportNullableSchemas + ) .tryMatchReferenceableType(for: schema) { return referenceableType @@ -452,12 +459,18 @@ extension FileTranslator { /// A configured type assigner. var typeAssigner: TypeAssigner { - TypeAssigner(asSwiftSafeName: swiftSafeName) + TypeAssigner( + asSwiftSafeName: swiftSafeName, + supportNullableSchemas: supportNullableSchemas + ) } /// A configured type matcher. var typeMatcher: TypeMatcher { - TypeMatcher(asSwiftSafeName: swiftSafeName) + TypeMatcher( + asSwiftSafeName: swiftSafeName, + supportNullableSchemas: supportNullableSchemas + ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index f425e87e..fe61d7c1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -20,6 +20,10 @@ struct TypeMatcher { /// safe to be used as a Swift identifier. var asSwiftSafeName: (String) -> String + /// A Boolean value indicating whether the `nullable` field on schemas + /// should be taken into account. + var supportNullableSchemas: Bool + /// Returns the type name of a built-in type that matches the specified /// schema. /// @@ -78,8 +82,11 @@ struct TypeMatcher { guard case let .reference(ref, _) = schema else { return nil } - return try TypeAssigner(asSwiftSafeName: asSwiftSafeName) - .typeName(for: ref).asUsage + return try TypeAssigner( + asSwiftSafeName: asSwiftSafeName, + supportNullableSchemas: supportNullableSchemas + ) + .typeName(for: ref).asUsage }, matchedArrayHandler: { elementType in elementType.asArray @@ -88,7 +95,7 @@ struct TypeMatcher { TypeName.arrayContainer.asUsage } )? - .withOptional(!schema.required) + .withOptional(!schema.required || (supportNullableSchemas && schema.nullable)) } /// Returns a Boolean value that indicates whether the schema diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md index e4be30c6..bc8a2da7 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md @@ -155,7 +155,7 @@ Supported features are always provided on _both_ client and server. - [x] description - [x] format - [ ] default -- [ ] nullable (only in 3.0, removed in 3.1) +- [x] nullable (only in 3.0, removed in 3.1, add `null` in `types` instead) - [x] discriminator - [ ] readOnly - [ ] writeOnly diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 2cc44a15..15c1a64e 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -101,6 +101,102 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasNullableStringProperty() throws { + try self.assertSchemasTranslation( + """ + schemas: + MyObj: + type: object + properties: + fooOptional: + type: string + fooRequired: + type: string + fooOptionalNullable: + type: [string, null] + fooRequiredNullable: + type: [string, null] + required: + - fooRequired + - fooRequiredNullable + """, + """ + public enum Schemas { + public struct MyObj: Codable, Hashable, Sendable { + public var fooOptional: Swift.String? + public var fooRequired: Swift.String + public var fooOptionalNullable: Swift.String? + public var fooRequiredNullable: Swift.String + public init( + fooOptional: Swift.String? = nil, + fooRequired: Swift.String, + fooOptionalNullable: Swift.String? = nil, + fooRequiredNullable: Swift.String + ) { + self.fooOptional = fooOptional + self.fooRequired = fooRequired + self.fooOptionalNullable = fooOptionalNullable + self.fooRequiredNullable = fooRequiredNullable + } + public enum CodingKeys: String, CodingKey { + case fooOptional + case fooRequired + case fooOptionalNullable + case fooRequiredNullable + } + } + } + """ + ) + try self.assertSchemasTranslation( + featureFlags: [.nullableSchemas], + """ + schemas: + MyObj: + type: object + properties: + fooOptional: + type: string + fooRequired: + type: string + fooOptionalNullable: + type: [string, null] + fooRequiredNullable: + type: [string, null] + required: + - fooRequired + - fooRequiredNullable + """, + """ + public enum Schemas { + public struct MyObj: Codable, Hashable, Sendable { + public var fooOptional: Swift.String? + public var fooRequired: Swift.String + public var fooOptionalNullable: Swift.String? + public var fooRequiredNullable: Swift.String? + public init( + fooOptional: Swift.String? = nil, + fooRequired: Swift.String, + fooOptionalNullable: Swift.String? = nil, + fooRequiredNullable: Swift.String? = nil + ) { + self.fooOptional = fooOptional + self.fooRequired = fooRequired + self.fooOptionalNullable = fooOptionalNullable + self.fooRequiredNullable = fooRequiredNullable + } + public enum CodingKeys: String, CodingKey { + case fooOptional + case fooRequired + case fooOptionalNullable + case fooRequiredNullable + } + } + } + """ + ) + } + func testComponentsObjectNoAdditionalProperties() throws { try self.assertSchemasTranslation( """