Skip to content

Commit bae4735

Browse files
Async/Await Support (#2)
* Async Request Execution * Implemented `Codable` and `Injectable` Async/Await. * Limit Async/Await to Darwin (Apple) Systems. * Removed duplicate UIKit import. * Updated documentation and tests
1 parent b426177 commit bae4735

File tree

13 files changed

+421
-128
lines changed

13 files changed

+421
-128
lines changed

README.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ A collection of extensions & wrappers around URLSession.
1212

1313
This package has been designed to work across multiple swift environments by utilizing conditional checks. It has been tested on Apple platforms (macOS, iOS, tvOS, watchOS), as well as Linux (Ubuntu).
1414

15+
## Installation
16+
17+
**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager).
18+
To install it into a project, add it as a dependency within your `Package.swift` manifest:
19+
20+
```swift
21+
let package = Package(
22+
...
23+
dependencies: [
24+
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.2.0")
25+
],
26+
...
27+
)
28+
```
29+
30+
Then import the **SessionPlus** packages wherever you'd like to use it:
31+
32+
```swift
33+
import SessionPlus
34+
```
35+
1536
## Quick Start
1637

1738
Checkout the `WebAPI` class.
@@ -51,7 +72,7 @@ public protocol HTTPClient {
5172

5273
`URLSession` is task-driven. The **SessionPlus** api is designed with this in mind; allowing you to construct your request and then either creating a _data task_ for you to references and execute, or automatically executing the request.
5374

54-
Example conformances for `request(method:path:queryItems:data:)`, `task(request:, completion)`, & `execut(request:completion:)` are provided in an extension, so the minimum required conformance to `HTTPClient` is `baseURL`, `session`, and `authorization`.
75+
Example conformances for `request(method:path:queryItems:data:)`, `task(request:, completion)`, & `execute(request:completion:)` are provided in an extension, so the minimum required conformance to `HTTPClient` is `baseURL`, `session`, and `authorization`.
5576

5677
Convenience methods for the common HTTP request methods **get**, **put**, **post**, **delete**, and **patch**, are all provided.
5778

@@ -75,24 +96,3 @@ public protocol HTTPInjectable {
7596
```
7697

7798
The `HTTPInjectable` protocol is used to extend an `HTTPClient` implementation by overriding the default `execute(request:completion:)` implementation to allow for the definition and usage of predefined responses. This makes for simple testing!
78-
79-
## Installation
80-
81-
**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager).
82-
To install it into a project, add it as a dependency within your `Package.swift` manifest:
83-
84-
```swift
85-
let package = Package(
86-
...
87-
dependencies: [
88-
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.1.0")
89-
],
90-
...
91-
)
92-
```
93-
94-
Then import the **SessionPlus** packages wherever you'd like to use it:
95-
96-
```swift
97-
import SessionPlus
98-
```

Sources/SessionPlus/Downloader.swift

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,42 @@ import Foundation
22
#if canImport(FoundationNetworking)
33
import FoundationNetworking
44
#endif
5+
#if canImport(UIKit)
6+
import UIKit
7+
#endif
58

6-
public typealias DownloaderDataCompletion = (_ statusCode: Int, _ responseData: Data?, _ error: Error?) -> Void
9+
@available(*, deprecated, renamed: "Downloader.DataCompletion")
10+
public typealias DownloaderDataCompletion = Downloader.DataCompletion
711

812
/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
913
open class Downloader {
14+
15+
public typealias DataCompletion = (_ statusCode: Int, _ responseData: Data?, _ error: Error?) -> Void
16+
#if swift(>=5.5) && canImport(ObjectiveC)
17+
public typealias AsyncDataCompletion = (statusCode: Int, responseData: Data)
18+
#endif
19+
20+
#if canImport(UIKit)
21+
public typealias ImageCompletion = (_ statusCode: Int, _ responseImage: UIImage?, _ error: Error?) -> Void
22+
#if swift(>=5.5)
23+
public typealias AsyncImageCompletion = (statusCode: Int, image: UIImage)
24+
#endif
25+
#endif
26+
1027
fileprivate static let twentyFiveMB: Int = (1024 * 1024 * 25)
1128
fileprivate static let twoHundredMB: Int = (1024 * 1024 * 200)
1229

1330
public enum Errors: Error {
1431
case invalidBaseURL
32+
case invalidResponseData
1533

1634
public var localizedDescription: String {
17-
return "Invalid Base URL: You can not use a `path` method without specifying a baseURL."
35+
switch self {
36+
case .invalidBaseURL:
37+
return "Invalid Base URL: You can not use a `path` method without specifying a baseURL."
38+
case .invalidResponseData:
39+
return "Invalid Response Data: The response data was nil or failed to be decoded."
40+
}
1841
}
1942
}
2043

@@ -59,18 +82,9 @@ open class Downloader {
5982
return baseURL.appendingPathComponent(path)
6083
}
6184

62-
open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderDataCompletion) {
63-
guard let url = self.urlForPath(path) else {
64-
completion(0, nil, Errors.invalidBaseURL)
65-
return
66-
}
67-
68-
self.getDataAtURL(url, cachePolicy: cachePolicy, completion: completion)
69-
}
70-
71-
open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderDataCompletion) {
85+
open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DataCompletion) {
7286
let request = NSMutableURLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeout)
73-
request.httpMethod = "GET"
87+
request.httpMethod = HTTP.RequestMethod.get.rawValue
7488

7589
let urlRequest: URLRequest = request as URLRequest
7690

@@ -96,25 +110,49 @@ open class Downloader {
96110
#endif
97111
}) .resume()
98112
}
99-
}
100-
101-
#if (os(iOS) || os(tvOS))
102-
import UIKit
103-
104-
public typealias DownloaderImageCompletion = (_ statusCode: Int, _ responseImage: UIImage?, _ error: Error?) -> Void
105-
106-
/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
107-
public extension Downloader {
108-
func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderImageCompletion) {
113+
114+
open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DataCompletion) {
109115
guard let url = self.urlForPath(path) else {
110116
completion(0, nil, Errors.invalidBaseURL)
111117
return
112118
}
113119

114-
self.getImageAtURL(url, cachePolicy: cachePolicy, completion: completion)
120+
self.getDataAtURL(url, cachePolicy: cachePolicy, completion: completion)
115121
}
116122

117-
func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderImageCompletion) {
123+
#if swift(>=5.5) && canImport(ObjectiveC)
124+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
125+
open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncDataCompletion {
126+
let request = NSMutableURLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeout)
127+
request.httpMethod = HTTP.RequestMethod.get.rawValue
128+
let urlRequest = request as URLRequest
129+
130+
let sessionData = try await session.data(for: urlRequest)
131+
guard let httpResponse = sessionData.1 as? HTTPURLResponse else {
132+
throw HTTP.Error.invalidResponse
133+
}
134+
135+
return (httpResponse.statusCode, sessionData.0)
136+
}
137+
138+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
139+
open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncDataCompletion {
140+
guard let url = self.urlForPath(path) else {
141+
throw Errors.invalidBaseURL
142+
}
143+
144+
return try await getDataAtURL(url, cachePolicy: cachePolicy)
145+
}
146+
#endif
147+
}
148+
149+
#if canImport(UIKit)
150+
@available(*, deprecated, renamed: "Downloader.ImageCompletion")
151+
public typealias DownloaderImageCompletion = Downloader.ImageCompletion
152+
153+
/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
154+
public extension Downloader {
155+
func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping ImageCompletion) {
118156
self.getDataAtURL(url, cachePolicy: cachePolicy) { (statusCode, responseData, error) -> Void in
119157
var image: UIImage?
120158
if responseData != nil {
@@ -124,5 +162,33 @@ public extension Downloader {
124162
completion(statusCode, image, error)
125163
}
126164
}
165+
166+
func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping ImageCompletion) {
167+
guard let url = self.urlForPath(path) else {
168+
completion(0, nil, Errors.invalidBaseURL)
169+
return
170+
}
171+
172+
self.getImageAtURL(url, cachePolicy: cachePolicy, completion: completion)
173+
}
174+
175+
#if swift(>=5.5)
176+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
177+
func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncImageCompletion {
178+
let response = try await getDataAtURL(url, cachePolicy: cachePolicy)
179+
guard let image = UIImage(data: response.responseData) else {
180+
throw Errors.invalidResponseData
181+
}
182+
return (response.statusCode, image)
183+
}
184+
185+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
186+
func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncImageCompletion {
187+
guard let url = self.urlForPath(path) else {
188+
throw Errors.invalidBaseURL
189+
}
190+
return try await getImageAtURL(url, cachePolicy: cachePolicy)
191+
}
192+
#endif
127193
}
128194
#endif
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
public extension HTTP {
4+
/// Authorization schemes used in the API
5+
enum Authorization {
6+
case basic(username: String, password: String?)
7+
case bearer(token: String)
8+
case custom(headerField: String, headerValue: String)
9+
10+
public var headerValue: String {
11+
switch self {
12+
case .basic(let username, let password):
13+
let pwd = password ?? ""
14+
guard let data = "\(username):\(pwd)".data(using: .utf8) else {
15+
return ""
16+
}
17+
18+
let base64 = data.base64EncodedString(options: [])
19+
20+
return "Basic \(base64)"
21+
case .bearer(let token):
22+
return "Bearer \(token)"
23+
case .custom(let headerField, let headerValue):
24+
return "\(headerField) \(headerValue))"
25+
}
26+
}
27+
}
28+
}

Sources/SessionPlus/HTTP+Header.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import Foundation
2-
#if canImport(FoundationNetworking)
3-
import FoundationNetworking
4-
#endif
52

63
public extension HTTP {
74
/// Command HTTP Header

Sources/SessionPlus/HTTP+MIMEType.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import Foundation
2-
#if canImport(FoundationNetworking)
3-
import FoundationNetworking
4-
#endif
52

63
public extension HTTP {
74
/// MIME Types used in the API

Sources/SessionPlus/HTTP+RequestMethod.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import Foundation
2-
#if canImport(FoundationNetworking)
3-
import FoundationNetworking
4-
#endif
52

63
public extension HTTP {
74
/// Desired action to be performed for a given resource.

Sources/SessionPlus/HTTP.swift

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,7 @@ public struct HTTP {
99
/// HTTP Headers as provided from HTTPURLResponse
1010
public typealias Headers = [AnyHashable : Any]
1111

12-
/// Authorization schemes used in the API
13-
public enum Authorization {
14-
case basic(username: String, password: String?)
15-
case bearer(token: String)
16-
case custom(headerField: String, headerValue: String)
17-
18-
public var headerValue: String {
19-
switch self {
20-
case .basic(let username, let password):
21-
let pwd = password ?? ""
22-
guard let data = "\(username):\(pwd)".data(using: .utf8) else {
23-
return ""
24-
}
25-
26-
let base64 = data.base64EncodedString(options: [])
27-
28-
return "Basic \(base64)"
29-
case .bearer(let token):
30-
return "Bearer \(token)"
31-
case .custom(let headerField, let headerValue):
32-
return "\(headerField) \(headerValue))"
33-
}
34-
}
35-
}
36-
37-
/// General errors that may be emitted during HTTP Request/Response handling.
12+
/// General errors that may be encountered during HTTP request/response handling.
3813
public enum Error: Swift.Error, LocalizedError {
3914
case invalidURL
4015
case invalidRequest
@@ -54,13 +29,28 @@ public struct HTTP {
5429

5530
/// A general completion handler for HTTP requests.
5631
public typealias DataTaskCompletion = (_ statusCode: Int, _ headers: Headers?, _ data: Data?, _ error: Swift.Error?) -> Void
32+
33+
#if swift(>=5.5) && canImport(ObjectiveC)
34+
/// The output of an async url request execution.
35+
public typealias AsyncDataTaskOutput = (statusCode: Int, headers: Headers, data: Data)
36+
#endif
5737
}
5838

5939
public extension URLRequest {
40+
/// Sets a value for the header field.
41+
///
42+
/// - parameters:
43+
/// - value: The new value for the header field. Any existing value for the field is replaced by the new value.
44+
/// - header: The header for which to set the value. (Headers are case sensitive)
6045
mutating func setValue(_ value: String, forHTTPHeader header: HTTP.Header) {
6146
self.setValue(value, forHTTPHeaderField: header.rawValue)
6247
}
6348

49+
/// Sets a value for the header field.
50+
///
51+
/// - parameters:
52+
/// - value: The new value for the header field. Any existing value for the field is replaced by the new value.
53+
/// - header: The header for which to set the value. (Headers are case sensitive)
6454
mutating func setValue(_ value: HTTP.MIMEType, forHTTPHeader header: HTTP.Header) {
6555
self.setValue(value.rawValue, forHTTPHeaderField: header.rawValue)
6656
}

0 commit comments

Comments
 (0)