Skip to content

Commit

Permalink
Async/Await Support (#2)
Browse files Browse the repository at this point in the history
* Async Request Execution
* Implemented `Codable` and `Injectable` Async/Await.
* Limit Async/Await to Darwin (Apple) Systems.
* Removed duplicate UIKit import.
* Updated documentation and tests
  • Loading branch information
richardpiazza authored Nov 29, 2021
1 parent b426177 commit bae4735
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 128 deletions.
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ A collection of extensions & wrappers around URLSession.

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).

## Installation

**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager).
To install it into a project, add it as a dependency within your `Package.swift` manifest:

```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.2.0")
],
...
)
```

Then import the **SessionPlus** packages wherever you'd like to use it:

```swift
import SessionPlus
```

## Quick Start

Checkout the `WebAPI` class.
Expand Down Expand Up @@ -51,7 +72,7 @@ public protocol HTTPClient {

`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.

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`.
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`.

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

Expand All @@ -75,24 +96,3 @@ public protocol HTTPInjectable {
```

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!

## Installation

**SessionPlus** is distributed using the [Swift Package Manager](https://swift.org/package-manager).
To install it into a project, add it as a dependency within your `Package.swift` manifest:

```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/richardpiazza/SessionPlus.git", .upToNextMinor(from: "1.1.0")
],
...
)
```

Then import the **SessionPlus** packages wherever you'd like to use it:

```swift
import SessionPlus
```
116 changes: 91 additions & 25 deletions Sources/SessionPlus/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@ import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
#if canImport(UIKit)
import UIKit
#endif

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

/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
open class Downloader {

public typealias DataCompletion = (_ statusCode: Int, _ responseData: Data?, _ error: Error?) -> Void
#if swift(>=5.5) && canImport(ObjectiveC)
public typealias AsyncDataCompletion = (statusCode: Int, responseData: Data)
#endif

#if canImport(UIKit)
public typealias ImageCompletion = (_ statusCode: Int, _ responseImage: UIImage?, _ error: Error?) -> Void
#if swift(>=5.5)
public typealias AsyncImageCompletion = (statusCode: Int, image: UIImage)
#endif
#endif

fileprivate static let twentyFiveMB: Int = (1024 * 1024 * 25)
fileprivate static let twoHundredMB: Int = (1024 * 1024 * 200)

public enum Errors: Error {
case invalidBaseURL
case invalidResponseData

public var localizedDescription: String {
return "Invalid Base URL: You can not use a `path` method without specifying a baseURL."
switch self {
case .invalidBaseURL:
return "Invalid Base URL: You can not use a `path` method without specifying a baseURL."
case .invalidResponseData:
return "Invalid Response Data: The response data was nil or failed to be decoded."
}
}
}

Expand Down Expand Up @@ -59,18 +82,9 @@ open class Downloader {
return baseURL.appendingPathComponent(path)
}

open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderDataCompletion) {
guard let url = self.urlForPath(path) else {
completion(0, nil, Errors.invalidBaseURL)
return
}

self.getDataAtURL(url, cachePolicy: cachePolicy, completion: completion)
}

open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderDataCompletion) {
open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DataCompletion) {
let request = NSMutableURLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeout)
request.httpMethod = "GET"
request.httpMethod = HTTP.RequestMethod.get.rawValue

let urlRequest: URLRequest = request as URLRequest

Expand All @@ -96,25 +110,49 @@ open class Downloader {
#endif
}) .resume()
}
}

#if (os(iOS) || os(tvOS))
import UIKit

public typealias DownloaderImageCompletion = (_ statusCode: Int, _ responseImage: UIImage?, _ error: Error?) -> Void

/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
public extension Downloader {
func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderImageCompletion) {

open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping DataCompletion) {
guard let url = self.urlForPath(path) else {
completion(0, nil, Errors.invalidBaseURL)
return
}

self.getImageAtURL(url, cachePolicy: cachePolicy, completion: completion)
self.getDataAtURL(url, cachePolicy: cachePolicy, completion: completion)
}

func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping DownloaderImageCompletion) {
#if swift(>=5.5) && canImport(ObjectiveC)
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
open func getDataAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncDataCompletion {
let request = NSMutableURLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeout)
request.httpMethod = HTTP.RequestMethod.get.rawValue
let urlRequest = request as URLRequest

let sessionData = try await session.data(for: urlRequest)
guard let httpResponse = sessionData.1 as? HTTPURLResponse else {
throw HTTP.Error.invalidResponse
}

return (httpResponse.statusCode, sessionData.0)
}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
open func getDataAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncDataCompletion {
guard let url = self.urlForPath(path) else {
throw Errors.invalidBaseURL
}

return try await getDataAtURL(url, cachePolicy: cachePolicy)
}
#endif
}

#if canImport(UIKit)
@available(*, deprecated, renamed: "Downloader.ImageCompletion")
public typealias DownloaderImageCompletion = Downloader.ImageCompletion

/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
public extension Downloader {
func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy, completion: @escaping ImageCompletion) {
self.getDataAtURL(url, cachePolicy: cachePolicy) { (statusCode, responseData, error) -> Void in
var image: UIImage?
if responseData != nil {
Expand All @@ -124,5 +162,33 @@ public extension Downloader {
completion(statusCode, image, error)
}
}

func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy, completion: @escaping ImageCompletion) {
guard let url = self.urlForPath(path) else {
completion(0, nil, Errors.invalidBaseURL)
return
}

self.getImageAtURL(url, cachePolicy: cachePolicy, completion: completion)
}

#if swift(>=5.5)
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
func getImageAtURL(_ url: URL, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncImageCompletion {
let response = try await getDataAtURL(url, cachePolicy: cachePolicy)
guard let image = UIImage(data: response.responseData) else {
throw Errors.invalidResponseData
}
return (response.statusCode, image)
}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
func getImageAtPath(_ path: String, cachePolicy: URLRequest.CachePolicy) async throws -> AsyncImageCompletion {
guard let url = self.urlForPath(path) else {
throw Errors.invalidBaseURL
}
return try await getImageAtURL(url, cachePolicy: cachePolicy)
}
#endif
}
#endif
28 changes: 28 additions & 0 deletions Sources/SessionPlus/HTTP+Authorization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

public extension HTTP {
/// Authorization schemes used in the API
enum Authorization {
case basic(username: String, password: String?)
case bearer(token: String)
case custom(headerField: String, headerValue: String)

public var headerValue: String {
switch self {
case .basic(let username, let password):
let pwd = password ?? ""
guard let data = "\(username):\(pwd)".data(using: .utf8) else {
return ""
}

let base64 = data.base64EncodedString(options: [])

return "Basic \(base64)"
case .bearer(let token):
return "Bearer \(token)"
case .custom(let headerField, let headerValue):
return "\(headerField) \(headerValue))"
}
}
}
}
3 changes: 0 additions & 3 deletions Sources/SessionPlus/HTTP+Header.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// Command HTTP Header
Expand Down
3 changes: 0 additions & 3 deletions Sources/SessionPlus/HTTP+MIMEType.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// MIME Types used in the API
Expand Down
3 changes: 0 additions & 3 deletions Sources/SessionPlus/HTTP+RequestMethod.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension HTTP {
/// Desired action to be performed for a given resource.
Expand Down
42 changes: 16 additions & 26 deletions Sources/SessionPlus/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,7 @@ public struct HTTP {
/// HTTP Headers as provided from HTTPURLResponse
public typealias Headers = [AnyHashable : Any]

/// Authorization schemes used in the API
public enum Authorization {
case basic(username: String, password: String?)
case bearer(token: String)
case custom(headerField: String, headerValue: String)

public var headerValue: String {
switch self {
case .basic(let username, let password):
let pwd = password ?? ""
guard let data = "\(username):\(pwd)".data(using: .utf8) else {
return ""
}

let base64 = data.base64EncodedString(options: [])

return "Basic \(base64)"
case .bearer(let token):
return "Bearer \(token)"
case .custom(let headerField, let headerValue):
return "\(headerField) \(headerValue))"
}
}
}

/// General errors that may be emitted during HTTP Request/Response handling.
/// General errors that may be encountered during HTTP request/response handling.
public enum Error: Swift.Error, LocalizedError {
case invalidURL
case invalidRequest
Expand All @@ -54,13 +29,28 @@ public struct HTTP {

/// A general completion handler for HTTP requests.
public typealias DataTaskCompletion = (_ statusCode: Int, _ headers: Headers?, _ data: Data?, _ error: Swift.Error?) -> Void

#if swift(>=5.5) && canImport(ObjectiveC)
/// The output of an async url request execution.
public typealias AsyncDataTaskOutput = (statusCode: Int, headers: Headers, data: Data)
#endif
}

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

/// Sets a value for the header field.
///
/// - parameters:
/// - value: The new value for the header field. Any existing value for the field is replaced by the new value.
/// - header: The header for which to set the value. (Headers are case sensitive)
mutating func setValue(_ value: HTTP.MIMEType, forHTTPHeader header: HTTP.Header) {
self.setValue(value.rawValue, forHTTPHeaderField: header.rawValue)
}
Expand Down
Loading

0 comments on commit bae4735

Please sign in to comment.