Skip to content

Commit

Permalink
Absolute URL Support (#5)
Browse files Browse the repository at this point in the history
* Initial Address Implementation

* AbsoluteURLSessionClient and updated Downloader deprecation
  • Loading branch information
richardpiazza authored May 11, 2022
1 parent eb2ace5 commit c45e400
Show file tree
Hide file tree
Showing 18 changed files with 627 additions and 73 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let package = Package(

```swift
let url = URL(string: "https://api.agify.io")!
let client = URLSessionClient(baseURL: url)
let client = BaseURLSessionClient(baseURL: url)
let request = Get(queryItems: [URLQueryItem(name: "name", value: "bob")])
let response = try await client.request(request)
```
Expand Down
2 changes: 1 addition & 1 deletion Sources/SessionPlus/Deprecated/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UIKit
#endif

/// A wrapper for `URLSession` similar to `WebAPI` for general purpose downloading of data and images.
@available(*, deprecated, message: "This will be removed in future versions of SessionPlus.")
@available(*, deprecated, message: "Use `AbsoluteURLSessionClient` with `URLSessionConfiguration.cachingElseLoad()`.")
open class Downloader {

public typealias DataCompletion = (_ statusCode: Int, _ responseData: Data?, _ error: Error?) -> Void
Expand Down
42 changes: 42 additions & 0 deletions Sources/SessionPlus/Extensions/URLCache+SessionPlus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension URLCache {
enum Capacity {
case bytes(Int)
case megabytes(Int)
case gigabytes(Int)

public static var twentyFiveMB: Capacity = .megabytes(25)
public static var twoHundredMB: Capacity = .megabytes(200)

public var bytes: Int {
switch self {
case .bytes(let value):
return value
case .megabytes(let value):
return value * (1024 * 1024)
case .gigabytes(let value):
return value * (1024 * 1024 * 1024)
}
}
}

convenience init(memoryCapacity: Capacity = .twentyFiveMB, diskCapacity: Capacity = .twoHundredMB) {
#if canImport(FoundationNetworking)
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes, diskPath: "SessionPlusCache")
#else
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes)
} else {
#if targetEnvironment(macCatalyst)
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes)
#else
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes, diskPath: "SessionPlusCache")
#endif
}
#endif
}
}
33 changes: 31 additions & 2 deletions Sources/SessionPlus/Extensions/URLRequest+SessionPlus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,37 @@ public extension URLRequest {
/// - parameters:
/// - request: `Request` parameters used to customize the request.
/// - baseUrl: The root of the API address.
init(request: Request, baseUrl: URL) throws {
self.init(url: try request.url(using: baseUrl))
init(request: Request, baseUrl: URL? = nil) throws {
let url: URL
switch request.address {
case .absolute(let value):
url = value
case .path(let value):
guard let baseURL = baseUrl else {
throw URLError(.badURL)
}

let pathUrl = baseURL.appendingPathComponent(value)

guard let queryItems = request.queryItems else {
url = pathUrl
break
}

guard var components = URLComponents(url: pathUrl, resolvingAgainstBaseURL: false) else {
throw URLError(.badURL)
}

components.queryItems = queryItems

guard let _url = components.url else {
throw URLError(.badURL)
}

url = _url
}

self.init(url: url)

httpMethod = request.method.rawValue
setValue(Header.dateFormatter.string(from: Date()), forHeader: .date)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public extension URLSessionConfiguration {
/// A `URLSessionConfiguration` which includes a `URLCache` and has the `.returnCacheDataElseLoad` policy applied.
static func cachingElseLoad(
memoryCapacity: URLCache.Capacity = .twentyFiveMB,
diskCapacity: URLCache.Capacity = .twoHundredMB
) -> URLSessionConfiguration {
let configuration: URLSessionConfiguration = .default
configuration.urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity)
configuration.requestCachePolicy = .returnCacheDataElseLoad
return configuration
}
}
75 changes: 75 additions & 0 deletions Sources/SessionPlus/Implementation/AbsoluteURLSessionClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
#if canImport(Combine)
import Combine
#endif

/// A `Client` implementation that operates expecting all requests use _absolute_ urls.
open class AbsoluteURLSessionClient: Client {

public let session: URLSession

public init(sessionConfiguration: URLSessionConfiguration = .default, sessionDelegate: URLSessionDelegate? = nil) {
self.session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
}

#if swift(>=5.5.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
/// Implementation that uses the `URLSession` async/await concurrency apis for handling a `Request`/`Response` interaction.
///
/// The `URLSession` api is only available on Apple platforms, as the `FoundationNetworking` version has not been updated.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public func performRequest(_ request: Request) async throws -> Response {
let urlRequest = try URLRequest(request: request)
let sessionResponse = try await session.data(for: urlRequest)
return AnyResponse(statusCode: sessionResponse.1.statusCode, headers: sessionResponse.1.headers, data: sessionResponse.0)
}
#endif

#if canImport(Combine)
/// Implementation that uses the `URLSession.DataTaskPublisher` to handle the `Request`/`Response` interaction.
public func performRequest(_ request: Request) -> AnyPublisher<Response, Error> {
let urlRequest: URLRequest
do {
urlRequest = try URLRequest(request: request)
} catch {
return Fail(outputType: Response.self, failure: error).eraseToAnyPublisher()
}

return session
.dataTaskPublisher(for: urlRequest)
.tryMap { taskResponse -> Response in
AnyResponse(statusCode: taskResponse.response.statusCode, headers: taskResponse.response.headers, data: taskResponse.data)
}
.eraseToAnyPublisher()
}
#endif

/// Implementation that uses the default `URLSessionDataTask` methods for handling a `Request`/`Response` interaction.
public func performRequest(_ request: Request, completion: @escaping (Result<Response, Error>) -> Void) {
let urlRequest: URLRequest
do {
urlRequest = try URLRequest(request: request)
} catch {
completion(.failure(error))
return
}

session.dataTask(with: urlRequest) { data, urlResponse, error in
guard error == nil else {
completion(.failure(error!))
return
}

guard let httpResponse = urlResponse else {
completion(.failure(URLError(.cannotParseResponse)))
return
}

let response = AnyResponse(statusCode: httpResponse.statusCode, headers: httpResponse.headers, data: data ?? Data())
completion(.success(response))
}
.resume()
}
}
68 changes: 63 additions & 5 deletions Sources/SessionPlus/Implementation/AnyRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,93 @@ import Foundation

/// Generalized implementation of a `Request`.
public struct AnyRequest: Request {
public let path: String
public let address: Address
public let method: Method
public let headers: Headers?
public let queryItems: [URLQueryItem]?
public let body: Data?

public init(
path: String = "",
address: Address = .path(""),
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.path = path
self.address = address
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
path: String = "",
address: Address = .path(""),
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.path = path
self.address = address
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = try encoder.encode(encoding)
}

public init(
path: String,
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.address = .path(path)
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
path: String,
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.address = .path(path)
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = try encoder.encode(encoding)
}

public init(
url: URL,
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.address = .absolute(url)
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
url: URL,
method: Method = .get,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.address = .absolute(url)
self.method = method
self.headers = headers
self.queryItems = queryItems
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import FoundationNetworking
import Combine
#endif

open class URLSessionClient: Client {
@available(*, deprecated, renamed: "BaseURLSessionClient")
public typealias URLSessionClient = BaseURLSessionClient

/// A `Client` implementation that operates with a _base_ URL which all requests use to form the address.
open class BaseURLSessionClient: Client {

open var baseURL: URL
public let session: URLSession
Expand Down
60 changes: 55 additions & 5 deletions Sources/SessionPlus/Implementation/Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,82 @@ import Foundation

/// A convenience `Request` that uses `Method.delete`.
public struct Delete: Request {
public let path: String
public let address: Address
public let method: Method = .delete
public let headers: Headers?
public let queryItems: [URLQueryItem]?
public let body: Data?

public init(
path: String = "",
address: Address = .path(""),
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.path = path
self.address = address
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
path: String = "",
address: Address = .path(""),
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.path = path
self.address = address
self.headers = headers
self.queryItems = queryItems
self.body = try encoder.encode(encoding)
}

public init(
path: String,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.address = .path(path)
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
path: String,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.address = .path(path)
self.headers = headers
self.queryItems = queryItems
self.body = try encoder.encode(encoding)
}

public init(
url: URL,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.address = .absolute(url)
self.headers = headers
self.queryItems = queryItems
self.body = body
}

public init<E>(
url: URL,
headers: Headers? = nil,
queryItems: [URLQueryItem]? = nil,
encoding: E,
using encoder: JSONEncoder = JSONEncoder()
) throws where E: Encodable {
self.address = .absolute(url)
self.headers = headers
self.queryItems = queryItems
self.body = try encoder.encode(encoding)
Expand Down
Loading

0 comments on commit c45e400

Please sign in to comment.