Skip to content

Commit

Permalink
Update to OpenAPIKit 3.0.0-alpha.9 (#147)
Browse files Browse the repository at this point in the history
### Motivation

Investigation into #128 led to a new release of OpenAPIKit, which
supports path references. Supporting path references isn't something we
want to do right now because:

1. OpenAPI 3.0.3 only supports external path references (cf. 3.1, which
supports internal references too).
2. Swift OpenAPI Generator currently only supports OpenAPI 3.0.x.
3. Swift OpenAPI Generator currently doesn't support external
references.

The OpenAPIKit update comes with a new API, because the paths are now an
`Either` to support being a `PathItem` or a reference to a `PathItem`.

For now we'd like to be keeping up to date with OpenAPIKit so that, when
we support OpenAPI 3.1 and/or external references, we have the APIs we
need in the upstream package.

### Modifications

- Update to OpenAPIKit 3.0.0-alpha.9.
- Attempt to resolve the path item during translation.

### Result

An error will be thrown when using an OpenAPI document with a path
reference.

### Test Plan

Added reference tests.

---------

Signed-off-by: Si Beaumont <[email protected]>
  • Loading branch information
simonjbeaumont authored Aug 1, 2023
1 parent 16420df commit 81f8743
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ let package = Package(
// Read OpenAPI documents
.package(
url: "https://github.com/mattpolzin/OpenAPIKit.git",
exact: "3.0.0-alpha.7"
exact: "3.0.0-alpha.9"
),
.package(
url: "https://github.com/jpsim/Yams.git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ extension FileTranslator {
switch type {
case .allOf:
// AllOf uses all required properties.
propertyType = rawPropertyType
propertyType = rawPropertyType.withOptional(false)
case .anyOf:
// AnyOf uses all optional properties.
propertyType = rawPropertyType.asOptional
propertyType = rawPropertyType.withOptional(true)
}
let comment: Comment? = .property(
originalName: key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,20 @@ extension OperationDescription {
/// - components: The components from the OpenAPI document.
/// - asSwiftSafeName: A converted function from user-provided strings
/// to strings safe to be used as a Swift identifier.
/// - Throws: if `map` contains any references; see discussion for details.
///
/// This function will throw an error if `map` contains any references, because:
/// 1. OpenAPI 3.0.3 only supports external path references (cf. 3.1, which supports internal references too)
/// 2. Swift OpenAPI Generator currently only supports OpenAPI 3.0.x.
/// 3. Swift OpenAPI Generator currently doesn't support external references.
static func all(
from map: OpenAPI.PathItem.Map,
in components: OpenAPI.Components,
asSwiftSafeName: @escaping (String) -> String
) -> [OperationDescription] {
map.flatMap { path, value in
value.endpoints.map { endpoint in
) throws -> [OperationDescription] {
try map.flatMap { path, value in
let value = try value.resolve(in: components)
return value.endpoints.map { endpoint in
OperationDescription(
path: path,
endpoint: endpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,17 @@ struct TypesFileTranslator: FileTranslator {
+ config.additionalImports
.map { ImportDescription(moduleName: $0) }

let apiProtocol = translateAPIProtocol(doc.paths)
let apiProtocol = try translateAPIProtocol(doc.paths)

let serversDecl = translateServers(doc.servers)

let components = try translateComponents(doc.components)

let operationDescriptions =
OperationDescription
.all(
from: parsedOpenAPI.paths,
in: doc.components,
asSwiftSafeName: swiftSafeName
)
let operationDescriptions = try OperationDescription.all(
from: parsedOpenAPI.paths,
in: doc.components,
asSwiftSafeName: swiftSafeName
)
let operations = try translateOperations(operationDescriptions)

let typesFile = FileDescription(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ extension TypesFileTranslator {
/// per HTTP operation defined in the OpenAPI document.
/// - Parameter paths: The paths object from the OpenAPI document.
/// - Returns: A protocol declaration.
func translateAPIProtocol(_ paths: OpenAPI.PathItem.Map) -> Declaration {
/// - Throws: If `paths` contains any references.
func translateAPIProtocol(_ paths: OpenAPI.PathItem.Map) throws -> Declaration {

let operations = OperationDescription.all(
let operations = try OperationDescription.all(
from: paths,
in: components,
asSwiftSafeName: swiftSafeName
Expand Down
8 changes: 6 additions & 2 deletions Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ final class Test_YamsParser: Test_Core {
description: Success
"""

let expected =
"/foo.yaml: error: Expected to find `responses` key for the **GET** endpoint under `/system` but it is missing."
let expected = """
/foo.yaml: error: Found neither a $ref nor a PathItem in Document.paths['/system'].
PathItem could not be decoded because:
Expected to find `responses` key for the **GET** endpoint under `/system` but it is missing..
"""
assertThrownError(try _test(yaml), expectedDiagnostic: expected)
}

Expand Down
156 changes: 156 additions & 0 deletions Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,103 @@ final class SnippetBasedReferenceTests: XCTestCase {
)
}

func testComponentsSchemasAllOfOneStringRef() throws {
try self.assertSchemasTranslation(
"""
schemas:
A:
type: string
MyAllOf:
allOf:
- $ref: '#/components/schemas/A'
""",
"""
public enum Schemas {
public typealias A = Swift.String
public struct MyAllOf: Codable, Equatable, Hashable, Sendable {
public var value1: Components.Schemas.A
public init(value1: Components.Schemas.A) {
self.value1 = value1
}
public init(from decoder: any Decoder) throws {
value1 = try .init(from: decoder)
}
public func encode(to encoder: any Encoder) throws {
try value1.encode(to: encoder)
}
}
}
"""
)
}

func testComponentsSchemasObjectWithRequiredAllOfOneStringRefProperty() throws {
try self.assertSchemasTranslation(
"""
schemas:
A:
type: string
B:
type: object
required:
- c
properties:
c:
allOf:
- $ref: "#/components/schemas/A"
""",
"""
public enum Schemas {
public typealias A = Swift.String
public struct B: Codable, Equatable, Hashable, Sendable {
public struct cPayload: Codable, Equatable, Hashable, Sendable {
public var value1: Components.Schemas.A
public init(value1: Components.Schemas.A) { self.value1 = value1 }
public init(from decoder: any Decoder) throws { value1 = try .init(from: decoder) }
public func encode(to encoder: any Encoder) throws { try value1.encode(to: encoder) }
}
public var c: Components.Schemas.B.cPayload
public init(c: Components.Schemas.B.cPayload) { self.c = c }
public enum CodingKeys: String, CodingKey { case c }
}
}
"""
)
}

func testComponentsSchemasObjectWithOptionalAllOfOneStringRefProperty() throws {
try self.assertSchemasTranslation(
"""
schemas:
A:
type: string
B:
type: object
required: []
properties:
c:
allOf:
- $ref: "#/components/schemas/A"
""",
"""
public enum Schemas {
public typealias A = Swift.String
public struct B: Codable, Equatable, Hashable, Sendable {
public struct cPayload: Codable, Equatable, Hashable, Sendable {
public var value1: Components.Schemas.A
public init(value1: Components.Schemas.A) { self.value1 = value1 }
public init(from decoder: any Decoder) throws { value1 = try .init(from: decoder) }
public func encode(to encoder: any Encoder) throws { try value1.encode(to: encoder) }
}
public var c: Components.Schemas.B.cPayload?
public init(c: Components.Schemas.B.cPayload? = nil) { self.c = c }
public enum CodingKeys: String, CodingKey { case c }
}
}
"""
)
}

func testComponentsSchemasEnum() throws {
try self.assertSchemasTranslation(
"""
Expand Down Expand Up @@ -629,6 +726,52 @@ final class SnippetBasedReferenceTests: XCTestCase {
"""
)
}

func testPathsSimplestCase() throws {
try self.assertPathsTranslation(
"""
/health:
get:
operationId: getHealth
responses:
'200':
description: A success response with a greeting.
content:
text/plain:
schema:
type: string
""",
"""
public protocol APIProtocol: Sendable {
func getHealth(_ input: Operations.getHealth.Input) async throws -> Operations.getHealth.Output
}
"""
)
}

func testPathWithPathItemReference() throws {
XCTAssertThrowsError(
try self.assertPathsTranslation(
"""
/health:
get:
operationId: getHealth
responses:
'200':
description: A success response with a greeting.
content:
text/plain:
schema:
type: string
/health2:
$ref: "#/paths/~1health"
""",
"""
unused: This test throws an error.
"""
)
)
}
}

extension SnippetBasedReferenceTests {
Expand Down Expand Up @@ -720,6 +863,19 @@ extension SnippetBasedReferenceTests {
let translation = try translator.translateComponentRequestBodies(translator.components.requestBodies)
try XCTAssertSwiftEquivalent(translation, expectedSwift, file: file, line: line)
}

func assertPathsTranslation(
_ pathsYAML: String,
componentsYAML: String = "{}",
_ expectedSwift: String,
file: StaticString = #filePath,
line: UInt = #line
) throws {
let translator = try makeTypesTranslator(componentsYAML: componentsYAML)
let paths = try YAMLDecoder().decode(OpenAPI.PathItem.Map.self, from: pathsYAML)
let translation = try translator.translateAPIProtocol(paths)
try XCTAssertSwiftEquivalent(translation, expectedSwift, file: file, line: line)
}
}

private func XCTAssertEqualWithDiff(
Expand Down

0 comments on commit 81f8743

Please sign in to comment.