Skip to content

Commit

Permalink
[URLSession Transport] Async bodies + swift-http-types adoption (#15)
Browse files Browse the repository at this point in the history
[URLSession Transport] Async bodies + swift-http-types adoption

### Motivation

URLSession transport changes of the approved proposals apple/swift-openapi-generator#255 and apple/swift-openapi-generator#254.

### Modifications

- Adapts to the runtime changes, depends on HTTPTypes now.
- Doesn't do streaming yet, we'll addressed that separately, continues to buffer for now (apple/swift-openapi-generator#301)

### Result

Transport 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 (nightly) - Build finished. 
     βœ”οΈŽ pull request validation (soundness) - Build finished. 

#15
  • Loading branch information
czechboy0 authored Oct 2, 2023
1 parent 8d34af5 commit 2a676df
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 55 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-runtime", "0.1.3" ..< "0.3.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Add the package dependency in your `Package.swift`:
```swift
.package(
url: "https://github.com/apple/swift-openapi-urlsession",
.upToNextMinor(from: "0.2.0")
.upToNextMinor(from: "0.3.0")
),
```

Expand Down
4 changes: 2 additions & 2 deletions Sources/OpenAPIURLSession/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Use the transport with client code generated by [Swift OpenAPI Generator](https:
### Supported platforms and minimum versions
| macOS | Linux | iOS | tvOS | watchOS |
| :-: | :-: | :-: | :-: | :-: |
| βœ… 10.15+ | βœ… | βœ… 13+ | βœ… 13+ | βœ… 6+ |
| βœ… 10.15+ | βœ… | βœ… 13+ | βœ… 13+ | βœ… 6+ |

### Usage

Expand All @@ -20,7 +20,7 @@ Add the package dependency in your `Package.swift`:
```swift
.package(
url: "https://github.com/apple/swift-openapi-urlsession",
.upToNextMinor(from: "0.2.0")
.upToNextMinor(from: "0.3.0")
),
```

Expand Down
95 changes: 68 additions & 27 deletions Sources/OpenAPIURLSession/URLSessionTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
import OpenAPIRuntime
import HTTPTypes
#if canImport(Darwin)
import Foundation
#else
Expand Down Expand Up @@ -90,13 +91,19 @@ public struct URLSessionTransport: ClientTransport {
}

public func send(
_ request: OpenAPIRuntime.Request,
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String
) async throws -> OpenAPIRuntime.Response {
let urlRequest = try URLRequest(request, baseURL: baseURL)
) async throws -> (HTTPResponse, HTTPBody?) {
// TODO: https://github.com/apple/swift-openapi-generator/issues/301
let urlRequest = try await URLRequest(request, body: body, baseURL: baseURL)
let (responseBody, urlResponse) = try await invokeSession(urlRequest)
return try OpenAPIRuntime.Response(from: urlResponse, body: responseBody)
return try HTTPResponse.response(
method: request.method,
urlResponse: urlResponse,
data: responseBody
)
}

private func invokeSession(_ urlRequest: URLRequest) async throws -> (Data, URLResponse) {
Expand Down Expand Up @@ -129,7 +136,7 @@ public struct URLSessionTransport: ClientTransport {
internal enum URLSessionTransportError: Error {

/// Invalid URL composed from base URL and received request.
case invalidRequestURL(request: OpenAPIRuntime.Request, baseURL: URL)
case invalidRequestURL(path: String, method: HTTPRequest.Method, baseURL: URL)

/// Returned `URLResponse` could not be converted to `HTTPURLResponse`.
case notHTTPResponse(URLResponse)
Expand All @@ -138,40 +145,74 @@ internal enum URLSessionTransportError: Error {
case noResponse(url: URL?)
}

extension OpenAPIRuntime.Response {
init(from urlResponse: URLResponse, body: Data) throws {
extension HTTPResponse {
static func response(
method: HTTPRequest.Method,
urlResponse: URLResponse,
data: Data
) throws -> (HTTPResponse, HTTPBody?) {
guard let httpResponse = urlResponse as? HTTPURLResponse else {
throw URLSessionTransportError.notHTTPResponse(urlResponse)
}
let headerFields: [HeaderField] = httpResponse
.allHeaderFields
.compactMap { headerName, headerValue in
guard let name = headerName as? String, let value = headerValue as? String else {
return nil
}
return HeaderField(name: name, value: value)
var headerFields = HTTPFields()
for (headerName, headerValue) in httpResponse.allHeaderFields {
guard
let rawName = headerName as? String,
let name = HTTPField.Name(rawName),
let value = headerValue as? String
else {
continue
}
self.init(statusCode: httpResponse.statusCode, headerFields: headerFields, body: body)
headerFields[name] = value
}
let body: HTTPBody?
switch method {
case .head, .connect, .trace:
body = nil
default:
body = .init(data)
}
return (
HTTPResponse(
status: .init(code: httpResponse.statusCode),
headerFields: headerFields
),
body
)
}
}

extension URLRequest {
init(_ request: OpenAPIRuntime.Request, baseURL: URL) throws {
guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString) else {
throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL)
init(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL) async throws {
guard
var baseUrlComponents = URLComponents(string: baseURL.absoluteString),
let requestUrlComponents = URLComponents(string: request.path ?? "")
else {
throw URLSessionTransportError.invalidRequestURL(
path: request.path ?? "<nil>",
method: request.method,
baseURL: baseURL
)
}
baseUrlComponents.percentEncodedPath += request.path
baseUrlComponents.percentEncodedQuery = request.query

let path = requestUrlComponents.percentEncodedPath
baseUrlComponents.percentEncodedPath += path
baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery
guard let url = baseUrlComponents.url else {
throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL)
throw URLSessionTransportError.invalidRequestURL(
path: path,
method: request.method,
baseURL: baseURL
)
}
self.init(url: url)
self.httpMethod = request.method.name
self.httpMethod = request.method.rawValue
for header in request.headerFields {
self.addValue(header.value, forHTTPHeaderField: header.name)
self.setValue(header.value, forHTTPHeaderField: header.name.canonicalName)
}
if let body = request.body {
self.httpBody = body
if let body {
// TODO: https://github.com/apple/swift-openapi-generator/issues/301
self.httpBody = try await Data(collecting: body, upTo: .max)
}
}
}
Expand All @@ -183,9 +224,9 @@ extension URLSessionTransportError: LocalizedError {
extension URLSessionTransportError: CustomStringConvertible {
public var description: String {
switch self {
case let .invalidRequestURL(request: request, baseURL: baseURL):
case let .invalidRequestURL(path: path, method: method, baseURL: baseURL):
return
"Invalid request URL from request path: \(request.path), query: \(request.query ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
"Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)"
case .notHTTPResponse(let response):
return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))"
case .noResponse(let url):
Expand Down
66 changes: 42 additions & 24 deletions Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,54 @@ import Foundation
@preconcurrency import class FoundationNetworking.URLSessionConfiguration
#endif
@testable import OpenAPIURLSession
import HTTPTypes

class URLSessionTransportTests: XCTestCase {

func testRequestConversion() throws {
let request = OpenAPIRuntime.Request(
path: "/hello%20world/Maria",
query: "greeting=Howdy",
func testRequestConversion() async throws {
let request = HTTPRequest(
method: .post,
scheme: nil,
authority: nil,
path: "/hello%20world/Maria?greeting=Howdy",
headerFields: [
.init(name: "X-Mumble", value: "mumble")
],
body: Data("πŸ‘‹".utf8)
.init("x-mumble2")!: "mumble"
]
)
let body: HTTPBody = "πŸ‘‹"
let urlRequest = try await URLRequest(
request,
body: body,
baseURL: URL(string: "http://example.com/api")!
)
let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!)
XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy"))
XCTAssertEqual(urlRequest.httpMethod, "POST")
XCTAssertEqual(urlRequest.allHTTPHeaderFields, ["X-Mumble": "mumble"])
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 1)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble")
XCTAssertEqual(urlRequest.httpBody, Data("πŸ‘‹".utf8))
}

func testResponseConversion() throws {
func testResponseConversion() async throws {
let urlResponse: URLResponse = HTTPURLResponse(
url: URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")!,
url: URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!,
statusCode: 201,
httpVersion: "HTTP/1.1",
headerFields: ["X-Mumble": "mumble"]
headerFields: ["x-mumble3": "mumble"]
)!
let response = try OpenAPIRuntime.Response(from: urlResponse, body: Data("πŸ‘‹".utf8))
XCTAssertEqual(response.statusCode, 201)
XCTAssertEqual(response.headerFields, [.init(name: "X-Mumble", value: "mumble")])
XCTAssertEqual(response.body, Data("πŸ‘‹".utf8))
let (response, maybeResponseBody) = try HTTPResponse.response(
method: .get,
urlResponse: urlResponse,
data: Data("πŸ‘‹".utf8)
)
let responseBody = try XCTUnwrap(maybeResponseBody)
XCTAssertEqual(response.status.code, 201)
XCTAssertEqual(response.headerFields, [.init("x-mumble3")!: "mumble"])
let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max)
XCTAssertEqual(bufferedResponseBody, "πŸ‘‹")
}

func testSend() async throws {
let endpointURL = URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")!
let endpointURL = URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!
MockURLProtocol.mockHTTPResponses.withValue { map in
map[endpointURL] = .success(
(
Expand All @@ -73,21 +86,26 @@ class URLSessionTransportTests: XCTestCase {
let transport: any ClientTransport = URLSessionTransport(
configuration: .init(session: MockURLProtocol.mockURLSession)
)
let request = OpenAPIRuntime.Request(
path: "/hello/Maria",
query: "greeting=Howdy",
let request = HTTPRequest(
method: .post,
scheme: nil,
authority: nil,
path: "/hello%20world/Maria?greeting=Howdy",
headerFields: [
.init(name: "X-Mumble", value: "mumble")
.init("x-mumble1")!: "mumble"
]
)
let response = try await transport.send(
let requestBody: HTTPBody = "πŸ‘‹"
let (response, maybeResponseBody) = try await transport.send(
request,
body: requestBody,
baseURL: URL(string: "http://example.com/api")!,
operationID: "postGreeting"
)
XCTAssertEqual(response.statusCode, 201)
XCTAssertEqual(response.body, Data("πŸ‘‹".utf8))
let responseBody = try XCTUnwrap(maybeResponseBody)
XCTAssertEqual(response.status.code, 201)
let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max)
XCTAssertEqual(bufferedResponseBody, "πŸ‘‹")
}
}

Expand Down

0 comments on commit 2a676df

Please sign in to comment.