Skip to content

Commit c45e400

Browse files
Absolute URL Support (#5)
* Initial Address Implementation * AbsoluteURLSessionClient and updated Downloader deprecation
1 parent eb2ace5 commit c45e400

18 files changed

+627
-73
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ let package = Package(
4141

4242
```swift
4343
let url = URL(string: "https://api.agify.io")!
44-
let client = URLSessionClient(baseURL: url)
44+
let client = BaseURLSessionClient(baseURL: url)
4545
let request = Get(queryItems: [URLQueryItem(name: "name", value: "bob")])
4646
let response = try await client.request(request)
4747
```

Sources/SessionPlus/Deprecated/Downloader.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import UIKit
77
#endif
88

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

1313
public typealias DataCompletion = (_ statusCode: Int, _ responseData: Data?, _ error: Error?) -> Void
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
public extension URLCache {
7+
enum Capacity {
8+
case bytes(Int)
9+
case megabytes(Int)
10+
case gigabytes(Int)
11+
12+
public static var twentyFiveMB: Capacity = .megabytes(25)
13+
public static var twoHundredMB: Capacity = .megabytes(200)
14+
15+
public var bytes: Int {
16+
switch self {
17+
case .bytes(let value):
18+
return value
19+
case .megabytes(let value):
20+
return value * (1024 * 1024)
21+
case .gigabytes(let value):
22+
return value * (1024 * 1024 * 1024)
23+
}
24+
}
25+
}
26+
27+
convenience init(memoryCapacity: Capacity = .twentyFiveMB, diskCapacity: Capacity = .twoHundredMB) {
28+
#if canImport(FoundationNetworking)
29+
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes, diskPath: "SessionPlusCache")
30+
#else
31+
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
32+
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes)
33+
} else {
34+
#if targetEnvironment(macCatalyst)
35+
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes)
36+
#else
37+
self.init(memoryCapacity: memoryCapacity.bytes, diskCapacity: diskCapacity.bytes, diskPath: "SessionPlusCache")
38+
#endif
39+
}
40+
#endif
41+
}
42+
}

Sources/SessionPlus/Extensions/URLRequest+SessionPlus.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,37 @@ public extension URLRequest {
99
/// - parameters:
1010
/// - request: `Request` parameters used to customize the request.
1111
/// - baseUrl: The root of the API address.
12-
init(request: Request, baseUrl: URL) throws {
13-
self.init(url: try request.url(using: baseUrl))
12+
init(request: Request, baseUrl: URL? = nil) throws {
13+
let url: URL
14+
switch request.address {
15+
case .absolute(let value):
16+
url = value
17+
case .path(let value):
18+
guard let baseURL = baseUrl else {
19+
throw URLError(.badURL)
20+
}
21+
22+
let pathUrl = baseURL.appendingPathComponent(value)
23+
24+
guard let queryItems = request.queryItems else {
25+
url = pathUrl
26+
break
27+
}
28+
29+
guard var components = URLComponents(url: pathUrl, resolvingAgainstBaseURL: false) else {
30+
throw URLError(.badURL)
31+
}
32+
33+
components.queryItems = queryItems
34+
35+
guard let _url = components.url else {
36+
throw URLError(.badURL)
37+
}
38+
39+
url = _url
40+
}
41+
42+
self.init(url: url)
1443

1544
httpMethod = request.method.rawValue
1645
setValue(Header.dateFormatter.string(from: Date()), forHeader: .date)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
public extension URLSessionConfiguration {
7+
/// A `URLSessionConfiguration` which includes a `URLCache` and has the `.returnCacheDataElseLoad` policy applied.
8+
static func cachingElseLoad(
9+
memoryCapacity: URLCache.Capacity = .twentyFiveMB,
10+
diskCapacity: URLCache.Capacity = .twoHundredMB
11+
) -> URLSessionConfiguration {
12+
let configuration: URLSessionConfiguration = .default
13+
configuration.urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity)
14+
configuration.requestCachePolicy = .returnCacheDataElseLoad
15+
return configuration
16+
}
17+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
#if canImport(Combine)
6+
import Combine
7+
#endif
8+
9+
/// A `Client` implementation that operates expecting all requests use _absolute_ urls.
10+
open class AbsoluteURLSessionClient: Client {
11+
12+
public let session: URLSession
13+
14+
public init(sessionConfiguration: URLSessionConfiguration = .default, sessionDelegate: URLSessionDelegate? = nil) {
15+
self.session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
16+
}
17+
18+
#if swift(>=5.5.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
19+
/// Implementation that uses the `URLSession` async/await concurrency apis for handling a `Request`/`Response` interaction.
20+
///
21+
/// The `URLSession` api is only available on Apple platforms, as the `FoundationNetworking` version has not been updated.
22+
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
23+
public func performRequest(_ request: Request) async throws -> Response {
24+
let urlRequest = try URLRequest(request: request)
25+
let sessionResponse = try await session.data(for: urlRequest)
26+
return AnyResponse(statusCode: sessionResponse.1.statusCode, headers: sessionResponse.1.headers, data: sessionResponse.0)
27+
}
28+
#endif
29+
30+
#if canImport(Combine)
31+
/// Implementation that uses the `URLSession.DataTaskPublisher` to handle the `Request`/`Response` interaction.
32+
public func performRequest(_ request: Request) -> AnyPublisher<Response, Error> {
33+
let urlRequest: URLRequest
34+
do {
35+
urlRequest = try URLRequest(request: request)
36+
} catch {
37+
return Fail(outputType: Response.self, failure: error).eraseToAnyPublisher()
38+
}
39+
40+
return session
41+
.dataTaskPublisher(for: urlRequest)
42+
.tryMap { taskResponse -> Response in
43+
AnyResponse(statusCode: taskResponse.response.statusCode, headers: taskResponse.response.headers, data: taskResponse.data)
44+
}
45+
.eraseToAnyPublisher()
46+
}
47+
#endif
48+
49+
/// Implementation that uses the default `URLSessionDataTask` methods for handling a `Request`/`Response` interaction.
50+
public func performRequest(_ request: Request, completion: @escaping (Result<Response, Error>) -> Void) {
51+
let urlRequest: URLRequest
52+
do {
53+
urlRequest = try URLRequest(request: request)
54+
} catch {
55+
completion(.failure(error))
56+
return
57+
}
58+
59+
session.dataTask(with: urlRequest) { data, urlResponse, error in
60+
guard error == nil else {
61+
completion(.failure(error!))
62+
return
63+
}
64+
65+
guard let httpResponse = urlResponse else {
66+
completion(.failure(URLError(.cannotParseResponse)))
67+
return
68+
}
69+
70+
let response = AnyResponse(statusCode: httpResponse.statusCode, headers: httpResponse.headers, data: data ?? Data())
71+
completion(.success(response))
72+
}
73+
.resume()
74+
}
75+
}

Sources/SessionPlus/Implementation/AnyRequest.swift

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,93 @@ import Foundation
22

33
/// Generalized implementation of a `Request`.
44
public struct AnyRequest: Request {
5-
public let path: String
5+
public let address: Address
66
public let method: Method
77
public let headers: Headers?
88
public let queryItems: [URLQueryItem]?
99
public let body: Data?
1010

1111
public init(
12-
path: String = "",
12+
address: Address = .path(""),
1313
method: Method = .get,
1414
headers: Headers? = nil,
1515
queryItems: [URLQueryItem]? = nil,
1616
body: Data? = nil
1717
) {
18-
self.path = path
18+
self.address = address
1919
self.method = method
2020
self.headers = headers
2121
self.queryItems = queryItems
2222
self.body = body
2323
}
2424

2525
public init<E>(
26-
path: String = "",
26+
address: Address = .path(""),
2727
method: Method = .get,
2828
headers: Headers? = nil,
2929
queryItems: [URLQueryItem]? = nil,
3030
encoding: E,
3131
using encoder: JSONEncoder = JSONEncoder()
3232
) throws where E: Encodable {
33-
self.path = path
33+
self.address = address
34+
self.method = method
35+
self.headers = headers
36+
self.queryItems = queryItems
37+
self.body = try encoder.encode(encoding)
38+
}
39+
40+
public init(
41+
path: String,
42+
method: Method = .get,
43+
headers: Headers? = nil,
44+
queryItems: [URLQueryItem]? = nil,
45+
body: Data? = nil
46+
) {
47+
self.address = .path(path)
48+
self.method = method
49+
self.headers = headers
50+
self.queryItems = queryItems
51+
self.body = body
52+
}
53+
54+
public init<E>(
55+
path: String,
56+
method: Method = .get,
57+
headers: Headers? = nil,
58+
queryItems: [URLQueryItem]? = nil,
59+
encoding: E,
60+
using encoder: JSONEncoder = JSONEncoder()
61+
) throws where E: Encodable {
62+
self.address = .path(path)
63+
self.method = method
64+
self.headers = headers
65+
self.queryItems = queryItems
66+
self.body = try encoder.encode(encoding)
67+
}
68+
69+
public init(
70+
url: URL,
71+
method: Method = .get,
72+
headers: Headers? = nil,
73+
queryItems: [URLQueryItem]? = nil,
74+
body: Data? = nil
75+
) {
76+
self.address = .absolute(url)
77+
self.method = method
78+
self.headers = headers
79+
self.queryItems = queryItems
80+
self.body = body
81+
}
82+
83+
public init<E>(
84+
url: URL,
85+
method: Method = .get,
86+
headers: Headers? = nil,
87+
queryItems: [URLQueryItem]? = nil,
88+
encoding: E,
89+
using encoder: JSONEncoder = JSONEncoder()
90+
) throws where E: Encodable {
91+
self.address = .absolute(url)
3492
self.method = method
3593
self.headers = headers
3694
self.queryItems = queryItems

Sources/SessionPlus/Implementation/URLSessionClient.swift renamed to Sources/SessionPlus/Implementation/BaseURLSessionClient.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import FoundationNetworking
66
import Combine
77
#endif
88

9-
open class URLSessionClient: Client {
9+
@available(*, deprecated, renamed: "BaseURLSessionClient")
10+
public typealias URLSessionClient = BaseURLSessionClient
11+
12+
/// A `Client` implementation that operates with a _base_ URL which all requests use to form the address.
13+
open class BaseURLSessionClient: Client {
1014

1115
open var baseURL: URL
1216
public let session: URLSession

Sources/SessionPlus/Implementation/Delete.swift

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,82 @@ import Foundation
22

33
/// A convenience `Request` that uses `Method.delete`.
44
public struct Delete: Request {
5-
public let path: String
5+
public let address: Address
66
public let method: Method = .delete
77
public let headers: Headers?
88
public let queryItems: [URLQueryItem]?
99
public let body: Data?
1010

1111
public init(
12-
path: String = "",
12+
address: Address = .path(""),
1313
headers: Headers? = nil,
1414
queryItems: [URLQueryItem]? = nil,
1515
body: Data? = nil
1616
) {
17-
self.path = path
17+
self.address = address
1818
self.headers = headers
1919
self.queryItems = queryItems
2020
self.body = body
2121
}
2222

2323
public init<E>(
24-
path: String = "",
24+
address: Address = .path(""),
2525
headers: Headers? = nil,
2626
queryItems: [URLQueryItem]? = nil,
2727
encoding: E,
2828
using encoder: JSONEncoder = JSONEncoder()
2929
) throws where E: Encodable {
30-
self.path = path
30+
self.address = address
31+
self.headers = headers
32+
self.queryItems = queryItems
33+
self.body = try encoder.encode(encoding)
34+
}
35+
36+
public init(
37+
path: String,
38+
headers: Headers? = nil,
39+
queryItems: [URLQueryItem]? = nil,
40+
body: Data? = nil
41+
) {
42+
self.address = .path(path)
43+
self.headers = headers
44+
self.queryItems = queryItems
45+
self.body = body
46+
}
47+
48+
public init<E>(
49+
path: String,
50+
headers: Headers? = nil,
51+
queryItems: [URLQueryItem]? = nil,
52+
encoding: E,
53+
using encoder: JSONEncoder = JSONEncoder()
54+
) throws where E: Encodable {
55+
self.address = .path(path)
56+
self.headers = headers
57+
self.queryItems = queryItems
58+
self.body = try encoder.encode(encoding)
59+
}
60+
61+
public init(
62+
url: URL,
63+
headers: Headers? = nil,
64+
queryItems: [URLQueryItem]? = nil,
65+
body: Data? = nil
66+
) {
67+
self.address = .absolute(url)
68+
self.headers = headers
69+
self.queryItems = queryItems
70+
self.body = body
71+
}
72+
73+
public init<E>(
74+
url: URL,
75+
headers: Headers? = nil,
76+
queryItems: [URLQueryItem]? = nil,
77+
encoding: E,
78+
using encoder: JSONEncoder = JSONEncoder()
79+
) throws where E: Encodable {
80+
self.address = .absolute(url)
3181
self.headers = headers
3282
self.queryItems = queryItems
3383
self.body = try encoder.encode(encoding)

0 commit comments

Comments
 (0)