Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,10 @@ extension JSONSchema {

/// Returns a human-readable description of the schema.
var prettyDescription: String { value.prettyDescription }

/// A Boolean value indicating whether the schema is the `null` type.
var isNullType: Bool {
if case .null = value { return true }
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ extension TypesFileTranslator {
func translateAllOrAnyOf(typeName: TypeName, openAPIDescription: String?, type: AllOrAnyOf, schemas: [JSONSchema])
throws -> Declaration
{
// A `null` branch only marks the composition as nullable, it has no member.
let schemas = schemas.filter { !$0.isNullType }
let properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)] = try schemas.enumerated()
.map { index, schema in
let key = "value\(index+1)"
Expand Down Expand Up @@ -129,6 +131,8 @@ extension TypesFileTranslator {
discriminator: OpenAPI.Discriminator?,
schemas: [JSONSchema]
) throws -> Declaration {
// A `null` branch only marks the oneOf as nullable, it has no case.
let schemas = schemas.filter { !$0.isNullType }
let cases: [(String, [String]?, Bool, Comment?, TypeUsage, [Declaration])]
if let discriminator {
// > When using the discriminator, inline schemas will not be considered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ extension TypesFileTranslator {
.`typealias`(
accessModifier: config.access,
name: typeName.shortSwiftName,
existingType: .init(elementType.typeName.asUsage.asArray)
// Preserve the element's optionality, like builtin `[Element?]`.
existingType: .init(elementType.asArray)
)
)
return inline + [arrayDecl]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,16 @@ extension FileTranslator {
// an array is supported iff its element schema is supported
return try isSchemaSupported(items, referenceStack: &referenceStack)
case .all(of: let schemas, _):
// A `null` branch only marks the composition as nullable, ignore it here.
let schemas = schemas.filter { !$0.isNullType }
guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) }
return try areSchemasSupported(schemas, referenceStack: &referenceStack)
case .any(of: let schemas, _):
let schemas = schemas.filter { !$0.isNullType }
guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) }
return try areSchemasSupported(schemas, referenceStack: &referenceStack)
case .one(of: let schemas, let context):
let schemas = schemas.filter { !$0.isNullType }
guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) }
guard context.discriminator != nil else {
return try areSchemasSupported(schemas, referenceStack: &referenceStack)
Expand Down
298 changes: 298 additions & 0 deletions Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,304 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testComponentsSchemasOneOfWithNull_singleRef() throws {
try self.assertSchemasTranslation(
"""
schemas:
B:
type: object
required: [c]
properties:
c:
type: string
A:
type: object
required: [b]
properties:
b:
oneOf:
- $ref: '#/components/schemas/B'
- type: 'null'
""",
"""
public enum Schemas {
public struct B: Codable, Hashable, Sendable {
public var c: Swift.String
public init(c: Swift.String) {
self.c = c
}
public enum CodingKeys: String, CodingKey {
case c
}
}
public struct A: Codable, Hashable, Sendable {
@frozen public enum bPayload: Codable, Hashable, Sendable {
case B(Components.Schemas.B)
public init(from decoder: any Swift.Decoder) throws {
var errors: [any Swift.Error] = []
do {
self = .B(try .init(from: decoder))
return
} catch {
errors.append(error)
}
throw Swift.DecodingError.failedToDecodeOneOfSchema(
type: Self.self,
codingPath: decoder.codingPath,
errors: errors
)
}
public func encode(to encoder: any Swift.Encoder) throws {
switch self {
case let .B(value):
try value.encode(to: encoder)
}
}
}
public var b: Components.Schemas.A.bPayload?
public init(b: Components.Schemas.A.bPayload? = nil) {
self.b = b
}
public enum CodingKeys: String, CodingKey {
case b
}
}
}
"""
)
}

func testComponentsSchemasOneOfWithNull_multiRef() throws {
try self.assertSchemasTranslation(
"""
schemas:
B1:
type: object
required: [c]
properties:
c:
type: string
B2:
type: object
required: [d]
properties:
d:
type: string
A:
type: object
required: [b]
properties:
b:
oneOf:
- $ref: '#/components/schemas/B1'
- $ref: '#/components/schemas/B2'
- type: 'null'
""",
"""
public enum Schemas {
public struct B1: Codable, Hashable, Sendable {
public var c: Swift.String
public init(c: Swift.String) {
self.c = c
}
public enum CodingKeys: String, CodingKey {
case c
}
}
public struct B2: Codable, Hashable, Sendable {
public var d: Swift.String
public init(d: Swift.String) {
self.d = d
}
public enum CodingKeys: String, CodingKey {
case d
}
}
public struct A: Codable, Hashable, Sendable {
@frozen public enum bPayload: Codable, Hashable, Sendable {
case B1(Components.Schemas.B1)
case B2(Components.Schemas.B2)
public init(from decoder: any Swift.Decoder) throws {
var errors: [any Swift.Error] = []
do {
self = .B1(try .init(from: decoder))
return
} catch {
errors.append(error)
}
do {
self = .B2(try .init(from: decoder))
return
} catch {
errors.append(error)
}
throw Swift.DecodingError.failedToDecodeOneOfSchema(
type: Self.self,
codingPath: decoder.codingPath,
errors: errors
)
}
public func encode(to encoder: any Swift.Encoder) throws {
switch self {
case let .B1(value):
try value.encode(to: encoder)
case let .B2(value):
try value.encode(to: encoder)
}
}
}
public var b: Components.Schemas.A.bPayload?
public init(b: Components.Schemas.A.bPayload? = nil) {
self.b = b
}
public enum CodingKeys: String, CodingKey {
case b
}
}
}
"""
)
}

func testComponentsSchemasAnyOfWithNull_singleRef() throws {
try self.assertSchemasTranslation(
"""
schemas:
B:
type: object
required: [c]
properties:
c:
type: string
A:
type: object
required: [b]
properties:
b:
anyOf:
- $ref: '#/components/schemas/B'
- type: 'null'
""",
"""
public enum Schemas {
public struct B: Codable, Hashable, Sendable {
public var c: Swift.String
public init(c: Swift.String) {
self.c = c
}
public enum CodingKeys: String, CodingKey {
case c
}
}
public struct A: Codable, Hashable, Sendable {
public struct bPayload: Codable, Hashable, Sendable {
public var value1: Components.Schemas.B?
public init(value1: Components.Schemas.B? = nil) {
self.value1 = value1
}
public init(from decoder: any Swift.Decoder) throws {
var errors: [any Swift.Error] = []
do {
self.value1 = try .init(from: decoder)
} catch {
errors.append(error)
}
try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil(
[
self.value1
],
type: Self.self,
codingPath: decoder.codingPath,
errors: errors
)
}
public func encode(to encoder: any Swift.Encoder) throws {
try self.value1?.encode(to: encoder)
}
}
public var b: Components.Schemas.A.bPayload?
public init(b: Components.Schemas.A.bPayload? = nil) {
self.b = b
}
public enum CodingKeys: String, CodingKey {
case b
}
}
}
"""
)
}

func testComponentsSchemasArrayWithNullableOneOfItems() throws {
try self.assertSchemasTranslation(
"""
schemas:
B:
type: object
required: [c]
properties:
c:
type: string
A:
type: object
required: [items]
properties:
items:
type: array
items:
oneOf:
- $ref: '#/components/schemas/B'
- type: 'null'
""",
"""
public enum Schemas {
public struct B: Codable, Hashable, Sendable {
public var c: Swift.String
public init(c: Swift.String) {
self.c = c
}
public enum CodingKeys: String, CodingKey {
case c
}
}
public struct A: Codable, Hashable, Sendable {
@frozen public enum itemsPayloadPayload: Codable, Hashable, Sendable {
case B(Components.Schemas.B)
public init(from decoder: any Swift.Decoder) throws {
var errors: [any Swift.Error] = []
do {
self = .B(try .init(from: decoder))
return
} catch {
errors.append(error)
}
throw Swift.DecodingError.failedToDecodeOneOfSchema(
type: Self.self,
codingPath: decoder.codingPath,
errors: errors
)
}
public func encode(to encoder: any Swift.Encoder) throws {
switch self {
case let .B(value):
try value.encode(to: encoder)
}
}
}
public typealias itemsPayload = [Components.Schemas.A.itemsPayloadPayload?]
public var items: Components.Schemas.A.itemsPayload
public init(items: Components.Schemas.A.itemsPayload) {
self.items = items
}
public enum CodingKeys: String, CodingKey {
case items
}
}
}
"""
)
}

func testComponentsSchemasOneOf_open_pattern() throws {
try self.assertSchemasTranslation(
"""
Expand Down