From b65592a7af26eb9969ea1d5939990ac50cb3b5e8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 2 Oct 2023 13:47:21 +0200 Subject: [PATCH] [Generator] Async bodies + swift-http-types adoption (#245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Generator] Async bodies + swift-http-types adoption ### Motivation Generator changes of the approved proposals apple/swift-openapi-generator#255 and apple/swift-openapi-generator#254. ### Modifications - Adapts to the runtime changes. - Most changes are tests updating to the new generated structure. - As usual, easiest to start with the diff to the file-based reference tests to understand the individual changes, and then review the rest of the PR. - To see how to use the generated code, check out some streaming examples in https://github.com/apple/swift-openapi-generator/pull/245/files#diff-2be042f4d1d5896dc213e3a5e451b168bd1f0143e76753f4a5be466a455255eb ### Result Generator works with the 0.3.0 runtime API of. ### Test Plan Adapted tests. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (compatibility test) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (integration test) - Build finished. https://github.com/apple/swift-openapi-generator/pull/245 --- Examples/GreetingService/Package.swift | 8 +- IntegrationTest/Package.swift | 4 +- Package.swift | 2 +- .../PetstoreConsumerTestCore/Assertions.swift | 44 +- Sources/PetstoreConsumerTestCore/Common.swift | 63 +- .../TestClientTransport.swift | 12 +- .../TestServerTransport.swift | 24 +- .../StructuredSwiftRepresentation.swift | 33 +- .../Renderer/TextBasedRenderer.swift | 11 +- .../ClientTranslator/ClientTranslator.swift | 2 +- .../translateClientMethod.swift | 40 +- .../Translator/CommonTypes/Constants.swift | 9 +- .../Translator/Content/CodingStrategy.swift | 5 - .../Translator/Content/ContentInspector.swift | 16 - .../Translator/Content/ContentType.swift | 21 - .../Operations/OperationDescription.swift | 33 +- .../Parameters/translateParameter.swift | 2 +- .../RequestBody/translateRequestBody.swift | 42 +- .../Responses/translateResponseOutcome.swift | 63 +- .../ServerTranslator/ServerTranslator.swift | 2 +- .../translateServerMethod.swift | 33 +- .../Translator/TypeAssignment/Builtins.swift | 14 +- .../TypeAssignment/TypeMatcher.swift | 2 +- .../TypesTranslator/translateServers.swift | 2 +- ...Converting-between-data-and-Swift-types.md | 9 - .../_Resources/client.Package.2.swift | 6 +- .../_Resources/client.Package.3.swift | 6 +- .../_Resources/client.Package.4.swift | 6 +- .../_Resources/client.Package.5.swift | 6 +- .../_Resources/server.Package.2.swift | 6 +- .../_Resources/server.Package.3.swift | 6 +- .../_Resources/server.Package.4.swift | 6 +- .../_Resources/server.Package.5.swift | 6 +- .../Renderer/Test_TextBasedRenderer.swift | 2 +- .../StructureHelpers.swift | 3 + .../Translator/Content/Test_ContentType.swift | 2 +- .../TypeAssignment/Test_TypeMatcher.swift | 2 +- .../CompatabilityTest.swift | 2 +- .../ReferenceSources/Petstore/Client.swift | 152 ++--- .../ReferenceSources/Petstore/Server.swift | 248 +++---- .../ReferenceSources/Petstore/Types.swift | 14 +- .../SnippetBasedReferenceTests.swift | 27 +- Tests/PetstoreConsumerTests/Common.swift | 7 + Tests/PetstoreConsumerTests/TestServer.swift | 21 +- Tests/PetstoreConsumerTests/Test_Client.swift | 535 ++++++++------- .../Test_Playground.swift | 234 +++++++ Tests/PetstoreConsumerTests/Test_Server.swift | 607 +++++++++++------- 47 files changed, 1446 insertions(+), 954 deletions(-) create mode 100644 Tests/PetstoreConsumerTests/Test_Playground.swift diff --git a/Examples/GreetingService/Package.swift b/Examples/GreetingService/Package.swift index 1aa912a0..50c5588c 100644 --- a/Examples/GreetingService/Package.swift +++ b/Examples/GreetingService/Package.swift @@ -20,10 +20,10 @@ let package = Package( .macOS(.v13) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/vapor/vapor", from: "4.76.0"), ], targets: [ diff --git a/IntegrationTest/Package.swift b/IntegrationTest/Package.swift index f906870b..e7078a02 100644 --- a/IntegrationTest/Package.swift +++ b/IntegrationTest/Package.swift @@ -32,8 +32,8 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), ], targets: [ .target( diff --git a/Package.swift b/Package.swift index 9bc671a4..621cd8db 100644 --- a/Package.swift +++ b/Package.swift @@ -89,7 +89,7 @@ let package = Package( // Tests-only: Runtime library linked by generated code, and also // helps keep the runtime library new enough to work with the generated // code. - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.4")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), // Build and preview docs .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), diff --git a/Sources/PetstoreConsumerTestCore/Assertions.swift b/Sources/PetstoreConsumerTestCore/Assertions.swift index 234eb603..9edc3d3a 100644 --- a/Sources/PetstoreConsumerTestCore/Assertions.swift +++ b/Sources/PetstoreConsumerTestCore/Assertions.swift @@ -13,18 +13,58 @@ //===----------------------------------------------------------------------===// import Foundation import XCTest +import OpenAPIRuntime public func XCTAssertEqualStringifiedData( - _ expression1: @autoclosure () throws -> Data, + _ expression1: @autoclosure () throws -> Data?, _ expression2: @autoclosure () throws -> String, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line ) { do { - let actualString = String(decoding: try expression1(), as: UTF8.self) + guard let value1 = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let actualString = String(decoding: value1, as: UTF8.self) XCTAssertEqual(actualString, try expression2(), file: file, line: line) } catch { XCTFail(error.localizedDescription, file: file, line: line) } } + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> S?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) where S.Element == UInt8 { + do { + guard let value1 = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let actualString = String(decoding: Array(value1), as: UTF8.self) + XCTAssertEqual(actualString, try expression2(), file: file, line: line) + } catch { + XCTFail(error.localizedDescription, file: file, line: line) + } +} + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> HTTPBody?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async throws { + let data: Data + if let body = try expression1() { + data = try await Data(collecting: body, upTo: .max) + } else { + data = .init() + } + XCTAssertEqualStringifiedData(data, try expression2(), message(), file: file, line: line) +} diff --git a/Sources/PetstoreConsumerTestCore/Common.swift b/Sources/PetstoreConsumerTestCore/Common.swift index 19048589..b60f3450 100644 --- a/Sources/PetstoreConsumerTestCore/Common.swift +++ b/Sources/PetstoreConsumerTestCore/Common.swift @@ -13,9 +13,10 @@ //===----------------------------------------------------------------------===// import OpenAPIRuntime import Foundation +import HTTPTypes public enum TestError: Swift.Error, LocalizedError, CustomStringConvertible, Sendable { - case noHandlerFound(method: HTTPMethod, path: [RouterPathComponent]) + case noHandlerFound(method: HTTPRequest.Method, path: String) case invalidURLString(String) case unexpectedValue(any Sendable) case unexpectedMissingRequestBody @@ -23,7 +24,7 @@ public enum TestError: Swift.Error, LocalizedError, CustomStringConvertible, Sen public var description: String { switch self { case .noHandlerFound(let method, let path): - return "No handler found for method \(method.name) and path \(path.stringPath)" + return "No handler found for method \(method) and path \(path)" case .invalidURLString(let string): return "Invalid URL string: \(string)" case .unexpectedValue(let value): @@ -48,32 +49,22 @@ public extension Date { } } -public extension Array where Element == RouterPathComponent { - var stringPath: String { - map(\.description).joined(separator: "/") - } -} +public extension HTTPResponse { -public extension Response { - init( - statusCode: Int, - headers: [HeaderField] = [], - encodedBody: String - ) { - self.init( - statusCode: statusCode, - headerFields: headers, - body: Data(encodedBody.utf8) - ) + func withEncodedBody(_ encodedBody: String) throws -> (HTTPResponse, HTTPBody) { + (self, .init(encodedBody)) } - static var listPetsSuccess: Self { - .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "application/json") - ], - encodedBody: #""" + static var listPetsSuccess: (HTTPResponse, HTTPBody) { + get throws { + try Self( + status: .ok, + headerFields: [ + .contentType: "application/json" + ] + ) + .withEncodedBody( + #""" [ { "id": 1, @@ -81,7 +72,8 @@ public extension Response { } ] """# - ) + ) + } } } @@ -111,21 +103,8 @@ public extension Data { } } -public extension Request { - init( - path: String, - query: String? = nil, - method: HTTPMethod, - headerFields: [HeaderField] = [], - encodedBody: String - ) throws { - let body = Data(encodedBody.utf8) - self.init( - path: path, - query: query, - method: method, - headerFields: headerFields, - body: body - ) +public extension HTTPRequest { + func withEncodedBody(_ encodedBody: String) -> (HTTPRequest, HTTPBody) { + (self, .init(encodedBody)) } } diff --git a/Sources/PetstoreConsumerTestCore/TestClientTransport.swift b/Sources/PetstoreConsumerTestCore/TestClientTransport.swift index 4381a980..8aa8273d 100644 --- a/Sources/PetstoreConsumerTestCore/TestClientTransport.swift +++ b/Sources/PetstoreConsumerTestCore/TestClientTransport.swift @@ -13,10 +13,13 @@ //===----------------------------------------------------------------------===// import OpenAPIRuntime import Foundation +import HTTPTypes public struct TestClientTransport: ClientTransport { - public typealias CallHandler = @Sendable (Request, URL, String) async throws -> Response + public typealias CallHandler = @Sendable (HTTPRequest, HTTPBody?, URL, String) async throws -> ( + HTTPResponse, HTTPBody? + ) public let callHandler: CallHandler @@ -25,10 +28,11 @@ public struct TestClientTransport: ClientTransport { } public func send( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> Response { - try await callHandler(request, baseURL, operationID) + ) async throws -> (HTTPResponse, HTTPBody?) { + try await callHandler(request, body, baseURL, operationID) } } diff --git a/Sources/PetstoreConsumerTestCore/TestServerTransport.swift b/Sources/PetstoreConsumerTestCore/TestServerTransport.swift index 74b317ca..9d1933bd 100644 --- a/Sources/PetstoreConsumerTestCore/TestServerTransport.swift +++ b/Sources/PetstoreConsumerTestCore/TestServerTransport.swift @@ -12,22 +12,23 @@ // //===----------------------------------------------------------------------===// import OpenAPIRuntime +import HTTPTypes public final class TestServerTransport: ServerTransport { public struct OperationInputs: Equatable { - public var method: HTTPMethod - public var path: [RouterPathComponent] - public var queryItemNames: Set + public var method: HTTPRequest.Method + public var path: String - public init(method: HTTPMethod, path: [RouterPathComponent], queryItemNames: Set) { + public init(method: HTTPRequest.Method, path: String) { self.method = method self.path = path - self.queryItemNames = queryItemNames } } - public typealias Handler = @Sendable (Request, ServerRequestMetadata) async throws -> Response + public typealias Handler = @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) public struct Operation { public var inputs: OperationInputs @@ -43,14 +44,15 @@ public final class TestServerTransport: ServerTransport { public private(set) var registered: [Operation] = [] public func register( - _ handler: @escaping Handler, - method: HTTPMethod, - path: [RouterPathComponent], - queryItemNames: Set + _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ), + method: HTTPRequest.Method, + path: String ) throws { registered.append( Operation( - inputs: .init(method: method, path: path, queryItemNames: queryItemNames), + inputs: .init(method: method, path: path), closure: handler ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 47e31cb2..8adc53b5 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -441,7 +441,7 @@ struct FunctionSignatureDescription: Equatable, Codable { var keywords: [FunctionKeyword] = [] /// The return type name of the function, such as `Int`. - var returnType: String? = nil + var returnType: Expression? = nil } /// A description of a function definition. @@ -479,7 +479,7 @@ struct FunctionDescription: Equatable, Codable { kind: FunctionKind, parameters: [ParameterDescription] = [], keywords: [FunctionKeyword] = [], - returnType: String? = nil, + returnType: Expression? = nil, body: [CodeBlock]? = nil ) { self.signature = .init( @@ -505,7 +505,7 @@ struct FunctionDescription: Equatable, Codable { kind: FunctionKind, parameters: [ParameterDescription] = [], keywords: [FunctionKeyword] = [], - returnType: String? = nil, + returnType: Expression? = nil, body: [Expression] ) { self.init( @@ -858,6 +858,17 @@ struct OptionalChainingDescription: Equatable, Codable { var referencedExpr: Expression } +/// A description of a tuple. +/// +/// For example: `(foo, bar)`. +struct TupleDescription: Equatable, Codable { + + /// The member expressions. + /// + /// For example, in `(foo, bar)`, `members` is `[foo, bar]`. + var members: [Expression] +} + /// A Swift expression. indirect enum Expression: Equatable, Codable { @@ -928,6 +939,11 @@ indirect enum Expression: Equatable, Codable { /// /// For example, in `foo?`, `referencedExpr` is `foo`. case optionalChaining(OptionalChainingDescription) + + /// A tuple expression. + /// + /// For example: `(foo, bar)`. + case tuple(TupleDescription) } /// A code block item, either a declaration or an expression. @@ -1076,7 +1092,7 @@ extension Declaration { kind: FunctionKind, parameters: [ParameterDescription], keywords: [FunctionKeyword] = [], - returnType: String? = nil, + returnType: Expression? = nil, body: [CodeBlock]? = nil ) -> Self { .function( @@ -1432,6 +1448,15 @@ extension Expression { func optionallyChained() -> Self { .optionalChaining(.init(referencedExpr: self)) } + + /// Returns a new tuple expression. + /// + /// For example, in `(foo, bar)`, `members` is `[foo, bar]`. + /// - Parameter expressions: The member expressions. + /// - Returns: A tuple expression. + static func tuple(_ expressions: [Expression]) -> Self { + .tuple(.init(members: expressions)) + } } extension MemberAccessDescription { diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index b67f7953..1b06e5d4 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -285,6 +285,13 @@ struct TextBasedRenderer: RendererProtocol { renderedExpression(description.referencedExpr) + "?" } + /// Renders the specified tuple expression. + func renderedTupleDescription( + _ description: TupleDescription + ) -> String { + "(" + description.members.map(renderedExpression).joined(separator: ", ") + ")" + } + /// Renders the specified expression. func renderedExpression(_ expression: Expression) -> String { switch expression { @@ -316,6 +323,8 @@ struct TextBasedRenderer: RendererProtocol { return renderedInOutDescription(inOut) case .optionalChaining(let optionalChaining): return renderedOptionalChainingDescription(optionalChaining) + case .tuple(let tuple): + return renderedTupleDescription(tuple) } } @@ -606,7 +615,7 @@ struct TextBasedRenderer: RendererProtocol { } if let returnType = signature.returnType { words.append("->") - words.append(returnType) + words.append(renderedExpression(returnType)) } return words.joinedWords() } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 393b0875..46b37ecd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -37,7 +37,7 @@ struct ClientFileTranslator: FileTranslator { let topComment: Comment = .inline(Constants.File.topComment) let imports = - Constants.File.imports + Constants.File.clientServerImports + config.additionalImports .map { ImportDescription(moduleName: $0) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index b40c3b0c..b9465c90 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -43,7 +43,7 @@ extension ClientFileTranslator { type: TypeName.request.fullyQualifiedSwiftName, right: .dot("init") .call([ - .init(label: "path", expression: .identifier("path")), + .init(label: "soar_path", expression: .identifier("path")), .init(label: "method", expression: .dot(description.httpMethodLowercased)), ]) ) @@ -51,7 +51,7 @@ extension ClientFileTranslator { let typedParameters = try typedParameters( from: description ) - var requestExprs: [Expression] = [] + var requestBlocks: [CodeBlock] = [] let nonPathParameters = typedParameters @@ -65,7 +65,7 @@ extension ClientFileTranslator { inputVariableName: "input" ) } - requestExprs.append(contentsOf: nonPathParamExprs) + requestBlocks.append(contentsOf: nonPathParamExprs.map { .expression($0) }) let acceptContent = try acceptHeaderContentTypes( for: description @@ -84,19 +84,39 @@ extension ClientFileTranslator { expression: .identifier("input").dot("headers").dot("accept") ), ]) - requestExprs.append(setAcceptHeaderExpr) + requestBlocks.append(.expression(setAcceptHeaderExpr)) } + let requestBodyReturnExpr: Expression if let requestBody = try typedRequestBody(in: description) { + let bodyVariableName = "body" + requestBlocks.append( + .declaration( + .variable( + kind: .let, + left: bodyVariableName, + type: TypeName.body.asUsage.asOptional.fullyQualifiedSwiftName + ) + ) + ) + requestBodyReturnExpr = .identifier(bodyVariableName) let requestBodyExpr = try translateRequestBodyInClient( requestBody, requestVariableName: "request", + bodyVariableName: bodyVariableName, inputVariableName: "input" ) - requestExprs.append(requestBodyExpr) + requestBlocks.append(.expression(requestBodyExpr)) + } else { + requestBodyReturnExpr = nil } - let returnRequestExpr: Expression = .return(.identifier("request")) + let returnRequestExpr: Expression = .return( + .tuple([ + .identifier("request"), + requestBodyReturnExpr, + ]) + ) return .closureInvocation( argumentNames: [ @@ -106,7 +126,7 @@ extension ClientFileTranslator { .declaration(pathDecl), .declaration(requestDecl), .expression(requestDecl.suppressMutabilityWarningExpr), - ] + requestExprs.map { .expression($0) } + [ + ] + requestBlocks + [ .expression(returnRequestExpr) ] ) @@ -131,7 +151,7 @@ extension ClientFileTranslator { let undocumentedExpr: Expression = .return( .dot(Constants.Operation.Output.undocumentedCaseName) .call([ - .init(label: "statusCode", expression: .identifier("response").dot("statusCode")), + .init(label: "statusCode", expression: .identifier("response").dot("status").dot("code")), .init(label: nil, expression: .dot("init").call([])), ]) ) @@ -145,11 +165,11 @@ extension ClientFileTranslator { ) } let switchStatusCodeExpr: Expression = .switch( - switchedExpression: .identifier("response").dot("statusCode"), + switchedExpression: .identifier("response").dot("status").dot("code"), cases: cases ) return .closureInvocation( - argumentNames: ["response"], + argumentNames: ["response", "responseBody"], body: [ .expression(switchStatusCodeExpr) ] diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index bde72936..958f81c0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -32,6 +32,12 @@ enum Constants { preconcurrency: .onOS(["Linux"]) ), ] + + /// The descriptions of modules imported by client and server files. + static let clientServerImports: [ImportDescription] = + imports + [ + ImportDescription(moduleName: "HTTPTypes") + ] } /// Constants related to the OpenAPI server object. @@ -369,9 +375,6 @@ enum Constants { /// The substring used in method names for the URI coding strategy. static let uri: String = "URI" - /// The substring used in method names for the string coding strategy. - static let string: String = "String" - /// The substring used in method names for the binary coding strategy. static let binary: String = "Binary" diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift index 016cb9ff..e65a53fc 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift @@ -21,9 +21,6 @@ enum CodingStrategy: String, Hashable, Sendable { /// A strategy using URIEncoder/URIDecoder. case uri - /// A strategy using StringEncoder/StringDecoder. - case string - /// A strategy that passes through the data unmodified. case binary @@ -37,8 +34,6 @@ enum CodingStrategy: String, Hashable, Sendable { return Constants.CodingStrategy.json case .uri: return Constants.CodingStrategy.uri - case .string: - return Constants.CodingStrategy.string case .binary: return Constants.CodingStrategy.binary case .urlEncodedForm: diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift index 1f4e68ef..2b1c9b22 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift @@ -163,15 +163,6 @@ extension FileTranslator { ), contentValue ) - } else if let (contentKey, contentValue) = map.first(where: { $0.key.isText }) { - let contentType = contentKey.asGeneratorContentType - chosenContent = ( - .init( - contentType: contentType, - schema: .b(.string) - ), - contentValue - ) } else if !excludeBinary, let (contentKey, contentValue) = map.first(where: { $0.key.isBinary }) { let contentType = contentKey.asGeneratorContentType chosenContent = ( @@ -240,13 +231,6 @@ extension FileTranslator { schema: contentValue.schema ) } - if contentKey.isText { - let contentType = contentKey.asGeneratorContentType - return .init( - contentType: contentType, - schema: .b(.string) - ) - } let urlEncodedFormsSupported = config.featureFlags.contains(.urlEncodedForm) if urlEncodedFormsSupported && contentKey.isUrlEncodedForm { let contentType = ContentType(contentKey.typeAndSubtype) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index 0c26a3d1..78368fd2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -30,11 +30,6 @@ struct ContentType: Hashable { /// The bytes are provided to a JSON encoder or decoder. case json - /// A content type for any plain text. - /// - /// The bytes are encoded or decoded as a UTF-8 string. - case text - /// A content type for raw binary data. /// /// This case covers both explicit binary data content types, such @@ -67,8 +62,6 @@ struct ContentType: Hashable { if (lowercasedType == "application" && lowercasedSubtype == "json") || lowercasedSubtype.hasSuffix("+json") { self = .json - } else if lowercasedType == "text" { - self = .text } else if lowercasedType == "application" && lowercasedSubtype == "x-www-form-urlencoded" { self = .urlEncodedForm } else { @@ -81,8 +74,6 @@ struct ContentType: Hashable { switch self { case .json: return .json - case .text: - return .string case .binary: return .binary case .urlEncodedForm: @@ -238,12 +229,6 @@ struct ContentType: Hashable { category == .json } - /// A Boolean value that indicates whether the content type - /// is a type of plain text. - var isText: Bool { - category == .text - } - /// A Boolean value that indicates whether the content type /// is just binary data. var isBinary: Bool { @@ -278,12 +263,6 @@ extension OpenAPI.ContentType { asGeneratorContentType.isUrlEncodedForm } - /// A Boolean value that indicates whether the content type - /// is a type of plain text. - var isText: Bool { - asGeneratorContentType.isText - } - /// A Boolean value that indicates whether the content type /// is just binary data. var isBinary: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index d2951aa0..a92d1dc3 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -264,7 +264,7 @@ extension OperationDescription { ) ], keywords: [.async, .throws], - returnType: outputTypeName.fullyQualifiedSwiftName + returnType: .identifier(outputTypeName.fullyQualifiedSwiftName) ) } @@ -275,11 +275,12 @@ extension OperationDescription { accessModifier: nil, kind: .function(name: methodName), parameters: [ - .init(label: "request", type: "Request"), + .init(label: "request", type: "HTTPRequest"), + .init(label: "body", type: "HTTPBody?"), .init(label: "metadata", type: "ServerRequestMetadata"), ], keywords: [.async, .throws], - returnType: "Response" + returnType: .tuple([.identifier("HTTPResponse"), .identifier("HTTPBody?")]) ) } @@ -309,32 +310,6 @@ extension OperationDescription { } } - /// Returns an array that contains the template to be generated for - /// the server that translates the URL template variable syntax to - /// the Runtime syntax. - /// - /// For example, `"/pets/{petId}"` -> `["pets", ":petId"]`. - var templatedPathForServer: [String] { - path.components.filter({ !$0.isEmpty }) - .map { component in - guard component.hasPrefix("{") && component.hasSuffix("}") else { - // FYI: Some components could also be "{foo}.zip", which we don't - // currently support. - return component - } - return ":\(component.dropFirst().dropLast())" - } - } - - /// Returns the list of all names of query parameters. - var queryParameterNames: [String] { - get throws { - try allResolvedParameters - .filter { $0.location == .query } - .map(\.name) - } - } - /// A Boolean value that indicates whether the operation defines /// a default response. var containsDefaultResponse: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index dfef7582..30185408 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -207,7 +207,7 @@ extension ServerFileTranslator { convertExpr = .try( .identifier("converter").dot(methodName("QueryItem")) .call([ - .init(label: "in", expression: .identifier("request").dot("query")), + .init(label: "in", expression: .identifier("request").dot("soar_query")), .init(label: "style", expression: .dot(typedParameter.style.runtimeName)), .init(label: "explode", expression: .literal(.bool(typedParameter.explode))), .init(label: "name", expression: .literal(parameter.name)), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index 21bc828a..27cf8f1b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -163,11 +163,13 @@ extension ClientFileTranslator { /// - Parameters: /// - requestBody: The request body to extract. /// - requestVariableName: The name of the request variable. + /// - bodyVariableName: The name of the body variable. /// - inputVariableName: The name of the Input variable. /// - Returns: An assignment expression. func translateRequestBodyInClient( _ requestBody: TypedRequestBody, requestVariableName: String, + bodyVariableName: String, inputVariableName: String ) throws -> Expression { let contents = requestBody.contents @@ -178,7 +180,7 @@ extension ClientFileTranslator { let contentTypeHeaderValue = contentType.headerValueForSending let bodyAssignExpr: Expression = .assignment( - left: .identifier(requestVariableName).dot("body"), + left: .identifier(bodyVariableName), right: .try( .identifier("converter") .dot( @@ -213,7 +215,7 @@ extension ClientFileTranslator { body: [ .expression( .assignment( - left: .identifier(requestVariableName).dot("body"), + left: .identifier(bodyVariableName), right: .literal(.nil) ) ) @@ -307,7 +309,8 @@ extension ServerFileTranslator { let contentTypeUsage = typedContent.resolvedTypeUsage let content = typedContent.content let contentType = content.contentType - let codingStrategyName = contentType.codingStrategy.runtimeName + let codingStrategy = contentType.codingStrategy + let codingStrategyName = codingStrategy.runtimeName let transformExpr: Expression = .closureInvocation( argumentNames: ["value"], body: [ @@ -319,21 +322,26 @@ extension ServerFileTranslator { ) ] ) - let bodyExpr: Expression = .try( + let converterExpr: Expression = .identifier("converter") - .dot("get\(isOptional ? "Optional" : "Required")RequestBodyAs\(codingStrategyName)") - .call([ - .init( - label: nil, - expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") - ), - .init(label: "from", expression: .identifier(requestVariableName).dot("body")), - .init( - label: "transforming", - expression: transformExpr - ), - ]) - ) + .dot("get\(isOptional ? "Optional" : "Required")RequestBodyAs\(codingStrategyName)") + .call([ + .init( + label: nil, + expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") + ), + .init(label: "from", expression: .identifier("requestBody")), + .init( + label: "transforming", + expression: transformExpr + ), + ]) + let bodyExpr: Expression + if codingStrategy == .binary { + bodyExpr = .try(converterExpr) + } else { + bodyExpr = .try(.await(converterExpr)) + } return .init( condition: .try(condition), body: [ diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index ff2eb20e..8e4b8815 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -220,21 +220,26 @@ extension ClientFileTranslator { ) ] ) - let bodyExpr: Expression = .try( - .identifier("converter") - .dot("getResponseBodyAs\(typedContent.content.contentType.codingStrategy.runtimeName)") - .call([ - .init( - label: nil, - expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") - ), - .init(label: "from", expression: .identifier("response").dot("body")), - .init( - label: "transforming", - expression: transformExpr - ), - ]) - ) + let codingStrategy = typedContent.content.contentType.codingStrategy + let converterExpr: Expression = .identifier("converter") + .dot("getResponseBodyAs\(codingStrategy.runtimeName)") + .call([ + .init( + label: nil, + expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") + ), + .init(label: "from", expression: .identifier("responseBody")), + .init( + label: "transforming", + expression: transformExpr + ), + ]) + let bodyExpr: Expression + if codingStrategy == .binary { + bodyExpr = .try(converterExpr) + } else { + bodyExpr = .try(.await(converterExpr)) + } return .init( condition: .try(condition), body: [ @@ -315,7 +320,7 @@ extension ClientFileTranslator { optionalStatusCode = [ .init( label: "statusCode", - expression: .identifier("response").dot("statusCode") + expression: .identifier("response").dot("status").dot("code") ) ] } else { @@ -372,9 +377,9 @@ extension ServerFileTranslator { let responseVarDecl: Declaration = .variable( kind: .var, left: "response", - right: .identifier("Response") + right: .identifier("HTTPResponse") .call([ - .init(label: "statusCode", expression: statusCodeExpr) + .init(label: "soar_statusCode", expression: statusCodeExpr) ]) ) codeBlocks.append(contentsOf: [ @@ -401,12 +406,21 @@ extension ServerFileTranslator { } codeBlocks.append(contentsOf: headerExprs.map { .expression($0) }) + let bodyReturnExpr: Expression let typedContents = try supportedTypedContents( typedResponse.response.content, inParent: bodyTypeName ) - if !typedContents.isEmpty { + codeBlocks.append( + .declaration( + .variable( + kind: .let, + left: "body", + type: "HTTPBody" + ) + ) + ) let switchContentCases: [SwitchCaseDescription] = typedContents.map { typedContent in var caseCodeBlocks: [CodeBlock] = [] @@ -423,7 +437,7 @@ extension ServerFileTranslator { let contentType = typedContent.content.contentType let assignBodyExpr: Expression = .assignment( - left: .identifier("response").dot("body"), + left: .identifier("body"), right: .try( .identifier("converter") .dot("setResponseBodyAs\(contentType.codingStrategy.runtimeName)") @@ -458,10 +472,17 @@ extension ServerFileTranslator { ) ) ) + + bodyReturnExpr = .identifier("body") + } else { + bodyReturnExpr = nil } let returnExpr: Expression = .return( - .identifier("response") + .tuple([ + .identifier("response"), + bodyReturnExpr, + ]) ) codeBlocks.append(.expression(returnExpr)) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index b183cffb..9cd39e05 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -35,7 +35,7 @@ struct ServerFileTranslator: FileTranslator { let topComment: Comment = .inline(Constants.File.topComment) let imports = - Constants.File.imports + Constants.File.clientServerImports + config.additionalImports .map { ImportDescription(moduleName: $0) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index 491d9f35..b1672529 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -139,7 +139,7 @@ extension ServerFileTranslator { contentsOf: inputMembers.flatMap(\.codeBlocks) + [.expression(returnExpr)] ) return .closureInvocation( - argumentNames: ["request", "metadata"], + argumentNames: ["request", "requestBody", "metadata"], body: closureBody ) } @@ -159,10 +159,13 @@ extension ServerFileTranslator { } if !description.containsDefaultResponse { let undocumentedExpr: Expression = .return( - .dot("init") - .call([ - .init(label: "statusCode", expression: .identifier("statusCode")) - ]) + .tuple([ + .dot("init") + .call([ + .init(label: "soar_statusCode", expression: .identifier("statusCode")) + ]), + nil, + ]) ) cases.append( .init( @@ -216,8 +219,12 @@ extension ServerFileTranslator { label: "request", expression: .identifier("request") ) + let requestBodyArg = FunctionArgumentDescription( + label: "requestBody", + expression: .identifier("body") + ) let metadataArg = FunctionArgumentDescription( - label: "with", + label: "metadata", expression: .identifier("metadata") ) let methodArg = FunctionArgumentDescription( @@ -252,7 +259,8 @@ extension ServerFileTranslator { .dot(description.methodName) .call([ .init(label: "request", expression: .identifier("$0")), - .init(label: "metadata", expression: .identifier("$1")), + .init(label: "body", expression: .identifier("$1")), + .init(label: "metadata", expression: .identifier("$2")), ]) ) ) @@ -272,19 +280,11 @@ extension ServerFileTranslator { .init( label: nil, expression: .literal( - .array(description.templatedPathForServer.map { .literal($0) }) + .string(description.path.rawValue) ) ) ]) ), - .init( - label: "queryItemNames", - expression: .literal( - .array( - try description.queryParameterNames.map { .literal($0) } - ) - ) - ), ]) ) @@ -293,6 +293,7 @@ extension ServerFileTranslator { .identifier("handle") .call([ requestArg, + requestBodyArg, metadataArg, operationArg, methodArg, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift index ba26552c..b114c80d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift @@ -45,6 +45,13 @@ extension TypeName { TypeName(swiftKeyPath: ["OpenAPIRuntime", name]) } + /// Returns a type name for a type with the specified name in the + /// HTTPTypes module. + /// - Parameter name: The name of the type. + static func httpTypes(_ name: String) -> TypeName { + TypeName(swiftKeyPath: ["HTTPTypes", name]) + } + /// Returns the type name for the UndocumentedPayload type. static var undocumentedPayload: Self { .runtime(Constants.Operation.Output.undocumentedCaseAssociatedValueTypeName) @@ -67,6 +74,11 @@ extension TypeName { /// Returns the type name for the request type. static var request: TypeName { - .runtime("Request") + .httpTypes("HTTPRequest") + } + + /// Returns the type name for the body type. + static var body: TypeName { + .runtime("HTTPBody") } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 47d55b4f..de409a25 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -295,7 +295,7 @@ struct TypeMatcher { case .byte: typeName = .swift("String") case .binary: - typeName = .foundation("Data") + typeName = .body case .dateTime: typeName = .foundation("Date") default: diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift index 8fb073d2..3708bd36 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift @@ -37,7 +37,7 @@ extension TypesFileTranslator { keywords: [ .throws ], - returnType: Constants.ServerURL.underlyingType, + returnType: .identifier(Constants.ServerURL.underlyingType), body: [ .expression( .try( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md b/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md index a8843ea4..1d01d37b 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md @@ -56,9 +56,6 @@ Below is a list of the "dimensions" across which the helper methods differ: - `urlEncodedForm` - example: request body with the `application/x-www-form-urlencoded` content type - `greeting=Hello+world` - - `string` - - example: `text/plain`, and any other `text/*` content type - - `"red color and power of 24"` - `binary` - example: `application/octet-stream` - serves as the fallback for content types that don't have more specific handling @@ -91,28 +88,22 @@ method parameters: value or type of value | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | | client | set | request path | URI | required | renderedPath | | client | set | request query | URI | both | setQueryItemAsURI | -| client | set | request body | string | optional | setOptionalRequestBodyAsString | -| client | set | request body | string | required | setRequiredRequestBodyAsString | | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | | client | set | request body | urlEncodedForm | optional | setOptionalRequestBodyAsURLEncodedForm | | client | set | request body | urlEncodedForm | required | setRequiredRequestBodyAsURLEncodedForm | -| client | get | response body | string | required | getResponseBodyAsString | | client | get | response body | JSON | required | getResponseBodyAsJSON | | client | get | response body | binary | required | getResponseBodyAsBinary | | server | get | request path | URI | required | getPathParameterAsURI | | server | get | request query | URI | optional | getOptionalQueryItemAsURI | | server | get | request query | URI | required | getRequiredQueryItemAsURI | -| server | get | request body | string | optional | getOptionalRequestBodyAsString | -| server | get | request body | string | required | getRequiredRequestBodyAsString | | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | | server | get | request body | urlEncodedForm | optional | getOptionalRequestBodyAsURLEncodedForm | | server | get | request body | urlEncodedForm | required | getRequiredRequestBodyAsURLEncodedForm | -| server | set | response body | string | required | setResponseBodyAsString | | server | set | response body | JSON | required | setResponseBodyAsJSON | | server | set | response body | binary | required | setResponseBodyAsBinary | diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.2.swift index 763fe88a..6ca9b71b 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.2.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.3.0")), ], targets: [ .executableTarget( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.3.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.3.swift index 3a699207..1534bc88 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.3.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.3.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.3.0")), ], targets: [ .executableTarget( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.4.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.4.swift index b15d414f..26985fa5 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.4.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.4.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.3.0")), ], targets: [ .executableTarget( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.5.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.5.swift index b15d414f..26985fa5 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.5.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.Package.5.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-urlsession", .upToNextMinor(from: "0.3.0")), ], targets: [ .executableTarget( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.2.swift index feda5157..f46c89f1 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.2.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/vapor/vapor", from: "4.76.0"), ], targets: [ diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.3.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.3.swift index 266c4f85..863add15 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.3.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.3.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/vapor/vapor", from: "4.76.0"), ], targets: [ diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.4.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.4.swift index 72a1a01d..b5dcd8a5 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.4.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.4.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/vapor/vapor", from: "4.76.0"), ], targets: [ diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.5.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.5.swift index 72a1a01d..b5dcd8a5 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.5.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.Package.5.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v10_15) ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0")), - .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/vapor/vapor", from: "4.76.0"), ], targets: [ diff --git a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift index 35cada04..f6e90cb9 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift @@ -535,7 +535,7 @@ final class Test_TextBasedRenderer: XCTestCase { kind: .function(name: "f"), parameters: [], keywords: [.async, .throws], - returnType: "String" + returnType: .identifier("String") ), renderedBy: renderer.renderedFunction, rendersAs: diff --git a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift index 823dd10d..38b4afbc 100644 --- a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift +++ b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift @@ -58,6 +58,7 @@ enum ExprKind: String, Equatable, CustomStringConvertible { case binaryOperation case inOut case optionalChaining + case tuple var description: String { rawValue @@ -227,6 +228,8 @@ extension Expression { return .init(name: value.referencedExpr.info.name, kind: .inOut) case .optionalChaining(let value): return .init(name: value.referencedExpr.info.name, kind: .optionalChaining) + case .tuple(_): + return .init(name: nil, kind: .tuple) } } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift index 18238256..db7b51de 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentType.swift @@ -81,7 +81,7 @@ final class Test_ContentType: Test_Core { ), ( "text/plain", - .text, + .binary, "text", "plain", "", diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index 806a4474..94b1f11e 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -25,7 +25,7 @@ final class Test_TypeMatcher: Test_Core { static let builtinTypes: [(JSONSchema, String)] = [ (.string, "Swift.String"), (.string(.init(format: .byte), .init()), "Swift.String"), - (.string(.init(format: .binary), .init()), "Foundation.Data"), + (.string(.init(format: .binary), .init()), "OpenAPIRuntime.HTTPBody"), (.string(.init(format: .date), .init()), "Swift.String"), (.string(.init(format: .dateTime), .init()), "Foundation.Date"), diff --git a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift index 3a1bb129..c34a68df 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift @@ -266,7 +266,7 @@ fileprivate extension CompatibilityTest { let package = Package( name: "\(packageName)", platforms: [.macOS(.v13)], - dependencies: [.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.2.0"))], + dependencies: [.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0"))], targets: [.target(name: "Harness", dependencies: [.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")])] ) """ diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index bd036823..46b60606 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -9,6 +9,7 @@ import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date #endif +import HTTPTypes /// Service for managing pet metadata. /// /// Because why not. @@ -49,7 +50,7 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.listPets.id, serializer: { input in let path = try converter.renderedPath(template: "/pets", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get) suppressMutabilityWarning(&request) try converter.setQueryItemAsURI( in: &request, @@ -85,10 +86,10 @@ public struct Client: APIProtocol { value: input.query.since ) converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) - return request + return (request, nil) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 200: let headers: Operations.listPets.Output.Ok.Headers = .init( My_hyphen_Response_hyphen_UUID: try converter.getRequiredHeaderFieldAsURI( @@ -107,9 +108,9 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Components.Schemas.Pets.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { @@ -122,15 +123,15 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Components.Schemas._Error.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } - return .`default`(statusCode: response.statusCode, .init(body: body)) + return .`default`(statusCode: response.status.code, .init(body: body)) } } ) @@ -144,7 +145,7 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.createPet.id, serializer: { input in let path = try converter.renderedPath(template: "/pets", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .post) suppressMutabilityWarning(&request) try converter.setHeaderFieldAsJSON( in: &request.headerFields, @@ -152,18 +153,19 @@ public struct Client: APIProtocol { value: input.headers.X_hyphen_Extra_hyphen_Arguments ) converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) + let body: OpenAPIRuntime.HTTPBody? switch input.body { case let .json(value): - request.body = try converter.setRequiredRequestBodyAsJSON( + body = try converter.setRequiredRequestBodyAsJSON( value, headerFields: &request.headerFields, contentType: "application/json; charset=utf-8" ) } - return request + return (request, body) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 201: let headers: Operations.createPet.Output.Created.Headers = .init( X_hyphen_Extra_hyphen_Arguments: try converter.getOptionalHeaderFieldAsJSON( @@ -177,9 +179,9 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Components.Schemas.Pet.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { @@ -199,16 +201,16 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Components.Responses.ErrorBadRequest.Body.jsonPayload.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } - return .clientError(statusCode: response.statusCode, .init(headers: headers, body: body)) - default: return .undocumented(statusCode: response.statusCode, .init()) + return .clientError(statusCode: response.status.code, .init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -224,22 +226,23 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.createPetWithForm.id, serializer: { input in let path = try converter.renderedPath(template: "/pets/create", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .post) suppressMutabilityWarning(&request) + let body: OpenAPIRuntime.HTTPBody? switch input.body { case let .urlEncodedForm(value): - request.body = try converter.setRequiredRequestBodyAsURLEncodedForm( + body = try converter.setRequiredRequestBodyAsURLEncodedForm( value, headerFields: &request.headerFields, contentType: "application/x-www-form-urlencoded" ) } - return request + return (request, body) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 204: return .noContent(.init()) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -251,28 +254,28 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.getStats.id, serializer: { input in let path = try converter.renderedPath(template: "/pets/stats", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get) suppressMutabilityWarning(&request) converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) - return request + return (request, nil) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 200: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.getStats.Output.Ok.Body if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Components.Schemas.PetStats.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else if try converter.isMatchingContentType(received: contentType, expectedRaw: "text/plain") { - body = try converter.getResponseBodyAsString( - Swift.String.self, - from: response.body, + body = try converter.getResponseBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: responseBody, transforming: { value in .plainText(value) } ) } else if try converter.isMatchingContentType( @@ -280,15 +283,15 @@ public struct Client: APIProtocol { expectedRaw: "application/octet-stream" ) { body = try converter.getResponseBodyAsBinary( - Foundation.Data.self, - from: response.body, + OpenAPIRuntime.HTTPBody.self, + from: responseBody, transforming: { value in .binary(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } return .ok(.init(body: body)) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -300,34 +303,35 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.postStats.id, serializer: { input in let path = try converter.renderedPath(template: "/pets/stats", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .post) suppressMutabilityWarning(&request) + let body: OpenAPIRuntime.HTTPBody? switch input.body { case let .json(value): - request.body = try converter.setRequiredRequestBodyAsJSON( + body = try converter.setRequiredRequestBodyAsJSON( value, headerFields: &request.headerFields, contentType: "application/json; charset=utf-8" ) case let .plainText(value): - request.body = try converter.setRequiredRequestBodyAsString( + body = try converter.setRequiredRequestBodyAsBinary( value, headerFields: &request.headerFields, contentType: "text/plain" ) case let .binary(value): - request.body = try converter.setRequiredRequestBodyAsBinary( + body = try converter.setRequiredRequestBodyAsBinary( value, headerFields: &request.headerFields, contentType: "application/octet-stream" ) } - return request + return (request, body) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 202: return .accepted(.init()) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -339,14 +343,14 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.probe.id, serializer: { input in let path = try converter.renderedPath(template: "/probe/", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .post) suppressMutabilityWarning(&request) - return request + return (request, nil) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 204: return .noContent(.init()) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -361,22 +365,23 @@ public struct Client: APIProtocol { forOperation: Operations.updatePet.id, serializer: { input in let path = try converter.renderedPath(template: "/pets/{}", parameters: [input.path.petId]) - var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .patch) suppressMutabilityWarning(&request) converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) + let body: OpenAPIRuntime.HTTPBody? switch input.body { - case .none: request.body = nil + case .none: body = nil case let .json(value): - request.body = try converter.setOptionalRequestBodyAsJSON( + body = try converter.setOptionalRequestBodyAsJSON( value, headerFields: &request.headerFields, contentType: "application/json; charset=utf-8" ) } - return request + return (request, body) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 204: return .noContent(.init()) case 400: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) @@ -384,16 +389,16 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } return .badRequest(.init(body: body)) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) @@ -410,21 +415,22 @@ public struct Client: APIProtocol { forOperation: Operations.uploadAvatarForPet.id, serializer: { input in let path = try converter.renderedPath(template: "/pets/{}/avatar", parameters: [input.path.petId]) - var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .put) suppressMutabilityWarning(&request) converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) + let body: OpenAPIRuntime.HTTPBody? switch input.body { case let .binary(value): - request.body = try converter.setRequiredRequestBodyAsBinary( + body = try converter.setRequiredRequestBodyAsBinary( value, headerFields: &request.headerFields, contentType: "application/octet-stream" ) } - return request + return (request, body) }, - deserializer: { response in - switch response.statusCode { + deserializer: { response, responseBody in + switch response.status.code { case 200: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.uploadAvatarForPet.Output.Ok.Body @@ -435,8 +441,8 @@ public struct Client: APIProtocol { ) { body = try converter.getResponseBodyAsBinary( - Foundation.Data.self, - from: response.body, + OpenAPIRuntime.HTTPBody.self, + from: responseBody, transforming: { value in .binary(value) } ) } else { @@ -449,9 +455,9 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getResponseBodyAsJSON( + body = try await converter.getResponseBodyAsJSON( Swift.String.self, - from: response.body, + from: responseBody, transforming: { value in .json(value) } ) } else { @@ -464,16 +470,16 @@ public struct Client: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "text/plain") { - body = try converter.getResponseBodyAsString( - Swift.String.self, - from: response.body, + body = try converter.getResponseBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: responseBody, transforming: { value in .plainText(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } return .internalServerError(.init(body: body)) - default: return .undocumented(statusCode: response.statusCode, .init()) + default: return .undocumented(statusCode: response.status.code, .init()) } } ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index 9effb5e6..e8c32ae2 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -9,6 +9,7 @@ import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date #endif +import HTTPTypes extension APIProtocol { /// Registers each operation handler with the provided transport. /// - Parameters: @@ -30,52 +31,44 @@ extension APIProtocol { middlewares: middlewares ) try transport.register( - { try await server.listPets(request: $0, metadata: $1) }, + { try await server.listPets(request: $0, body: $1, metadata: $2) }, method: .get, - path: server.apiPathComponentsWithServerPrefix(["pets"]), - queryItemNames: ["limit", "habitat", "feeds", "since"] + path: server.apiPathComponentsWithServerPrefix("/pets") ) try transport.register( - { try await server.createPet(request: $0, metadata: $1) }, + { try await server.createPet(request: $0, body: $1, metadata: $2) }, method: .post, - path: server.apiPathComponentsWithServerPrefix(["pets"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets") ) try transport.register( - { try await server.createPetWithForm(request: $0, metadata: $1) }, + { try await server.createPetWithForm(request: $0, body: $1, metadata: $2) }, method: .post, - path: server.apiPathComponentsWithServerPrefix(["pets", "create"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets/create") ) try transport.register( - { try await server.getStats(request: $0, metadata: $1) }, + { try await server.getStats(request: $0, body: $1, metadata: $2) }, method: .get, - path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets/stats") ) try transport.register( - { try await server.postStats(request: $0, metadata: $1) }, + { try await server.postStats(request: $0, body: $1, metadata: $2) }, method: .post, - path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets/stats") ) try transport.register( - { try await server.probe(request: $0, metadata: $1) }, + { try await server.probe(request: $0, body: $1, metadata: $2) }, method: .post, - path: server.apiPathComponentsWithServerPrefix(["probe"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/probe/") ) try transport.register( - { try await server.updatePet(request: $0, metadata: $1) }, + { try await server.updatePet(request: $0, body: $1, metadata: $2) }, method: .patch, - path: server.apiPathComponentsWithServerPrefix(["pets", ":petId"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets/{petId}") ) try transport.register( - { try await server.uploadAvatarForPet(request: $0, metadata: $1) }, + { try await server.uploadAvatarForPet(request: $0, body: $1, metadata: $2) }, method: .put, - path: server.apiPathComponentsWithServerPrefix(["pets", ":petId", "avatar"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/pets/{petId}/avatar") ) } } @@ -87,37 +80,40 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { /// /// - Remark: HTTP `GET /pets`. /// - Remark: Generated from `#/paths//pets/get(listPets)`. - func listPets(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func listPets(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.listPets.id, using: { APIHandler.listPets($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let query: Operations.listPets.Input.Query = .init( limit: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "limit", as: Swift.Int32.self ), habitat: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "habitat", as: Operations.listPets.Input.Query.habitatPayload.self ), feeds: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "feeds", as: Operations.listPets.Input.Query.feedsPayload.self ), since: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "since", @@ -138,7 +134,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .ok(value): suppressUnusedWarning(value) - var response = Response(statusCode: 200) + var response = HTTPResponse(soar_statusCode: 200) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsURI( in: &response.headerFields, @@ -150,30 +146,32 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { name: "My-Tracing-Header", value: value.headers.My_hyphen_Tracing_hyphen_Header ) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response + return (response, body) case let .`default`(statusCode, value): suppressUnusedWarning(value) - var response = Response(statusCode: statusCode) + var response = HTTPResponse(soar_statusCode: statusCode) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response + return (response, body) } } ) @@ -182,13 +180,16 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { /// /// - Remark: HTTP `POST /pets`. /// - Remark: Generated from `#/paths//pets/post(createPet)`. - func createPet(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func createPet(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.createPet.id, using: { APIHandler.createPet($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let headers: Operations.createPet.Input.Headers = .init( X_hyphen_Extra_hyphen_Arguments: try converter.getOptionalHeaderFieldAsJSON( in: request.headerFields, @@ -202,9 +203,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getRequiredRequestBodyAsJSON( + body = try await converter.getRequiredRequestBodyAsJSON( Components.Schemas.CreatePetRequest.self, - from: request.body, + from: requestBody, transforming: { value in .json(value) } ) } else { @@ -216,43 +217,45 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .created(value): suppressUnusedWarning(value) - var response = Response(statusCode: 201) + var response = HTTPResponse(soar_statusCode: 201) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsJSON( in: &response.headerFields, name: "X-Extra-Arguments", value: value.headers.X_hyphen_Extra_hyphen_Arguments ) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response + return (response, body) case let .clientError(statusCode, value): suppressUnusedWarning(value) - var response = Response(statusCode: statusCode) + var response = HTTPResponse(soar_statusCode: statusCode) suppressMutabilityWarning(&response) try converter.setHeaderFieldAsURI( in: &response.headerFields, name: "X-Reason", value: value.headers.X_hyphen_Reason ) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, body) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) @@ -261,13 +264,16 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { /// /// - Remark: HTTP `POST /pets/create`. /// - Remark: Generated from `#/paths//pets/create/post(createPetWithForm)`. - func createPetWithForm(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func createPetWithForm(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.createPetWithForm.id, using: { APIHandler.createPetWithForm($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.createPetWithForm.Input.Body if try contentType == nil @@ -276,9 +282,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { expectedRaw: "application/x-www-form-urlencoded" ) { - body = try converter.getRequiredRequestBodyAsURLEncodedForm( + body = try await converter.getRequiredRequestBodyAsURLEncodedForm( Components.Schemas.CreatePetRequest.self, - from: request.body, + from: requestBody, transforming: { value in .urlEncodedForm(value) } ) } else { @@ -290,23 +296,26 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .noContent(value): suppressUnusedWarning(value) - var response = Response(statusCode: 204) + var response = HTTPResponse(soar_statusCode: 204) suppressMutabilityWarning(&response) - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, nil) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) } /// - Remark: HTTP `GET /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. - func getStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func getStats(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.getStats.id, using: { APIHandler.getStats($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let headers: Operations.getStats.Input.Headers = .init( accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) @@ -316,60 +325,64 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .ok(value): suppressUnusedWarning(value) - var response = Response(statusCode: 200) + var response = HTTPResponse(soar_statusCode: 200) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) case let .plainText(value): try converter.validateAcceptIfPresent("text/plain", in: request.headerFields) - response.body = try converter.setResponseBodyAsString( + body = try converter.setResponseBodyAsBinary( value, headerFields: &response.headerFields, contentType: "text/plain" ) case let .binary(value): try converter.validateAcceptIfPresent("application/octet-stream", in: request.headerFields) - response.body = try converter.setResponseBodyAsBinary( + body = try converter.setResponseBodyAsBinary( value, headerFields: &response.headerFields, contentType: "application/octet-stream" ) } - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, body) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) } /// - Remark: HTTP `POST /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. - func postStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func postStats(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.postStats.id, using: { APIHandler.postStats($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.postStats.Input.Body if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getRequiredRequestBodyAsJSON( + body = try await converter.getRequiredRequestBodyAsJSON( Components.Schemas.PetStats.self, - from: request.body, + from: requestBody, transforming: { value in .json(value) } ) } else if try converter.isMatchingContentType(received: contentType, expectedRaw: "text/plain") { - body = try converter.getRequiredRequestBodyAsString( - Swift.String.self, - from: request.body, + body = try converter.getRequiredRequestBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: requestBody, transforming: { value in .plainText(value) } ) } else if try converter.isMatchingContentType( @@ -377,8 +390,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { expectedRaw: "application/octet-stream" ) { body = try converter.getRequiredRequestBodyAsBinary( - Foundation.Data.self, - from: request.body, + OpenAPIRuntime.HTTPBody.self, + from: requestBody, transforming: { value in .binary(value) } ) } else { @@ -390,31 +403,34 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .accepted(value): suppressUnusedWarning(value) - var response = Response(statusCode: 202) + var response = HTTPResponse(soar_statusCode: 202) suppressMutabilityWarning(&response) - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, nil) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) } /// - Remark: HTTP `POST /probe/`. /// - Remark: Generated from `#/paths//probe//post(probe)`. - func probe(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func probe(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.probe.id, using: { APIHandler.probe($0) }, - deserializer: { request, metadata in return Operations.probe.Input() }, + deserializer: { request, requestBody, metadata in return Operations.probe.Input() }, serializer: { output, request in switch output { case let .noContent(value): suppressUnusedWarning(value) - var response = Response(statusCode: 204) + var response = HTTPResponse(soar_statusCode: 204) suppressMutabilityWarning(&response) - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, nil) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) @@ -423,13 +439,16 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { /// /// - Remark: HTTP `PATCH /pets/{petId}`. /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)`. - func updatePet(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func updatePet(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.updatePet.id, using: { APIHandler.updatePet($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let path: Operations.updatePet.Input.Path = .init( petId: try converter.getPathParameterAsURI( in: metadata.pathParameters, @@ -445,9 +464,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { if try contentType == nil || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") { - body = try converter.getOptionalRequestBodyAsJSON( + body = try await converter.getOptionalRequestBodyAsJSON( Components.RequestBodies.UpdatePetRequest.jsonPayload.self, - from: request.body, + from: requestBody, transforming: { value in .json(value) } ) } else { @@ -459,24 +478,25 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .noContent(value): suppressUnusedWarning(value) - var response = Response(statusCode: 204) + var response = HTTPResponse(soar_statusCode: 204) suppressMutabilityWarning(&response) - return response + return (response, nil) case let .badRequest(value): suppressUnusedWarning(value) - var response = Response(statusCode: 400) + var response = HTTPResponse(soar_statusCode: 400) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, body) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) @@ -485,13 +505,16 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { /// /// - Remark: HTTP `PUT /pets/{petId}/avatar`. /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)`. - func uploadAvatarForPet(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + func uploadAvatarForPet(request: HTTPRequest, body: HTTPBody?, metadata: ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await handle( request: request, - with: metadata, + requestBody: body, + metadata: metadata, forOperation: Operations.uploadAvatarForPet.id, using: { APIHandler.uploadAvatarForPet($0) }, - deserializer: { request, metadata in + deserializer: { request, requestBody, metadata in let path: Operations.uploadAvatarForPet.Input.Path = .init( petId: try converter.getPathParameterAsURI( in: metadata.pathParameters, @@ -508,8 +531,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { || converter.isMatchingContentType(received: contentType, expectedRaw: "application/octet-stream") { body = try converter.getRequiredRequestBodyAsBinary( - Foundation.Data.self, - from: request.body, + OpenAPIRuntime.HTTPBody.self, + from: requestBody, transforming: { value in .binary(value) } ) } else { @@ -521,47 +544,50 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { switch output { case let .ok(value): suppressUnusedWarning(value) - var response = Response(statusCode: 200) + var response = HTTPResponse(soar_statusCode: 200) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .binary(value): try converter.validateAcceptIfPresent("application/octet-stream", in: request.headerFields) - response.body = try converter.setResponseBodyAsBinary( + body = try converter.setResponseBodyAsBinary( value, headerFields: &response.headerFields, contentType: "application/octet-stream" ) } - return response + return (response, body) case let .preconditionFailed(value): suppressUnusedWarning(value) - var response = Response(statusCode: 412) + var response = HTTPResponse(soar_statusCode: 412) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .json(value): try converter.validateAcceptIfPresent("application/json", in: request.headerFields) - response.body = try converter.setResponseBodyAsJSON( + body = try converter.setResponseBodyAsJSON( value, headerFields: &response.headerFields, contentType: "application/json; charset=utf-8" ) } - return response + return (response, body) case let .internalServerError(value): suppressUnusedWarning(value) - var response = Response(statusCode: 500) + var response = HTTPResponse(soar_statusCode: 500) suppressMutabilityWarning(&response) + let body: HTTPBody switch value.body { case let .plainText(value): try converter.validateAcceptIfPresent("text/plain", in: request.headerFields) - response.body = try converter.setResponseBodyAsString( + body = try converter.setResponseBodyAsBinary( value, headerFields: &response.headerFields, contentType: "text/plain" ) } - return response - case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + return (response, body) + case let .undocumented(statusCode, _): return (.init(soar_statusCode: statusCode), nil) } } ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 15e708ea..89b5df17 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -1087,9 +1087,9 @@ public enum Operations { /// - Remark: Generated from `#/paths/pets/stats/GET/responses/200/content/application\/json`. case json(Components.Schemas.PetStats) /// - Remark: Generated from `#/paths/pets/stats/GET/responses/200/content/text\/plain`. - case plainText(Swift.String) + case plainText(OpenAPIRuntime.HTTPBody) /// - Remark: Generated from `#/paths/pets/stats/GET/responses/200/content/application\/octet-stream`. - case binary(Foundation.Data) + case binary(OpenAPIRuntime.HTTPBody) } /// Received HTTP response body public var body: Operations.getStats.Output.Ok.Body @@ -1144,9 +1144,9 @@ public enum Operations { /// - Remark: Generated from `#/paths/pets/stats/POST/requestBody/content/application\/json`. case json(Components.Schemas.PetStats) /// - Remark: Generated from `#/paths/pets/stats/POST/requestBody/content/text\/plain`. - case plainText(Swift.String) + case plainText(OpenAPIRuntime.HTTPBody) /// - Remark: Generated from `#/paths/pets/stats/POST/requestBody/content/application\/octet-stream`. - case binary(Foundation.Data) + case binary(OpenAPIRuntime.HTTPBody) } public var body: Operations.postStats.Input.Body /// Creates a new `Input`. @@ -1350,7 +1350,7 @@ public enum Operations { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/requestBody`. @frozen public enum Body: Sendable, Hashable { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/requestBody/content/application\/octet-stream`. - case binary(Foundation.Data) + case binary(OpenAPIRuntime.HTTPBody) } public var body: Operations.uploadAvatarForPet.Input.Body /// Creates a new `Input`. @@ -1374,7 +1374,7 @@ public enum Operations { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/responses/200/content`. @frozen public enum Body: Sendable, Hashable { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/responses/200/content/application\/octet-stream`. - case binary(Foundation.Data) + case binary(OpenAPIRuntime.HTTPBody) } /// Received HTTP response body public var body: Operations.uploadAvatarForPet.Output.Ok.Body @@ -1414,7 +1414,7 @@ public enum Operations { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/responses/500/content`. @frozen public enum Body: Sendable, Hashable { /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/responses/500/content/text\/plain`. - case plainText(Swift.String) + case plainText(OpenAPIRuntime.HTTPBody) } /// Received HTTP response body public var body: Operations.uploadAvatarForPet.Output.InternalServerError.Body diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 80370625..dfa7f950 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1016,8 +1016,8 @@ final class SnippetBasedReferenceTests: XCTestCase { @frozen public enum Body: Sendable, Hashable { case json(Swift.Int) case application_json_foo_bar(Swift.Int) - case plainText(Swift.String) - case binary(Foundation.Data) + case plainText(OpenAPIRuntime.HTTPBody) + case binary(OpenAPIRuntime.HTTPBody) } public var body: Components.Responses.MultipleContentTypes.Body public init( @@ -1150,8 +1150,8 @@ final class SnippetBasedReferenceTests: XCTestCase { public enum RequestBodies { @frozen public enum MyResponseBody: Sendable, Hashable { case json(Components.Schemas.MyBody) - case plainText(Swift.String) - case binary(Foundation.Data) + case plainText(OpenAPIRuntime.HTTPBody) + case binary(OpenAPIRuntime.HTTPBody) } } """ @@ -1175,7 +1175,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public enum RequestBodies { @frozen public enum MyRequestBody: Sendable, Hashable { - case urlEncodedForm(Foundation.Data) + case urlEncodedForm(OpenAPIRuntime.HTTPBody) } } """ @@ -1259,10 +1259,9 @@ final class SnippetBasedReferenceTests: XCTestCase { middlewares: middlewares ) try transport.register( - { try await server.getHealth(request: $0, metadata: $1) }, + { try await server.getHealth(request: $0, body: $1, metadata: $2) }, method: .get, - path: server.apiPathComponentsWithServerPrefix(["health"]), - queryItemNames: [] + path: server.apiPathComponentsWithServerPrefix("/health") ) } """ @@ -1427,7 +1426,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """, client: """ { input in let path = try converter.renderedPath(template: "/foo", parameters: []) - var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get) suppressMutabilityWarning(&request) try converter.setQueryItemAsURI( in: &request, @@ -1450,28 +1449,28 @@ final class SnippetBasedReferenceTests: XCTestCase { name: "manyUnexploded", value: input.query.manyUnexploded ) - return request + return (request, nil) } """, server: """ - { request, metadata in + { request, requestBody, metadata in let query: Operations.get_sol_foo.Input.Query = .init( single: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "single", as: Swift.String.self ), manyExploded: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: true, name: "manyExploded", as: [Swift.String].self ), manyUnexploded: try converter.getOptionalQueryItemAsURI( - in: request.query, + in: request.soar_query, style: .form, explode: false, name: "manyUnexploded", diff --git a/Tests/PetstoreConsumerTests/Common.swift b/Tests/PetstoreConsumerTests/Common.swift index b8b719c4..cd544454 100644 --- a/Tests/PetstoreConsumerTests/Common.swift +++ b/Tests/PetstoreConsumerTests/Common.swift @@ -12,9 +12,16 @@ // //===----------------------------------------------------------------------===// import XCTest +import HTTPTypes extension Operations.listPets.Output { static var success: Self { .ok(.init(headers: .init(My_hyphen_Response_hyphen_UUID: "abcd"), body: .json([]))) } } + +extension HTTPRequest { + public init(soar_path path: String, method: Method, headerFields: HTTPFields = .init()) { + self.init(method: method, scheme: nil, authority: nil, path: path, headerFields: headerFields) + } +} diff --git a/Tests/PetstoreConsumerTests/TestServer.swift b/Tests/PetstoreConsumerTests/TestServer.swift index 6b48be71..c688a7e1 100644 --- a/Tests/PetstoreConsumerTests/TestServer.swift +++ b/Tests/PetstoreConsumerTests/TestServer.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import OpenAPIRuntime +import HTTPTypes import Foundation import PetstoreConsumerTestCore @@ -31,8 +32,8 @@ extension APIProtocol { extension TestServerTransport { private func findHandler( - method: HTTPMethod, - path: [RouterPathComponent] + method: HTTPRequest.Method, + path: String ) throws -> TestServerTransport.Handler { guard let handler = registered.first(where: { operation in @@ -54,7 +55,7 @@ extension TestServerTransport { get throws { try findHandler( method: .get, - path: ["api", "pets"] + path: "/api/pets" ) } } @@ -63,7 +64,7 @@ extension TestServerTransport { get throws { try findHandler( method: .post, - path: ["api", "pets"] + path: "/api/pets" ) } } @@ -72,7 +73,7 @@ extension TestServerTransport { get throws { try findHandler( method: .post, - path: ["api", "pets", "create"] + path: "/api/pets/create" ) } } @@ -81,7 +82,7 @@ extension TestServerTransport { get throws { try findHandler( method: .patch, - path: ["api", "pets", ":petId"] + path: "/api/pets/{petId}" ) } } @@ -90,7 +91,7 @@ extension TestServerTransport { get throws { try findHandler( method: .get, - path: ["api", "pets", "stats"] + path: "/api/pets/stats" ) } } @@ -99,7 +100,7 @@ extension TestServerTransport { get throws { try findHandler( method: .post, - path: ["api", "pets", "stats"] + path: "/api/pets/stats" ) } } @@ -108,7 +109,7 @@ extension TestServerTransport { get throws { try findHandler( method: .post, - path: ["api", "probe"] + path: "/api/probe/" ) } } @@ -117,7 +118,7 @@ extension TestServerTransport { get throws { try findHandler( method: .put, - path: ["api", "pets", ":petId", "avatar"] + path: "/api/pets/{petId}/avatar" ) } } diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 3c7da8db..19a587ec 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import XCTest import OpenAPIRuntime +import HTTPTypes import PetstoreConsumerTestCore final class Test_Client: XCTestCase { @@ -33,38 +34,39 @@ final class Test_Client: XCTestCase { } func testListPets_200() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { (request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String) in XCTAssertEqual(operationID, "listPets") - XCTAssertEqual(request.path, "/pets") XCTAssertEqual( - request.query, - "limit=24&habitat=water&feeds=herbivore&feeds=carnivore&since=2023-01-18T10%3A04%3A11Z" + request.path, + "/pets?limit=24&habitat=water&feeds=herbivore&feeds=carnivore&since=2023-01-18T10%3A04%3A11Z" ) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "My-Request-UUID", value: "abcd-1234"), - .init(name: "accept", value: "application/json"), + .accept: "application/json", + .init("My-Request-UUID")!: "abcd-1234", ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "application/json"), - .init(name: "my-response-uuid", value: "abcd"), - .init(name: "my-tracing-header", value: "1234"), - ], - encodedBody: #""" - [ - { - "id": 1, - "name": "Fluffz" - } - ] - """# + XCTAssertNil(body) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "application/json", + .init("my-response-uuid")!: "abcd", + .init("my-tracing-header")!: "1234", + ] + ) + .withEncodedBody( + #""" + [ + { + "id": 1, + "name": "Fluffz" + } + ] + """# ) } let response = try await client.listPets( @@ -93,31 +95,32 @@ final class Test_Client: XCTestCase { } func testListPets_default() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, body, baseURL, operationID in XCTAssertEqual(operationID, "listPets") - XCTAssertEqual(request.path, "/pets") - XCTAssertEqual(request.query, "limit=24") + XCTAssertEqual(request.path, "/pets?limit=24") XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json") + .accept: "application/json" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 400, - headers: [ - .init(name: "content-type", value: "application/json") - ], - encodedBody: #""" - { - "code": 1, - "me$sage": "Oh no!", - "userData": {"one" : 1} - } - """# + XCTAssertNil(body) + return try HTTPResponse( + status: .badRequest, + headerFields: [ + .contentType: "application/json" + ] + ) + .withEncodedBody( + #""" + { + "code": 1, + "me$sage": "Oh no!", + "userData": {"one" : 1} + } + """# ) } let response = try await client.listPets( @@ -144,40 +147,47 @@ final class Test_Client: XCTestCase { } func testCreatePet_201() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, body, baseURL, operationID in XCTAssertEqual(operationID, "createPet") XCTAssertEqual(request.path, "/pets") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) XCTAssertEqual( request.headerFields, [ - .init(name: "X-Extra-Arguments", value: #"{"code":1}"#), - .init(name: "accept", value: "application/json"), - .init(name: "content-type", value: "application/json; charset=utf-8"), + .accept: "application/json", + .contentType: "application/json; charset=utf-8", + .init("X-Extra-Arguments")!: #"{"code":1}"#, ] ) + let bodyString: String + if let body { + bodyString = try await String(collecting: body, upTo: .max) + } else { + bodyString = "" + } XCTAssertEqual( - request.body?.pretty, + bodyString, #""" { "name" : "Fluffz" } """# ) - return .init( - statusCode: 201, - headers: [ - .init(name: "content-type", value: "application/json; charset=utf-8"), - .init(name: "x-extra-arguments", value: #"{"code":1}"#), - ], - encodedBody: #""" - { - "id": 1, - "name": "Fluffz" - } - """# + return try HTTPResponse( + status: .created, + headerFields: [ + .contentType: "application/json; charset=utf-8", + .init("x-extra-arguments")!: #"{"code":1}"#, + ] + ) + .withEncodedBody( + #""" + { + "id": 1, + "name": "Fluffz" + } + """# ) } let response = try await client.createPet( @@ -200,18 +210,20 @@ final class Test_Client: XCTestCase { } func testCreatePet_400() async throws { - transport = .init { request, baseURL, operationID in - .init( - statusCode: 400, - headers: [ - .init(name: "content-type", value: "application/json; charset=utf-8"), - .init(name: "x-reason", value: "bad luck"), - ], - encodedBody: #""" - { - "code": 1 - } - """# + transport = .init { request, body, baseURL, operationID in + try HTTPResponse( + status: .badRequest, + headerFields: [ + .contentType: "application/json; charset=utf-8", + .init("x-reason")!: "bad luck", + ] + ) + .withEncodedBody( + #""" + { + "code": 1 + } + """# ) } let response = try await client.createPet( @@ -230,24 +242,31 @@ final class Test_Client: XCTestCase { } func testCreatePetWithForm_204() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, body, baseURL, operationID in XCTAssertEqual(operationID, "createPetWithForm") XCTAssertEqual(request.path, "/pets/create") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) XCTAssertEqual( request.headerFields, [ - .init(name: "content-type", value: "application/x-www-form-urlencoded") + .contentType: "application/x-www-form-urlencoded" ] ) + let bodyString: String + if let body { + bodyString = try await String(collecting: body, upTo: .max) + } else { + bodyString = "" + } XCTAssertEqual( - request.body?.pretty, + bodyString, "name=Fluffz" ) - - return .init(statusCode: 204) + return ( + HTTPResponse(status: .noContent), + nil + ) } let response = try await client.createPetWithForm( .init( @@ -262,32 +281,34 @@ final class Test_Client: XCTestCase { } func testUpdatePet_204_withBody() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "updatePet") XCTAssertEqual(request.path, "/pets/1") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .patch) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json"), - .init(name: "content-type", value: "application/json; charset=utf-8"), + .accept: "application/json", + .contentType: "application/json; charset=utf-8", ] ) - XCTAssertEqual( - request.body?.pretty, + try await XCTAssertEqualStringifiedData( + requestBody, #""" { "name" : "Fluffz" } """# ) - return .init( - statusCode: 204, - headerFields: [ - .init(name: "content-type", value: "application/json") - ] + return ( + HTTPResponse( + status: .noContent, + headerFields: [ + .contentType: "application/json" + ] + ), + nil ) } let response = try await client.updatePet( @@ -303,24 +324,26 @@ final class Test_Client: XCTestCase { } func testUpdatePet_204_withoutBody() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "updatePet") XCTAssertEqual(request.path, "/pets/1") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .patch) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json") + .accept: "application/json" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 204, - headerFields: [ - .init(name: "content-type", value: "application/json") - ] + XCTAssertNil(requestBody) + return ( + HTTPResponse( + status: .noContent, + headerFields: [ + .contentType: "application/json" + ] + ), + .init() ) } let response = try await client.updatePet( @@ -335,29 +358,30 @@ final class Test_Client: XCTestCase { } func testUpdatePet_400() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "updatePet") XCTAssertEqual(request.path, "/pets/1") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .patch) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json") + .accept: "application/json" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 400, - headers: [ - .init(name: "content-type", value: "application/json") - ], - encodedBody: #""" - { - "message" : "Oh no!" - } - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .badRequest, + headerFields: [ + .contentType: "application/json" + ] + ) + .withEncodedBody( + #""" + { + "message" : "Oh no!" + } + """# ) } let response = try await client.updatePet( @@ -376,27 +400,29 @@ final class Test_Client: XCTestCase { } func testGetStats_200_json() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + .accept: "application/json, text/plain, application/octet-stream" + ] + ) + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "application/json" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "application/json") - ], - encodedBody: #""" - { - "count" : 1 - } - """# + .withEncodedBody( + #""" + { + "count" : 1 + } + """# ) } let response = try await client.getStats(.init()) @@ -413,19 +439,20 @@ final class Test_Client: XCTestCase { } func testGetStats_200_default_json() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [], - encodedBody: #""" - { - "count" : 1 - } - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok + ) + .withEncodedBody( + #""" + { + "count" : 1 + } + """# ) } let response = try await client.getStats(.init()) @@ -442,25 +469,27 @@ final class Test_Client: XCTestCase { } func testGetStats_200_text() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + .accept: "application/json, text/plain, application/octet-stream" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "text/plain") - ], - encodedBody: #""" - count is 1 - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "text/plain" + ] + ) + .withEncodedBody( + #""" + count is 1 + """# ) } let response = try await client.getStats(.init()) @@ -470,32 +499,34 @@ final class Test_Client: XCTestCase { } switch value.body { case .plainText(let stats): - XCTAssertEqual(stats, "count is 1") + try await XCTAssertEqualStringifiedData(stats, "count is 1") default: XCTFail("Unexpected content type") } } func testGetStats_200_text_requestedSpecific() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "text/plain, application/json; q=0.500") + .accept: "text/plain, application/json; q=0.500" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "text/plain") - ], - encodedBody: #""" - count is 1 - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "text/plain" + ] + ) + .withEncodedBody( + #""" + count is 1 + """# ) } let response = try await client.getStats( @@ -512,32 +543,34 @@ final class Test_Client: XCTestCase { } switch value.body { case .plainText(let stats): - XCTAssertEqual(stats, "count is 1") + try await XCTAssertEqualStringifiedData(stats, "count is 1") default: XCTFail("Unexpected content type") } } func testGetStats_200_text_customAccept() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/json; q=0.800, text/plain") + .accept: "application/json; q=0.800, text/plain" ] ) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "text/plain") - ], - encodedBody: #""" - count is 1 - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "text/plain" + ] + ) + .withEncodedBody( + #""" + count is 1 + """# ) } let response = try await client.getStats( @@ -554,26 +587,28 @@ final class Test_Client: XCTestCase { } switch value.body { case .plainText(let stats): - XCTAssertEqual(stats, "count is 1") + try await XCTAssertEqualStringifiedData(stats, "count is 1") default: XCTFail("Unexpected content type") } } func testGetStats_200_binary() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "application/octet-stream") - ], - encodedBody: #""" - count_is_1 - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "application/octet-stream" + ] + ) + .withEncodedBody( + #""" + count_is_1 + """# ) } let response = try await client.getStats(.init()) @@ -583,26 +618,28 @@ final class Test_Client: XCTestCase { } switch value.body { case .binary(let stats): - XCTAssertEqual(String(decoding: stats, as: UTF8.self), "count_is_1") + try await XCTAssertEqualStringifiedData(stats, "count_is_1") default: XCTFail("Unexpected content type") } } func testGetStats_200_unexpectedContentType() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) - XCTAssertNil(request.body) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "foo/bar") - ], - encodedBody: #""" - count_is_1 - """# + XCTAssertNil(requestBody) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "foo/bar" + ] + ) + .withEncodedBody( + #""" + count_is_1 + """# ) } do { @@ -612,27 +649,26 @@ final class Test_Client: XCTestCase { } func testPostStats_202_json() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "postStats") XCTAssertEqual(request.path, "/pets/stats") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) XCTAssertEqual( request.headerFields, [ - .init(name: "content-type", value: "application/json; charset=utf-8") + .contentType: "application/json; charset=utf-8" ] ) - XCTAssertEqual( - request.body?.pretty, + try await XCTAssertEqualStringifiedData( + requestBody, #""" { "count" : 1 } """# ) - return .init(statusCode: 202) + return (.init(status: .accepted), nil) } let response = try await client.postStats( .init(body: .json(.init(count: 1))) @@ -644,25 +680,24 @@ final class Test_Client: XCTestCase { } func testPostStats_202_text() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "postStats") XCTAssertEqual(request.path, "/pets/stats") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) XCTAssertEqual( request.headerFields, [ - .init(name: "content-type", value: "text/plain") + .contentType: "text/plain" ] ) - XCTAssertEqual( - request.body?.pretty, + try await XCTAssertEqualStringifiedData( + requestBody, #""" count is 1 """# ) - return .init(statusCode: 202) + return (.init(status: .accepted), nil) } let response = try await client.postStats( .init(body: .plainText("count is 1")) @@ -674,28 +709,27 @@ final class Test_Client: XCTestCase { } func testPostStats_202_binary() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "postStats") XCTAssertEqual(request.path, "/pets/stats") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) XCTAssertEqual( request.headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) - XCTAssertEqual( - request.body?.pretty, + try await XCTAssertEqualStringifiedData( + requestBody, #""" count_is_1 """# ) - return .init(statusCode: 202) + return (.init(status: .accepted), nil) } let response = try await client.postStats( - .init(body: .binary(Data("count_is_1".utf8))) + .init(body: .binary("count_is_1")) ) guard case .accepted = response else { XCTFail("Unexpected response: \(response)") @@ -705,15 +739,14 @@ final class Test_Client: XCTestCase { @available(*, deprecated) func testProbe_204() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "probe") XCTAssertEqual(request.path, "/probe/") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .post) - XCTAssertEqual(request.headerFields, []) - XCTAssertNil(request.body) - return .init(statusCode: 204) + XCTAssertEqual(request.headerFields, [:]) + XCTAssertNil(requestBody) + return (.init(status: .noContent), nil) } let response = try await client.probe(.init()) guard case .noContent = response else { @@ -724,8 +757,8 @@ final class Test_Client: XCTestCase { @available(*, deprecated) func testProbe_undocumented() async throws { - transport = .init { request, baseURL, operationID in - .init(statusCode: 503) + transport = .init { request, requestBody, baseURL, operationID in + (.init(status: .serviceUnavailable), nil) } let response = try await client.probe(.init()) guard case let .undocumented(statusCode, _) = response else { @@ -736,32 +769,33 @@ final class Test_Client: XCTestCase { } func testUploadAvatarForPet_200() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "uploadAvatarForPet") XCTAssertEqual(request.path, "/pets/1/avatar") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .put) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), - .init(name: "content-type", value: "application/octet-stream"), + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", + ] + ) + try await XCTAssertEqualStringifiedData(requestBody, Data.abcdString) + return try HTTPResponse( + status: .ok, + headerFields: [ + .contentType: "application/octet-stream" ] ) - XCTAssertEqual(request.body?.pretty, Data.abcdString) - return .init( - statusCode: 200, - headers: [ - .init(name: "content-type", value: "application/octet-stream") - ], - encodedBody: Data.efghString + .withEncodedBody( + Data.efghString ) } let response = try await client.uploadAvatarForPet( .init( path: .init(petId: 1), - body: .binary(.abcd) + body: .binary(.init(.abcd)) ) ) guard case let .ok(value) = response else { @@ -770,37 +804,38 @@ final class Test_Client: XCTestCase { } switch value.body { case .binary(let binary): - XCTAssertEqualStringifiedData(binary, Data.efghString) + try await XCTAssertEqualStringifiedData(binary, Data.efghString) } } func testUploadAvatarForPet_412() async throws { - transport = .init { request, baseURL, operationID in + transport = .init { request, requestBody, baseURL, operationID in XCTAssertEqual(operationID, "uploadAvatarForPet") XCTAssertEqual(request.path, "/pets/1/avatar") - XCTAssertNil(request.query) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .put) XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), - .init(name: "content-type", value: "application/octet-stream"), + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", ] ) - XCTAssertEqual(request.body?.pretty, Data.abcdString) - return .init( - statusCode: 412, - headers: [ - .init(name: "content-type", value: "application/json") - ], - encodedBody: Data.quotedEfghString + try await XCTAssertEqualStringifiedData(requestBody, Data.abcdString) + return try HTTPResponse( + status: .preconditionFailed, + headerFields: [ + .contentType: "application/json" + ] + ) + .withEncodedBody( + Data.quotedEfghString ) } let response = try await client.uploadAvatarForPet( .init( path: .init(petId: 1), - body: .binary(.abcd) + body: .binary(.init(.abcd)) ) ) guard case let .preconditionFailed(value) = response else { @@ -814,19 +849,21 @@ final class Test_Client: XCTestCase { } func testUploadAvatarForPet_500() async throws { - transport = .init { request, baseURL, operationID in - return .init( - statusCode: 500, - headers: [ - .init(name: "content-type", value: "text/plain") - ], - encodedBody: Data.efghString + transport = .init { request, requestBody, baseURL, operationID in + return try HTTPResponse( + status: .internalServerError, + headerFields: [ + .contentType: "text/plain" + ] + ) + .withEncodedBody( + Data.efghString ) } let response = try await client.uploadAvatarForPet( .init( path: .init(petId: 1), - body: .binary(.abcd) + body: .binary(.init(.abcd)) ) ) guard case let .internalServerError(value) = response else { @@ -835,7 +872,7 @@ final class Test_Client: XCTestCase { } switch value.body { case .plainText(let text): - XCTAssertEqual(text, Data.efghString) + try await XCTAssertEqualStringifiedData(text, Data.efghString) } } } diff --git a/Tests/PetstoreConsumerTests/Test_Playground.swift b/Tests/PetstoreConsumerTests/Test_Playground.swift new file mode 100644 index 00000000..20e91d26 --- /dev/null +++ b/Tests/PetstoreConsumerTests/Test_Playground.swift @@ -0,0 +1,234 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import OpenAPIRuntime +import PetstoreConsumerTestCore + +final class Test_Playground: XCTestCase { + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + } + + func testBidiStreaming() async throws { + + // Server + let serverHandler: + @Sendable (Operations.uploadAvatarForPet.Input) async throws -> Operations.uploadAvatarForPet.Output = { + input in + // The server handler verifies the pet id, sends back + // the start of the 200 response, and then streams back + // the body it's receiviving from the request, with every + // byte being decremented by 1. + + guard input.path.petId == 1 else { + return .preconditionFailed(.init(body: .json("bad id"))) + } + + let requestSequence: HTTPBody + switch input.body { + case .binary(let body): + requestSequence = body + } + + let responseSequence = requestSequence.map { chunk in + print("Server received a chunk: \(String(decoding: chunk, as: UTF8.self))") + return chunk.map { $0 - 1 }[...] + } + + return .ok( + .init( + body: .binary( + .init( + responseSequence, + length: requestSequence.length, + iterationBehavior: requestSequence.iterationBehavior + ) + ) + ) + ) + } + + // Client + + // Create the client. + let client: some APIProtocol = TestClient(uploadAvatarForPetBlock: serverHandler) + + // Create the request stream. + var requestContinuation: AsyncStream.Continuation! + let requestStream = AsyncStream(String.self) { _continuation in + requestContinuation = _continuation + } + + // Create a request body wrapping the request stream. + let requestBody = HTTPBody(requestStream, length: .unknown) + + // Send the request, wait for the response. + // At this point, both the request and response streams are still open. + let response = try await client.uploadAvatarForPet( + .init( + path: .init(petId: 1), + body: .binary(requestBody) + ) + ) + + // Verify the response status and content type, extract the response stream. + guard case .ok(let ok) = response, case .binary(let body) = ok.body else { + XCTFail("Unexpected response") + return + } + + let loggedBody = body.map { chunk in + print("Client received a chunk: \(String(decoding: chunk, as: UTF8.self))") + return chunk + } + var responseIterator = loggedBody.makeAsyncIterator() + + // Send a chunk into the request stream, get one from the response stream + // verify the contents. + requestContinuation.yield("hello") + let firstResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(firstResponseChunk, "gdkkn") + + // Send a second chunk. + requestContinuation.yield("world") + let secondResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(secondResponseChunk, "vnqkc") + + // End the request stream. + requestContinuation.finish() + let lastResponseChunk = try await responseIterator.next() + XCTAssertNil(lastResponseChunk) + } + + func testServerStreaming() async throws { + + // Server + + let serverHandler: @Sendable (Operations.getStats.Input) async throws -> Operations.getStats.Output = { input in + + // The server handler sends back the start of the 200 response, + // and then sends a few chunks. + + let responseStream = AsyncStream( + String.self, + bufferingPolicy: .unbounded + ) { continuation in + continuation.yield("hello") + continuation.yield("world") + continuation.finish() + } + let responseBody = HTTPBody(responseStream, length: .unknown) + return .ok(.init(body: .binary(responseBody))) + } + + // Client + + // Create the client. + let client: some APIProtocol = TestClient(getStatsBlock: serverHandler) + + // Send the request, wait for the response. + // At this point, both the request and response streams are still open. + let response = try await client.getStats(.init()) + + // Verify the response status and content type, extract the response stream. + guard case .ok(let ok) = response, case .binary(let body) = ok.body else { + XCTFail("Unexpected response") + return + } + + let loggedBody = body.map { chunk in + print("Client received a chunk: \(String(decoding: chunk, as: UTF8.self))") + return chunk + } + var responseIterator = loggedBody.makeAsyncIterator() + + // Get a chunk from the response stream, verify the contents. + let firstResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(firstResponseChunk, "hello") + + // Get a second chunk. + let secondResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(secondResponseChunk, "world") + + // Verify the end of the response stream. + let lastResponseChunk = try await responseIterator.next() + XCTAssertNil(lastResponseChunk) + } + + func testServerStreaming2() async throws { + + // Server + + let serverHandler: @Sendable (Operations.getStats.Input) async throws -> Operations.getStats.Output = { input in + + // The server handler sends back the start of the 200 response, + // and then sends a few chunks. + + actor ChunkProducer { + private var chunks: [String] = ["hello", "world"] + + func produceNext() -> String? { + guard !chunks.isEmpty else { + return nil + } + return chunks.removeFirst() + } + } + let chunkProducer = ChunkProducer() + + let responseStream = AsyncStream( + unfolding: { + await chunkProducer.produceNext() + } + ) + + let responseBody = HTTPBody(responseStream, length: .unknown) + return .ok(.init(body: .binary(responseBody))) + } + + // Client + + // Create the client. + let client: some APIProtocol = TestClient(getStatsBlock: serverHandler) + + // Send the request, wait for the response. + // At this point, both the request and response streams are still open. + let response = try await client.getStats(.init()) + + // Verify the response status and content type, extract the response stream. + guard case .ok(let ok) = response, case .binary(let body) = ok.body else { + XCTFail("Unexpected response") + return + } + + let loggedBody = body.map { chunk in + print("Client received a chunk: \(String(decoding: chunk, as: UTF8.self))") + return chunk + } + var responseIterator = loggedBody.makeAsyncIterator() + + // Get a chunk from the response stream, verify the contents. + let firstResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(firstResponseChunk, "hello") + + // Get a second chunk. + let secondResponseChunk = try await responseIterator.next() + XCTAssertEqualStringifiedData(secondResponseChunk, "world") + + // Verify the end of the response stream. + let lastResponseChunk = try await responseIterator.next() + XCTAssertNil(lastResponseChunk) + } +} diff --git a/Tests/PetstoreConsumerTests/Test_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 98ac0dcf..f0390744 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -14,6 +14,7 @@ import XCTest import OpenAPIRuntime import PetstoreConsumerTestCore +import HTTPTypes final class Test_Server: XCTestCase { @@ -50,27 +51,32 @@ final class Test_Server: XCTestCase { ) } ) - let response = try await server.listPets( + let (response, responseBody) = try await server.listPets( .init( - path: "/api/pets", - query: "limit=24&habitat=water&feeds=carnivore&feeds=herbivore&since=\(Date.testString)", + soar_path: "/api/pets?limit=24&habitat=water&feeds=carnivore&feeds=herbivore&since=\(Date.testString)", method: .get, headerFields: [ - .init(name: "My-Request-UUID", value: "abcd-1234") + .init("My-Request-UUID")!: "abcd-1234" ] ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "My-Response-UUID", value: "abcd"), - .init(name: "My-Tracing-Header", value: "1234"), - .init(name: "content-type", value: "application/json; charset=utf-8"), + .init("My-Response-UUID")!: "abcd", + .init("My-Tracing-Header")!: "1234", + .contentType: "application/json; charset=utf-8", ] ) - let bodyString = String(decoding: response.body, as: UTF8.self) + let bodyString: String + if let responseBody { + bodyString = try await String(collecting: responseBody, upTo: .max) + } else { + bodyString = "" + } XCTAssertEqual( bodyString, #""" @@ -93,23 +99,23 @@ final class Test_Server: XCTestCase { ) } ) - let response = try await server.listPets( + let (response, responseBody) = try await server.listPets( .init( - path: "/api/pets", + soar_path: "/api/pets", method: .get ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 400) + XCTAssertEqual(response.status.code, 400) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/json; charset=utf-8") + .contentType: "application/json; charset=utf-8" ] ) - let bodyString = String(decoding: response.body, as: UTF8.self) - XCTAssertEqual( - bodyString, + try await XCTAssertEqualStringifiedData( + responseBody, #""" { "code" : 1, @@ -139,33 +145,34 @@ final class Test_Server: XCTestCase { ) } ) - let response = try await server.createPet( + let (response, responseBody) = try await server.createPet( .init( - path: "/api/pets", + soar_path: "/api/pets", method: .post, headerFields: [ - .init(name: "x-extra-arguments", value: #"{"code":1}"#), - .init(name: "content-type", value: "application/json; charset=utf-8"), - ], - encodedBody: #""" - { - "name" : "Fluffz" - } - """# + .init("x-extra-arguments")!: #"{"code":1}"#, + .contentType: "application/json; charset=utf-8", + ] + ), + .init( + #""" + { + "name" : "Fluffz" + } + """# ), .init() ) - XCTAssertEqual(response.statusCode, 201) + XCTAssertEqual(response.status.code, 201) XCTAssertEqual( response.headerFields, [ - .init(name: "X-Extra-Arguments", value: #"{"code":1}"#), - .init(name: "content-type", value: "application/json; charset=utf-8"), + .init("X-Extra-Arguments")!: #"{"code":1}"#, + .contentType: "application/json; charset=utf-8", ] ) - let bodyString = String(decoding: response.body, as: UTF8.self) - XCTAssertEqual( - bodyString, + try await XCTAssertEqualStringifiedData( + responseBody, #""" { "id" : 1, @@ -191,32 +198,33 @@ final class Test_Server: XCTestCase { ) } ) - let response = try await server.createPet( + let (response, responseBody) = try await server.createPet( .init( - path: "/api/pets", + soar_path: "/api/pets", method: .post, headerFields: [ - .init(name: "content-type", value: "application/json; charset=utf-8") - ], - encodedBody: #""" - { - "name" : "Fluffz" - } - """# + .contentType: "application/json; charset=utf-8" + ] + ), + .init( + #""" + { + "name" : "Fluffz" + } + """# ), .init() ) - XCTAssertEqual(response.statusCode, 400) + XCTAssertEqual(response.status.code, 400) XCTAssertEqual( response.headerFields, [ - .init(name: "X-Reason", value: "bad%20luck"), - .init(name: "content-type", value: "application/json; charset=utf-8"), + .init("X-Reason")!: "bad%20luck", + .contentType: "application/json; charset=utf-8", ] ) - let bodyString = String(decoding: response.body, as: UTF8.self) - XCTAssertEqual( - bodyString, + try await XCTAssertEqualStringifiedData( + responseBody, #""" { "code" : 1 @@ -235,17 +243,19 @@ final class Test_Server: XCTestCase { do { _ = try await server.createPet( .init( - path: "/api/pets", + soar_path: "/api/pets", method: .post, headerFields: [ - .init(name: "x-extra-arguments", value: #"{"code":1}"#), - .init(name: "content-type", value: "text/plain; charset=utf-8"), - ], - encodedBody: #""" - { - "name" : "Fluffz" - } - """# + .init("x-extra-arguments")!: #"{"code":1}"#, + .contentType: "text/plain; charset=utf-8", + ] + ), + .init( + #""" + { + "name" : "Fluffz" + } + """# ), .init() ) @@ -263,22 +273,23 @@ final class Test_Server: XCTestCase { return .noContent(.init()) } ) - let response = try await server.createPetWithForm( + let (response, responseBody) = try await server.createPetWithForm( .init( - path: "/api/pets/create", + soar_path: "/api/pets/create", method: .post, headerFields: [ - .init(name: "x-extra-arguments", value: #"{"code":1}"#), - .init(name: "content-type", value: "application/x-www-form-urlencoded"), - ], - encodedBody: "name=Fluffz" + .init("x-extra-arguments")!: #"{"code":1}"#, + .contentType: "application/x-www-form-urlencoded", + ] ), + .init("name=Fluffz"), .init() ) - XCTAssertEqual(response.statusCode, 204) + XCTAssertEqual(response.status.code, 204) + XCTAssertNil(responseBody) XCTAssertEqual( response.headerFields, - [] + [:] ) } @@ -296,19 +307,21 @@ final class Test_Server: XCTestCase { return .noContent(.init()) } ) - let response = try await server.updatePet( + let (response, responseBody) = try await server.updatePet( .init( - path: "/api/pets/1", + soar_path: "/api/pets/1", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json"), - .init(name: "content-type", value: "application/json"), - ], - encodedBody: #""" - { - "name" : "Fluffz" - } - """# + .accept: "application/json", + .contentType: "application/json", + ] + ), + .init( + #""" + { + "name" : "Fluffz" + } + """# ), .init( pathParameters: [ @@ -316,8 +329,9 @@ final class Test_Server: XCTestCase { ] ) ) - XCTAssertEqual(response.statusCode, 204) - XCTAssertEqual(response.headerFields, []) + XCTAssertEqual(response.status.code, 204) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testUpdatePet_204_withBody_default_json() async throws { @@ -334,16 +348,18 @@ final class Test_Server: XCTestCase { return .noContent(.init()) } ) - let response = try await server.updatePet( + let (response, responseBody) = try await server.updatePet( .init( - path: "/api/pets/1", + soar_path: "/api/pets/1", method: .patch, - headerFields: [], - encodedBody: #""" - { - "name" : "Fluffz" - } - """# + headerFields: [:] + ), + .init( + #""" + { + "name" : "Fluffz" + } + """# ), .init( pathParameters: [ @@ -351,8 +367,9 @@ final class Test_Server: XCTestCase { ] ) ) - XCTAssertEqual(response.statusCode, 204) - XCTAssertEqual(response.headerFields, []) + XCTAssertEqual(response.status.code, 204) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testUpdatePet_204_withoutBody() async throws { @@ -363,19 +380,21 @@ final class Test_Server: XCTestCase { return .noContent(.init()) } ) - let response = try await server.updatePet( + let (response, responseBody) = try await server.updatePet( .init( - path: "/api/pets/1", + soar_path: "/api/pets/1", method: .patch ), + nil, .init( pathParameters: [ "petId": "1" ] ) ) - XCTAssertEqual(response.statusCode, 204) - XCTAssertEqual(response.headerFields, []) + XCTAssertEqual(response.status.code, 204) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testUpdatePet_400() async throws { @@ -386,26 +405,27 @@ final class Test_Server: XCTestCase { return .badRequest(.init(body: .json(.init(message: "Oh no!")))) } ) - let response = try await server.updatePet( + let (response, responseBody) = try await server.updatePet( .init( - path: "/api/pets/1", + soar_path: "/api/pets/1", method: .patch ), + nil, .init( pathParameters: [ "petId": "1" ] ) ) - XCTAssertEqual(response.statusCode, 400) + XCTAssertEqual(response.status.code, 400) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/json; charset=utf-8") + .contentType: "application/json; charset=utf-8" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" { "message" : "Oh no!" @@ -420,25 +440,26 @@ final class Test_Server: XCTestCase { return .ok(.init(body: .json(.init(count: 1)))) } ) - let response = try await server.getStats( + let (response, responseBody) = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + .accept: "application/json, text/plain, application/octet-stream" ] ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/json; charset=utf-8") + .contentType: "application/json; charset=utf-8" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" { "count" : 1 @@ -456,12 +477,13 @@ final class Test_Server: XCTestCase { do { _ = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "foo/bar") + .accept: "foo/bar" ] ), + nil, .init() ) XCTFail("Should have thrown an error.") @@ -474,31 +496,75 @@ final class Test_Server: XCTestCase { return .ok(.init(body: .plainText("count is 1"))) } ) - let response = try await server.getStats( + let (response, responseBody) = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + .accept: "application/json, text/plain, application/octet-stream" ] ), + .init(), .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "text/plain") + .contentType: "text/plain" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" count is 1 """# ) } + func testGetStats_200_streaming_text() async throws { + client = .init( + getStatsBlock: { input in + let body = HTTPBody( + AsyncStream { continuation in + continuation.yield([72]) + continuation.yield([69]) + continuation.yield([76]) + continuation.yield([76]) + continuation.yield([79]) + continuation.finish() + }, + length: .unknown + ) + return .ok(.init(body: .plainText(body))) + } + ) + let (response, responseBody) = try await server.getStats( + .init( + soar_path: "/api/pets/stats", + method: .patch, + headerFields: [ + .accept: "application/json, text/plain, application/octet-stream" + ] + ), + .init(), + .init() + ) + XCTAssertEqual(response.status.code, 200) + XCTAssertEqual( + response.headerFields, + [ + .contentType: "text/plain" + ] + ) + try await XCTAssertEqualStringifiedData( + responseBody, + #""" + HELLO + """# + ) + } + func testGetStats_200_text_requestedSpecific() async throws { client = .init( getStatsBlock: { input in @@ -512,25 +578,26 @@ final class Test_Server: XCTestCase { return .ok(.init(body: .plainText("count is 1"))) } ) - let response = try await server.getStats( + let (response, responseBody) = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "text/plain, application/json; q=0.500") + .accept: "text/plain, application/json; q=0.500" ] ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "text/plain") + .contentType: "text/plain" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" count is 1 """# @@ -550,25 +617,26 @@ final class Test_Server: XCTestCase { return .ok(.init(body: .plainText("count is 1"))) } ) - let response = try await server.getStats( + let (response, responseBody) = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json; q=0.8, text/plain") + .accept: "application/json; q=0.8, text/plain" ] ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "text/plain") + .contentType: "text/plain" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" count is 1 """# @@ -578,28 +646,29 @@ final class Test_Server: XCTestCase { func testGetStats_200_binary() async throws { client = .init( getStatsBlock: { input in - return .ok(.init(body: .binary(Data("count_is_1".utf8)))) + return .ok(.init(body: .binary("count_is_1"))) } ) - let response = try await server.getStats( + let (response, responseBody) = try await server.getStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + .accept: "application/json, text/plain, application/octet-stream" ] ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, #""" count_is_1 """# @@ -616,27 +685,26 @@ final class Test_Server: XCTestCase { return .accepted(.init()) } ) - let response = try await server.postStats( + let (response, responseBody) = try await server.postStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .post, headerFields: [ - .init(name: "content-type", value: "application/json; charset=utf-8") - ], - encodedBody: #""" - { - "count" : 1 - } - """# + .contentType: "application/json; charset=utf-8" + ] + ), + .init( + #""" + { + "count" : 1 + } + """# ), .init() ) - XCTAssertEqual(response.statusCode, 202) - XCTAssertEqual( - response.headerFields, - [] - ) - XCTAssert(response.body.isEmpty) + XCTAssertEqual(response.status.code, 202) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testPostStats_202_default_json() async throws { @@ -649,25 +717,24 @@ final class Test_Server: XCTestCase { return .accepted(.init()) } ) - let response = try await server.postStats( + let (response, responseBody) = try await server.postStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .post, - headerFields: [], - encodedBody: #""" - { - "count" : 1 - } - """# + headerFields: [:] + ), + .init( + #""" + { + "count" : 1 + } + """# ), .init() ) - XCTAssertEqual(response.statusCode, 202) - XCTAssertEqual( - response.headerFields, - [] - ) - XCTAssert(response.body.isEmpty) + XCTAssertEqual(response.status.code, 202) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testPostStats_202_text() async throws { @@ -676,29 +743,28 @@ final class Test_Server: XCTestCase { guard case let .plainText(stats) = input.body else { throw TestError.unexpectedValue(input.body) } - XCTAssertEqual(stats, "count is 1") + try await XCTAssertEqualStringifiedData(stats, "count is 1") return .accepted(.init()) } ) - let response = try await server.postStats( + let (response, responseBody) = try await server.postStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .post, headerFields: [ - .init(name: "content-type", value: "text/plain") - ], - encodedBody: #""" - count is 1 - """# + .contentType: "text/plain" + ] + ), + .init( + #""" + count is 1 + """# ), .init() ) - XCTAssertEqual(response.statusCode, 202) - XCTAssertEqual( - response.headerFields, - [] - ) - XCTAssert(response.body.isEmpty) + XCTAssertEqual(response.status.code, 202) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testPostStats_202_binary() async throws { @@ -707,29 +773,28 @@ final class Test_Server: XCTestCase { guard case let .binary(stats) = input.body else { throw TestError.unexpectedValue(input.body) } - XCTAssertEqualStringifiedData(stats, "count_is_1") + try await XCTAssertEqualStringifiedData(stats, "count_is_1") return .accepted(.init()) } ) - let response = try await server.postStats( + let (response, responseBody) = try await server.postStats( .init( - path: "/api/pets/stats", + soar_path: "/api/pets/stats", method: .post, headerFields: [ - .init(name: "content-type", value: "application/octet-stream") - ], - encodedBody: #""" - count_is_1 - """# + .contentType: "application/octet-stream" + ] + ), + .init( + #""" + count_is_1 + """# ), .init() ) - XCTAssertEqual(response.statusCode, 202) - XCTAssertEqual( - response.headerFields, - [] - ) - XCTAssert(response.body.isEmpty) + XCTAssertEqual(response.status.code, 202) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testProbe_204() async throws { @@ -738,16 +803,17 @@ final class Test_Server: XCTestCase { return .noContent(.init()) } ) - let response = try await server.probe( + let (response, responseBody) = try await server.probe( .init( - path: "/api/probe", + soar_path: "/api/probe/", method: .post ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 204) - XCTAssertEqual(response.headerFields, []) - XCTAssertEqual(response.body, .init()) + XCTAssertEqual(response.status.code, 204) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } func testProbe_undocumented() async throws { @@ -756,92 +822,154 @@ final class Test_Server: XCTestCase { .undocumented(statusCode: 503, .init()) } ) - let response = try await server.probe( + let (response, responseBody) = try await server.probe( .init( - path: "/api/probe", + soar_path: "/api/probe/", method: .post ), + nil, .init() ) - XCTAssertEqual(response.statusCode, 503) - XCTAssertEqual(response.headerFields, []) - XCTAssertEqual(response.body, .init()) + XCTAssertEqual(response.status.code, 503) + XCTAssertEqual(response.headerFields, [:]) + XCTAssertNil(responseBody) } - func testUploadAvatarForPet_200() async throws { + func testUploadAvatarForPet_200_buffered() async throws { client = .init( uploadAvatarForPetBlock: { input in guard case let .binary(avatar) = input.body else { throw TestError.unexpectedValue(input.body) } - XCTAssertEqualStringifiedData(avatar, Data.abcdString) - return .ok(.init(body: .binary(.efgh))) + try await XCTAssertEqualStringifiedData(avatar, Data.abcdString) + return .ok(.init(body: .binary(.init(.efgh)))) } ) - let response = try await server.uploadAvatarForPet( + let (response, responseBody) = try await server.uploadAvatarForPet( .init( - path: "/api/pets/1/avatar", + soar_path: "/api/pets/1/avatar", method: .put, headerFields: [ - .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), - .init(name: "content-type", value: "application/octet-stream"), - ], - encodedBody: Data.abcdString + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", + ] ), + .init(Data.abcdString), .init( pathParameters: [ "petId": "1" ] ) ) - XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.status.code, 200) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, Data.efghString ) } + func testUploadAvatarForPet_200_streaming() async throws { + actor CollectedChunkSizes { + private(set) var sizes: [Int] = [] + func record(size: Int) { + sizes.append(size) + } + } + let chunkSizeCollector = CollectedChunkSizes() + client = .init( + uploadAvatarForPetBlock: { input in + guard case let .binary(avatar) = input.body else { + throw TestError.unexpectedValue(input.body) + } + let responseSequence = avatar.map { chunk in + await chunkSizeCollector.record(size: chunk.count) + return chunk + } + return .ok( + .init( + body: .binary( + .init( + responseSequence, + length: avatar.length, + iterationBehavior: avatar.iterationBehavior + ) + ) + ) + ) + } + ) + let (response, responseBody) = try await server.uploadAvatarForPet( + .init( + soar_path: "/api/pets/1/avatar", + method: .put, + headerFields: [ + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", + ] + ), + .init([97, 98, 99, 100], length: .known(4)), + .init( + pathParameters: [ + "petId": "1" + ] + ) + ) + XCTAssertEqual(response.status.code, 200) + XCTAssertEqual( + response.headerFields, + [ + .contentType: "application/octet-stream" + ] + ) + try await XCTAssertEqualStringifiedData( + responseBody, + Data.abcdString + ) + let sizes = await chunkSizeCollector.sizes + XCTAssertEqual(sizes, [4]) + } + func testUploadAvatarForPet_412() async throws { client = .init( uploadAvatarForPetBlock: { input in guard case let .binary(avatar) = input.body else { throw TestError.unexpectedValue(input.body) } - XCTAssertEqualStringifiedData(avatar, Data.abcdString) + try await XCTAssertEqualStringifiedData(avatar, Data.abcdString) return .preconditionFailed(.init(body: .json(Data.efghString))) } ) - let response = try await server.uploadAvatarForPet( + let (response, responseBody) = try await server.uploadAvatarForPet( .init( - path: "/api/pets/1/avatar", + soar_path: "/api/pets/1/avatar", method: .put, headerFields: [ - .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), - .init(name: "content-type", value: "application/octet-stream"), - ], - encodedBody: Data.abcdString + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", + ] ), + .init(Data.abcdString), .init( pathParameters: [ "petId": "1" ] ) ) - XCTAssertEqual(response.statusCode, 412) + XCTAssertEqual(response.status.code, 412) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "application/json; charset=utf-8") + .contentType: "application/json; charset=utf-8" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, Data.quotedEfghString ) } @@ -852,37 +980,40 @@ final class Test_Server: XCTestCase { guard case let .binary(avatar) = input.body else { throw TestError.unexpectedValue(input.body) } - XCTAssertEqualStringifiedData(avatar, Data.abcdString) - return .internalServerError(.init(body: .plainText(Data.efghString))) + try await XCTAssertEqualStringifiedData(avatar, Data.abcdString) + return .internalServerError( + .init( + body: .plainText(.init(Data.efghString)) + ) + ) } ) - let response = try await server.uploadAvatarForPet( + let (response, responseBody) = try await server.uploadAvatarForPet( .init( - path: "/api/pets/1/avatar", + soar_path: "/api/pets/1/avatar", method: .put, headerFields: [ - .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), - .init(name: "content-type", value: "application/octet-stream"), - ], - encodedBody: Data.abcdString + .accept: "application/octet-stream, application/json, text/plain", + .contentType: "application/octet-stream", + ] ), + .init(Data.abcdString), .init( pathParameters: [ "petId": "1" ] ) ) - XCTAssertEqual(response.statusCode, 500) + XCTAssertEqual(response.status.code, 500) XCTAssertEqual( response.headerFields, [ - .init(name: "content-type", value: "text/plain") + .contentType: "text/plain" ] ) - XCTAssertEqualStringifiedData( - response.body, + try await XCTAssertEqualStringifiedData( + responseBody, Data.efghString ) } - }