Skip to content

Commit 684af55

Browse files
authored
HTTP Client abstraction (#3040)
motivation: incoming features use HTTP to access remote resources: package collection and package registry. we should abstract given complexy around error handling and non-apple platform support changes: * create HTTPClient utility in Basics modules * implement URLSession based backend * implment exponential backoff * implement (naive) circuit breaker * add tests Next Steps: * discuss our approach to non-apple platforms (given FoundationNetworking dependency on OpenSSL)
1 parent 37c9234 commit 684af55

8 files changed

+1256
-2
lines changed

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ let package = Package(
208208

209209
// MARK: SwiftPM tests
210210

211+
.testTarget(
212+
name: "BasicsTests",
213+
dependencies: ["Basics", "SPMTestSupport"]),
211214
.testTarget(
212215
name: "BuildTests",
213216
dependencies: ["Build", "SPMTestSupport"]),

Sources/Basics/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

99
add_library(Basics
10-
FileSystem+Extensions.swift)
10+
DispatchTimeInterval+Extensions.swift
11+
FileSystem+Extensions.swift
12+
HTPClient+URLSession.swift
13+
HTTPClient.swift)
1114
target_link_libraries(Basics PUBLIC
1215
TSCBasic
1316
TSCUtility)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Dispatch
12+
import struct Foundation.TimeInterval
13+
14+
extension DispatchTimeInterval {
15+
func timeInterval() -> TimeInterval? {
16+
switch self {
17+
case .seconds(let value):
18+
return Double(value)
19+
case .milliseconds(let value):
20+
return Double(value) / 1000
21+
case .microseconds(let value):
22+
return Double(value) / 1_000_000
23+
case .nanoseconds(let value):
24+
return Double(value) / 1_000_000_000
25+
default:
26+
return nil
27+
}
28+
}
29+
30+
func milliseconds() -> Int? {
31+
switch self {
32+
case .seconds(let value):
33+
return value.multipliedReportingOverflow(by: 1000).partialValue
34+
case .milliseconds(let value):
35+
return value
36+
case .microseconds(let value):
37+
return Int(Double(value) / 1000)
38+
case .nanoseconds(let value):
39+
return Int(Double(value) / 1_000_000)
40+
default:
41+
return nil
42+
}
43+
}
44+
45+
func seconds() -> Int? {
46+
switch self {
47+
case .seconds(let value):
48+
return value
49+
case .milliseconds(let value):
50+
return Int(Double(value) / 1000)
51+
case .microseconds(let value):
52+
return Int(Double(value) / 1_000_000)
53+
case .nanoseconds(let value):
54+
return Int(Double(value) / 1_000_000_000)
55+
default:
56+
return nil
57+
}
58+
}
59+
}

Sources/Basics/FileSystem+Extensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension FileSystem {
1818
}
1919

2020
extension FileSystem {
21-
/// SwiftPM cache directory under usre's caches directory (if exists)
21+
/// SwiftPM cache directory under user's caches directory (if exists)
2222
public var swiftPMCacheDirectory: AbsolutePath {
2323
if let cachesDirectory = self.cachesDirectory {
2424
return cachesDirectory.appending(component: "org.swift.swiftpm")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
2+
3+
import Foundation
4+
import struct TSCUtility.Versioning
5+
#if canImport(FoundationNetworking)
6+
// FIXME: this brings OpenSSL dependency on Linux
7+
// need to decide how to best deal with that
8+
import FoundationNetworking
9+
#endif
10+
11+
public struct URLSessionHTTPClient {
12+
private let configuration: URLSessionConfiguration
13+
14+
public init(configuration: URLSessionConfiguration) {
15+
self.configuration = configuration
16+
}
17+
18+
public func execute(request: HTTPClient.Request, callback: @escaping (Result<HTTPClient.Response, Error>) -> Void) {
19+
let session = URLSession(configuration: self.configuration)
20+
let task = session.dataTask(with: request.urlRequest()) { data, response, error in
21+
if let error = error {
22+
callback(.failure(error))
23+
} else if let response = response as? HTTPURLResponse {
24+
callback(.success(response.response(body: data)))
25+
} else {
26+
callback(.failure(HTTPClientError.invalidResponse))
27+
}
28+
}
29+
task.resume()
30+
}
31+
}
32+
33+
extension HTTPClient.Request {
34+
func urlRequest() -> URLRequest {
35+
var request = URLRequest(url: self.url)
36+
request.httpMethod = self.methodString()
37+
self.headers.forEach { header in
38+
request.addValue(header.value, forHTTPHeaderField: header.name)
39+
}
40+
request.httpBody = self.body
41+
if let interval = self.options.timeout?.timeInterval() {
42+
request.timeoutInterval = interval
43+
}
44+
return request
45+
}
46+
47+
func methodString() -> String {
48+
switch self.method {
49+
case .head:
50+
return "HEAD"
51+
case .get:
52+
return "GET"
53+
case .post:
54+
return "POST"
55+
case .put:
56+
return "PUT"
57+
case .delete:
58+
return "DELETE"
59+
}
60+
}
61+
}
62+
63+
extension HTTPURLResponse {
64+
func response(body: Data?) -> HTTPClient.Response {
65+
let headers = HTTPClientHeaders(self.allHeaderFields.map { header in
66+
.init(name: "\(header.key)", value: "\(header.value)")
67+
})
68+
return HTTPClient.Response(statusCode: self.statusCode,
69+
statusText: Self.localizedString(forStatusCode: self.statusCode),
70+
headers: headers,
71+
body: body)
72+
}
73+
}

0 commit comments

Comments
 (0)