Skip to content

Commit 12b9d6a

Browse files
authored
Merge pull request #48 from GoodRequest/feature/optional-url-init
Support for optionals in URL and Data resolution
2 parents 4b9a36b + 2488fc2 commit 12b9d6a

4 files changed

Lines changed: 90 additions & 18 deletions

File tree

Sources/GoodNetworking/Models/Endpoint.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,23 @@ public protocol Endpoint {
2828
@available(*, deprecated, message: "Encoding will be automatically determined by the kind of `parameters` in the future.")
2929
var encoding: ParameterEncoding { get }
3030

31-
/// Creates a URL by combining `path` with `baseUrl`.
31+
/// Creates a URL by resolving `path` over `baseUrl`.
32+
///
3233
/// This function is a customization point for modifying the URL by current runtime,
3334
/// for example for API versioning or platform separation.
35+
///
36+
/// Note that this function will be only called if the ``path`` resolved
37+
/// is a relative URL. If ``path`` specifies an absolute URL, it will be
38+
/// used instead, without any modifications.
39+
///
3440
/// - Parameter baseUrl: Base URL for the request to combine with.
35-
/// - Throws: If creating a concrete URL fails.
36-
/// - Returns: URL for the request.
41+
/// - Returns: URL for the request or `nil` if such URL cannot be constructed.
3742
@NetworkActor func url(on baseUrl: URLConvertible) async -> URL?
3843

3944
}
4045

41-
@available(*, deprecated, message: "Default values for deprecated properties")
4246
public extension Endpoint {
43-
44-
var encoding: ParameterEncoding { AutomaticEncoding.default }
45-
47+
4648
@NetworkActor func url(on baseUrl: URLConvertible) async -> URL? {
4749
let baseUrl = await baseUrl.resolveUrl()
4850
let path = await path.resolveUrl()
@@ -53,6 +55,13 @@ public extension Endpoint {
5355

5456
}
5557

58+
@available(*, deprecated, message: "Default values for deprecated properties")
59+
public extension Endpoint {
60+
61+
var encoding: ParameterEncoding { AutomaticEncoding.default }
62+
63+
}
64+
5665
// MARK: - Parameters
5766

5867
/// Enum that represents the data to be sent with the request,

Sources/GoodNetworking/Models/EndpointFactory.swift renamed to Sources/GoodNetworking/Models/EndpointBuilder.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// EndpointFactory.swift
2+
// EndpointBuilder.swift
33
// GoodNetworking
44
//
55
// Created by Filip Šašala on 06/08/2025.
@@ -10,7 +10,7 @@ import Foundation
1010
/// Modified implementation of factory pattern to build
1111
/// endpoint as a series of function calls instead of conforming
1212
/// to a protocol.
13-
public final class EndpointFactory: Endpoint {
13+
public final class EndpointBuilder: Endpoint {
1414

1515
public var path: URLConvertible
1616
public var method: HTTPMethod = .get
@@ -29,7 +29,7 @@ public final class EndpointFactory: Endpoint {
2929

3030
}
3131

32-
public extension EndpointFactory {
32+
public extension EndpointBuilder {
3333

3434
func method(_ method: HTTPMethod) -> Self {
3535
self.method = method
@@ -82,14 +82,17 @@ public extension EndpointFactory {
8282

8383
}
8484

85-
private extension EndpointFactory {
85+
private extension EndpointBuilder {
8686

8787
func assertBothQueryAndBodyUsage() {
8888
assert(self.parameters == nil, "Support for query and body parameters at the same time is currently not available.")
8989
}
9090

9191
}
9292

93-
public func at(_ path: URLConvertible) -> EndpointFactory {
94-
EndpointFactory(at: path)
93+
public func at(_ path: URLConvertible) -> EndpointBuilder {
94+
EndpointBuilder(at: path)
9595
}
96+
97+
@available(*, deprecated, renamed: "EndpointBuilder")
98+
public typealias EndpointFactory = EndpointBuilder

Sources/GoodNetworking/Models/URLConvertible.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,48 @@ extension String: URLConvertible {
4848
}
4949

5050
}
51+
52+
// MARK: - Extensions
53+
54+
extension URL {
55+
56+
/// Initialize with optional string.
57+
///
58+
/// Returns `nil` if a `URL` cannot be formed with the string (for example, if the string
59+
/// contains characters that are illegal in a URL, or is an empty string, or is `nil`).
60+
/// - Parameter string: String containing the URL or `nil`
61+
public init?(_ string: String?) {
62+
guard let string else { return nil }
63+
self.init(string: string)
64+
}
65+
66+
/// Checks if URL contains a non-empty scheme.
67+
///
68+
/// Returns `true` if ``scheme`` is not `nil` and not empty, eg. `https://`
69+
var hasScheme: Bool {
70+
if let scheme, !scheme.isEmpty {
71+
return true
72+
} else {
73+
return false
74+
}
75+
}
76+
77+
/// Checks if URL contains a non-empty host.
78+
///
79+
/// Returns `true` if ``host`` is not `nil` and not empty, eg. `goodrequest.com`
80+
var hasHost: Bool {
81+
if let host, !host.isEmpty {
82+
return true
83+
} else {
84+
return false
85+
}
86+
}
87+
88+
/// Checks if URL can be considered an absolute URL.
89+
///
90+
/// Returns `true` if the URL is a file URL or has both host and scheme.
91+
var isAbsolute: Bool {
92+
return isFileURL || hasScheme && hasHost
93+
}
94+
95+
}

Sources/GoodNetworking/Session/NetworkSession.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ extension NetworkSession {
212212
// handle decoding corner cases
213213
var decoder = JSONDecoder()
214214
switch T.self {
215-
case is Data.Type:
215+
case is Data.Type, is Optional<Data>.Type:
216216
return data as! T
217217

218218
case let t as WithCustomDecoder:
@@ -248,10 +248,25 @@ extension NetworkSession {
248248

249249
@discardableResult
250250
public func request(endpoint: Endpoint) async throws(NetworkError) -> Data {
251-
guard let basePath = await baseUrl.resolveUrl()?.absoluteString,
252-
let url = await endpoint.url(on: basePath)
253-
else {
254-
throw URLError(.badURL).asNetworkError()
251+
let endpointPath = await endpoint.path.resolveUrl()
252+
let url: URL
253+
254+
// If endpoint already contains an absolute path, do not concatenate
255+
// with baseURL and use that instead
256+
if let endpointPath, endpointPath.isAbsolute {
257+
url = endpointPath
258+
} else {
259+
// If endpoint has only relative path, resolve it over baseURL
260+
let baseUrl = await baseUrl.resolveUrl()
261+
let endpointResolvedUrl = await endpoint.url(on: baseUrl)
262+
263+
// If neither endpoint nor baseURL are specified, URL cannot be resolved
264+
guard let endpointResolvedUrl else {
265+
throw URLError(.badURL).asNetworkError()
266+
}
267+
268+
// URL is resolved
269+
url = endpointResolvedUrl
255270
}
256271

257272
// url + method

0 commit comments

Comments
 (0)