diff --git a/Package.swift b/Package.swift index fdee2d147..2937a3014 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index 4f106d36f..be92013ce 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -172,19 +172,19 @@ extension ClientFileTranslator { .identifier(Constants.Operations.namespace) .dot(description.methodName) - let operationArg: FunctionArgumentDescription = .init( + let operationArg = FunctionArgumentDescription( label: "forOperation", expression: operationTypeExpr.dot("id") ) - let inputArg: FunctionArgumentDescription = .init( + let inputArg = FunctionArgumentDescription( label: "input", expression: .identifier(Constants.Operation.Input.variableName) ) - let serializerArg: FunctionArgumentDescription = .init( + let serializerArg = FunctionArgumentDescription( label: "serializer", expression: try translateClientSerializer(description) ) - let deserializerArg: FunctionArgumentDescription = .init( + let deserializerArg = FunctionArgumentDescription( label: "deserializer", expression: try translateClientDeserializer(description) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index d47a6b0b9..c28fc00ef 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -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, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift index 47247f08b..4795a4efb 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift @@ -29,7 +29,7 @@ extension FileTranslator { let knownKeys = properties .map(\.originalName) - let knownKeysFunctionArg: FunctionArgumentDescription = .init( + let knownKeysFunctionArg = FunctionArgumentDescription( label: "knownKeys", expression: .literal( .array( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift index 480c66792..d5369c794 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift @@ -144,7 +144,7 @@ extension FileTranslator { typeUsage = valueTypeUsage.asDictionaryValue } - let extraProperty: PropertyBlueprint = .init( + let extraProperty = PropertyBlueprint( comment: .doc("A container of undocumented properties."), originalName: "additionalProperties", typeUsage: typeUsage, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index 5e97d58ea..5c87f9df3 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -72,7 +72,7 @@ extension FileTranslator { ] ) } - let unknownCase: SwitchCaseDescription = .init( + let unknownCase = SwitchCaseDescription( kind: .default, body: [ .expression( @@ -126,7 +126,7 @@ extension FileTranslator { ] ) } - let unknownCase: SwitchCaseDescription = .init( + let unknownCase = SwitchCaseDescription( kind: .case( .valueBinding( kind: .let, @@ -147,7 +147,7 @@ extension FileTranslator { ] ) - let variableDescription: VariableDescription = .init( + let variableDescription = VariableDescription( accessModifier: config.access, kind: .var, left: "rawValue", @@ -186,7 +186,7 @@ extension FileTranslator { ) } - let enumDescription: EnumDescription = .init( + let enumDescription = EnumDescription( isFrozen: true, accessModifier: config.access, name: typeName.shortSwiftName, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift index f35ad7e8c..00304c162 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift @@ -26,7 +26,7 @@ extension FileTranslator { userDescription: String?, to existingTypeUsage: TypeUsage ) throws -> Declaration { - let typealiasDescription: TypealiasDescription = .init( + let typealiasDescription = TypealiasDescription( accessModifier: config.access, name: typeName.shortSwiftName, existingType: existingTypeUsage.fullyQualifiedNonOptionalSwiftName diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index 4872cc655..547079968 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -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, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index 55b6732f3..b1d124ec2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -201,7 +201,7 @@ extension ClientFileTranslator { ]) ) ) - let caseDesc: SwitchCaseDescription = .init( + let caseDesc = SwitchCaseDescription( kind: .case(.dot(contentTypeIdentifier), ["value"]), body: [ .expression(bodyAssignExpr) @@ -210,7 +210,7 @@ extension ClientFileTranslator { return caseDesc } if !requestBody.request.required { - let noneCase: SwitchCaseDescription = .init( + let noneCase = SwitchCaseDescription( kind: .case(.dot("none")), body: [ .expression( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 5e58dc57e..c63dd3b81 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -349,8 +349,7 @@ extension ServerFileTranslator { let responseVarDecl: Declaration = .variable( kind: .var, left: "response", - type: "Response", - right: .dot("init") + right: .identifier("Response") .call([ .init(label: "statusCode", expression: statusCodeExpr) ]) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index 250eaf80a..c3afd31b1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -178,19 +178,19 @@ extension ServerFileTranslator { .identifier(Constants.Operations.namespace) .dot(description.methodName) - let operationArg: FunctionArgumentDescription = .init( + let operationArg = FunctionArgumentDescription( label: "forOperation", expression: operationTypeExpr.dot("id") ) - let requestArg: FunctionArgumentDescription = .init( + let requestArg = FunctionArgumentDescription( label: "request", expression: .identifier("request") ) - let metadataArg: FunctionArgumentDescription = .init( + let metadataArg = FunctionArgumentDescription( label: "with", expression: .identifier("metadata") ) - let methodArg: FunctionArgumentDescription = .init( + let methodArg = FunctionArgumentDescription( label: "using", expression: .closureInvocation( body: [ @@ -204,11 +204,11 @@ extension ServerFileTranslator { ] ) ) - let deserializerArg: FunctionArgumentDescription = .init( + let deserializerArg = FunctionArgumentDescription( label: "deserializer", expression: try translateServerDeserializer(description) ) - let serializerArg: FunctionArgumentDescription = .init( + let serializerArg = FunctionArgumentDescription( label: "serializer", expression: try translateServerSerializer(description) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index 0e76feb04..7aa46c68c 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -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( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift index 1a44377be..422320ee0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift @@ -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 @@ -30,7 +31,7 @@ extension TypesFileTranslator { operations .map(translateAPIProtocolDeclaration(operation:)) - let protocolDescription: ProtocolDescription = .init( + let protocolDescription = ProtocolDescription( accessModifier: config.access, name: Constants.APIProtocol.typeName, conformances: Constants.APIProtocol.conformances, diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 18d9f52b2..9cbed1933 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift @@ -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) } diff --git a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift index d8c1f4935..f1b30e63f 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift @@ -16,7 +16,7 @@ import XCTest final class Test_TextBasedRenderer: XCTestCase { - var renderer: TextBasedRenderer = .init() + var renderer = TextBasedRenderer() func testComment() throws { try _test( diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index 6b89de6ea..8b148046b 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -125,7 +125,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .ok(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 200) + var response = Response(statusCode: 200) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsText( in: &response.headerFields, @@ -152,7 +152,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { return response case let .`default`(statusCode, value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: statusCode) + var response = Response(statusCode: statusCode) suppressMutabilityWarning(&response) switch value.body { case let .json(value): @@ -219,7 +219,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .created(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 201) + var response = Response(statusCode: 201) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsJSON( in: &response.headerFields, @@ -241,7 +241,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { return response case let .badRequest(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 400) + var response = Response(statusCode: 400) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsText( in: &response.headerFields, @@ -384,7 +384,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .noContent(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 204) + var response = Response(statusCode: 204) suppressMutabilityWarning(&response) return response case let .undocumented(statusCode, _): return .init(statusCode: statusCode) @@ -441,12 +441,12 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .noContent(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 204) + var response = Response(statusCode: 204) suppressMutabilityWarning(&response) return response case let .badRequest(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 400) + var response = Response(statusCode: 400) suppressMutabilityWarning(&response) switch value.body { case let .json(value): @@ -517,7 +517,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .ok(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 200) + var response = Response(statusCode: 200) suppressMutabilityWarning(&response) switch value.body { case let .binary(value): @@ -534,7 +534,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { return response case let .preconditionFailed(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 412) + var response = Response(statusCode: 412) suppressMutabilityWarning(&response) switch value.body { case let .json(value): @@ -551,7 +551,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { return response case let .internalServerError(value): suppressUnusedWarning(value) - var response: Response = .init(statusCode: 500) + var response = Response(statusCode: 500) suppressMutabilityWarning(&response) switch value.body { case let .text(value): diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 2ade4d1fc..aacdff9ab 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -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( """ @@ -748,6 +845,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 { @@ -839,6 +982,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(