Skip to content

Commit 2a676df

Browse files
authored
[URLSession Transport] Async bodies + swift-http-types adoption (#15)
[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
1 parent 8d34af5 commit 2a676df

File tree

5 files changed

+114
-55
lines changed

5 files changed

+114
-55
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ let package = Package(
3838
),
3939
],
4040
dependencies: [
41-
.package(url: "https://github.com/apple/swift-openapi-runtime", "0.1.3" ..< "0.3.0"),
41+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")),
4242
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
4343
],
4444
targets: [

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Add the package dependency in your `Package.swift`:
1616
```swift
1717
.package(
1818
url: "https://github.com/apple/swift-openapi-urlsession",
19-
.upToNextMinor(from: "0.2.0")
19+
.upToNextMinor(from: "0.3.0")
2020
),
2121
```
2222

Sources/OpenAPIURLSession/Documentation.docc/Documentation.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Use the transport with client code generated by [Swift OpenAPI Generator](https:
1111
### Supported platforms and minimum versions
1212
| macOS | Linux | iOS | tvOS | watchOS |
1313
| :-: | :-: | :-: | :-: | :-: |
14-
| ✅ 10.15+ | | ✅ 13+ | ✅ 13+ | ✅ 6+ |
14+
| ✅ 10.15+ || ✅ 13+ | ✅ 13+ | ✅ 6+ |
1515

1616
### Usage
1717

@@ -20,7 +20,7 @@ Add the package dependency in your `Package.swift`:
2020
```swift
2121
.package(
2222
url: "https://github.com/apple/swift-openapi-urlsession",
23-
.upToNextMinor(from: "0.2.0")
23+
.upToNextMinor(from: "0.3.0")
2424
),
2525
```
2626

Sources/OpenAPIURLSession/URLSessionTransport.swift

+68-27
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIRuntime
15+
import HTTPTypes
1516
#if canImport(Darwin)
1617
import Foundation
1718
#else
@@ -90,13 +91,19 @@ public struct URLSessionTransport: ClientTransport {
9091
}
9192

9293
public func send(
93-
_ request: OpenAPIRuntime.Request,
94+
_ request: HTTPRequest,
95+
body: HTTPBody?,
9496
baseURL: URL,
9597
operationID: String
96-
) async throws -> OpenAPIRuntime.Response {
97-
let urlRequest = try URLRequest(request, baseURL: baseURL)
98+
) async throws -> (HTTPResponse, HTTPBody?) {
99+
// TODO: https://github.com/apple/swift-openapi-generator/issues/301
100+
let urlRequest = try await URLRequest(request, body: body, baseURL: baseURL)
98101
let (responseBody, urlResponse) = try await invokeSession(urlRequest)
99-
return try OpenAPIRuntime.Response(from: urlResponse, body: responseBody)
102+
return try HTTPResponse.response(
103+
method: request.method,
104+
urlResponse: urlResponse,
105+
data: responseBody
106+
)
100107
}
101108

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

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

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

141-
extension OpenAPIRuntime.Response {
142-
init(from urlResponse: URLResponse, body: Data) throws {
148+
extension HTTPResponse {
149+
static func response(
150+
method: HTTPRequest.Method,
151+
urlResponse: URLResponse,
152+
data: Data
153+
) throws -> (HTTPResponse, HTTPBody?) {
143154
guard let httpResponse = urlResponse as? HTTPURLResponse else {
144155
throw URLSessionTransportError.notHTTPResponse(urlResponse)
145156
}
146-
let headerFields: [HeaderField] = httpResponse
147-
.allHeaderFields
148-
.compactMap { headerName, headerValue in
149-
guard let name = headerName as? String, let value = headerValue as? String else {
150-
return nil
151-
}
152-
return HeaderField(name: name, value: value)
157+
var headerFields = HTTPFields()
158+
for (headerName, headerValue) in httpResponse.allHeaderFields {
159+
guard
160+
let rawName = headerName as? String,
161+
let name = HTTPField.Name(rawName),
162+
let value = headerValue as? String
163+
else {
164+
continue
153165
}
154-
self.init(statusCode: httpResponse.statusCode, headerFields: headerFields, body: body)
166+
headerFields[name] = value
167+
}
168+
let body: HTTPBody?
169+
switch method {
170+
case .head, .connect, .trace:
171+
body = nil
172+
default:
173+
body = .init(data)
174+
}
175+
return (
176+
HTTPResponse(
177+
status: .init(code: httpResponse.statusCode),
178+
headerFields: headerFields
179+
),
180+
body
181+
)
155182
}
156183
}
157184

158185
extension URLRequest {
159-
init(_ request: OpenAPIRuntime.Request, baseURL: URL) throws {
160-
guard var baseUrlComponents = URLComponents(string: baseURL.absoluteString) else {
161-
throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL)
186+
init(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL) async throws {
187+
guard
188+
var baseUrlComponents = URLComponents(string: baseURL.absoluteString),
189+
let requestUrlComponents = URLComponents(string: request.path ?? "")
190+
else {
191+
throw URLSessionTransportError.invalidRequestURL(
192+
path: request.path ?? "<nil>",
193+
method: request.method,
194+
baseURL: baseURL
195+
)
162196
}
163-
baseUrlComponents.percentEncodedPath += request.path
164-
baseUrlComponents.percentEncodedQuery = request.query
197+
198+
let path = requestUrlComponents.percentEncodedPath
199+
baseUrlComponents.percentEncodedPath += path
200+
baseUrlComponents.percentEncodedQuery = requestUrlComponents.percentEncodedQuery
165201
guard let url = baseUrlComponents.url else {
166-
throw URLSessionTransportError.invalidRequestURL(request: request, baseURL: baseURL)
202+
throw URLSessionTransportError.invalidRequestURL(
203+
path: path,
204+
method: request.method,
205+
baseURL: baseURL
206+
)
167207
}
168208
self.init(url: url)
169-
self.httpMethod = request.method.name
209+
self.httpMethod = request.method.rawValue
170210
for header in request.headerFields {
171-
self.addValue(header.value, forHTTPHeaderField: header.name)
211+
self.setValue(header.value, forHTTPHeaderField: header.name.canonicalName)
172212
}
173-
if let body = request.body {
174-
self.httpBody = body
213+
if let body {
214+
// TODO: https://github.com/apple/swift-openapi-generator/issues/301
215+
self.httpBody = try await Data(collecting: body, upTo: .max)
175216
}
176217
}
177218
}
@@ -183,9 +224,9 @@ extension URLSessionTransportError: LocalizedError {
183224
extension URLSessionTransportError: CustomStringConvertible {
184225
public var description: String {
185226
switch self {
186-
case let .invalidRequestURL(request: request, baseURL: baseURL):
227+
case let .invalidRequestURL(path: path, method: method, baseURL: baseURL):
187228
return
188-
"Invalid request URL from request path: \(request.path), query: \(request.query ?? "<nil>") relative to base URL: \(baseURL.absoluteString)"
229+
"Invalid request URL from request path: \(path), method: \(method), relative to base URL: \(baseURL.absoluteString)"
189230
case .notHTTPResponse(let response):
190231
return "Received a non-HTTP response, of type: \(String(describing: type(of: response)))"
191232
case .noResponse(let url):

Tests/OpenAPIURLSessionTests/URLSessionTransportTests.swift

+42-24
Original file line numberDiff line numberDiff line change
@@ -27,41 +27,54 @@ import Foundation
2727
@preconcurrency import class FoundationNetworking.URLSessionConfiguration
2828
#endif
2929
@testable import OpenAPIURLSession
30+
import HTTPTypes
3031

3132
class URLSessionTransportTests: XCTestCase {
3233

33-
func testRequestConversion() throws {
34-
let request = OpenAPIRuntime.Request(
35-
path: "/hello%20world/Maria",
36-
query: "greeting=Howdy",
34+
func testRequestConversion() async throws {
35+
let request = HTTPRequest(
3736
method: .post,
37+
scheme: nil,
38+
authority: nil,
39+
path: "/hello%20world/Maria?greeting=Howdy",
3840
headerFields: [
39-
.init(name: "X-Mumble", value: "mumble")
40-
],
41-
body: Data("👋".utf8)
41+
.init("x-mumble2")!: "mumble"
42+
]
43+
)
44+
let body: HTTPBody = "👋"
45+
let urlRequest = try await URLRequest(
46+
request,
47+
body: body,
48+
baseURL: URL(string: "http://example.com/api")!
4249
)
43-
let urlRequest = try URLRequest(request, baseURL: URL(string: "http://example.com/api")!)
4450
XCTAssertEqual(urlRequest.url, URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy"))
4551
XCTAssertEqual(urlRequest.httpMethod, "POST")
46-
XCTAssertEqual(urlRequest.allHTTPHeaderFields, ["X-Mumble": "mumble"])
52+
XCTAssertEqual(urlRequest.allHTTPHeaderFields?.count, 1)
53+
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "x-mumble2"), "mumble")
4754
XCTAssertEqual(urlRequest.httpBody, Data("👋".utf8))
4855
}
4956

50-
func testResponseConversion() throws {
57+
func testResponseConversion() async throws {
5158
let urlResponse: URLResponse = HTTPURLResponse(
52-
url: URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")!,
59+
url: URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!,
5360
statusCode: 201,
5461
httpVersion: "HTTP/1.1",
55-
headerFields: ["X-Mumble": "mumble"]
62+
headerFields: ["x-mumble3": "mumble"]
5663
)!
57-
let response = try OpenAPIRuntime.Response(from: urlResponse, body: Data("👋".utf8))
58-
XCTAssertEqual(response.statusCode, 201)
59-
XCTAssertEqual(response.headerFields, [.init(name: "X-Mumble", value: "mumble")])
60-
XCTAssertEqual(response.body, Data("👋".utf8))
64+
let (response, maybeResponseBody) = try HTTPResponse.response(
65+
method: .get,
66+
urlResponse: urlResponse,
67+
data: Data("👋".utf8)
68+
)
69+
let responseBody = try XCTUnwrap(maybeResponseBody)
70+
XCTAssertEqual(response.status.code, 201)
71+
XCTAssertEqual(response.headerFields, [.init("x-mumble3")!: "mumble"])
72+
let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max)
73+
XCTAssertEqual(bufferedResponseBody, "👋")
6174
}
6275

6376
func testSend() async throws {
64-
let endpointURL = URL(string: "http://example.com/api/hello/Maria?greeting=Howdy")!
77+
let endpointURL = URL(string: "http://example.com/api/hello%20world/Maria?greeting=Howdy")!
6578
MockURLProtocol.mockHTTPResponses.withValue { map in
6679
map[endpointURL] = .success(
6780
(
@@ -73,21 +86,26 @@ class URLSessionTransportTests: XCTestCase {
7386
let transport: any ClientTransport = URLSessionTransport(
7487
configuration: .init(session: MockURLProtocol.mockURLSession)
7588
)
76-
let request = OpenAPIRuntime.Request(
77-
path: "/hello/Maria",
78-
query: "greeting=Howdy",
89+
let request = HTTPRequest(
7990
method: .post,
91+
scheme: nil,
92+
authority: nil,
93+
path: "/hello%20world/Maria?greeting=Howdy",
8094
headerFields: [
81-
.init(name: "X-Mumble", value: "mumble")
95+
.init("x-mumble1")!: "mumble"
8296
]
8397
)
84-
let response = try await transport.send(
98+
let requestBody: HTTPBody = "👋"
99+
let (response, maybeResponseBody) = try await transport.send(
85100
request,
101+
body: requestBody,
86102
baseURL: URL(string: "http://example.com/api")!,
87103
operationID: "postGreeting"
88104
)
89-
XCTAssertEqual(response.statusCode, 201)
90-
XCTAssertEqual(response.body, Data("👋".utf8))
105+
let responseBody = try XCTUnwrap(maybeResponseBody)
106+
XCTAssertEqual(response.status.code, 201)
107+
let bufferedResponseBody = try await String(collecting: responseBody, upTo: .max)
108+
XCTAssertEqual(bufferedResponseBody, "👋")
91109
}
92110
}
93111

0 commit comments

Comments
 (0)