Skip to content

Commit

Permalink
feat: Make StackError a struct (#49)
Browse files Browse the repository at this point in the history
Co-authored-by: danthorpe <[email protected]>
  • Loading branch information
danthorpe and danthorpe authored Mar 5, 2024
1 parent 14f9cc8 commit b75d1db
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 86 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
import PackageDescription

var package = Package(name: "danthorpe-networking")
Expand Down
11 changes: 7 additions & 4 deletions Sources/Helpers/AsyncSequence+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import Foundation

extension AsyncSequence {
public typealias ProcessElement = @Sendable (Element) async -> Void
public typealias ProcessError = @Sendable (Error) async -> Void
public typealias TransformError = @Sendable (Error) async -> Error?
public typealias OnTermination = @Sendable () async -> Void

public func redirect(
into continuation: AsyncThrowingStream<Element, Error>.Continuation,
onElement processElement: ProcessElement? = nil,
onError processError: ProcessError? = nil,
mapError transformError: TransformError? = nil,
onTermination: OnTermination? = nil
) async {
do {
Expand All @@ -22,8 +22,11 @@ extension AsyncSequence {
continuation.finish()
await onTermination?()
} catch {
await processError?(error)
continuation.finish(throwing: error)
if let transform = transformError, let transformedError = await transform(error) {
continuation.finish(throwing: transformedError)
} else {
continuation.finish(throwing: error)
}
await onTermination?()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,17 @@ struct Authentication<Delegate: AuthenticationDelegate>: NetworkingModifier {
continuation.yield(event)
}
continuation.finish()
} catch let StackError.unauthorized(response) {
} catch let error as NetworkingError {
guard let response = error.isUnauthorizedResponse else {
throw error
}

let newRequest = try await refresh(
unauthorized: &credentials,
response: response,
continuation: continuation
)

await upstream.send(newRequest).redirect(into: continuation)
} catch {
continuation.finish(throwing: error)
Expand All @@ -76,6 +81,8 @@ struct Authentication<Delegate: AuthenticationDelegate>: NetworkingModifier {
}
}

// MARK: - Errors

public enum AuthenticationError: Error {
case fetchCredentialsFailed(HTTPRequestData, AuthenticationMethod, Error)
case refreshCredentialsFailed(HTTPResponseData, AuthenticationMethod, Error)
Expand Down
11 changes: 7 additions & 4 deletions Sources/Networking/Components/Cached.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ private struct Cached: NetworkingModifier {
}

await upstream.send(request)
.redirect(into: continuation, onElement: { element in
if !Task.isCancelled, case let .value(response, _) = element {
await cache.insert(response, forKey: request, cost: response.cacheCost, duration: timeToLive)
.redirect(
into: continuation,
onElement: { element in
if !Task.isCancelled, case let .value(response, _) = element {
await cache.insert(response, forKey: request, cost: response.cacheCost, duration: timeToLive)
}
}
}, onError: nil, onTermination: nil)
)
}
return stream
}
Expand Down
15 changes: 13 additions & 2 deletions Sources/Networking/Components/CheckedStatusCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ struct CheckedStatusCode: NetworkingModifier {
// Check for authentication issues
switch response.status {
case .unauthorized:
throw StackError.unauthorized(response)
throw StackError(unauthorized: response)
default:
throw StackError.statusCode(response)
throw StackError(statusCode: response)
}
}
}

// MARK: - Error Handling

extension StackError {
init(unauthorized response: HTTPResponseData) {
self.init(response: response, kind: .unauthorized)
}
init(statusCode response: HTTPResponseData) {
self.init(response: response, kind: .statusCode)
}
}
37 changes: 27 additions & 10 deletions Sources/Networking/Components/URLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,40 @@ extension URLSession: NetworkingComponent {
public func send(_ request: HTTPRequestData) -> ResponseStream<HTTPResponseData> {
let request = resolve(request)
.applyAuthenticationCredentials()

@NetworkEnvironment(\.instrument) var instrument

return ResponseStream<HTTPResponseData> { continuation in
guard let urlRequest = URLRequest(http: request) else {
continuation.finish(throwing: StackError(createURLRequestFailed: request))
return
}

Task {
@NetworkEnvironment(\.instrument) var instrument
guard let urlRequest = URLRequest(http: request) else {
continuation.finish(throwing: StackError.createURLRequestFailed(request))
return
}
do {
await send(urlRequest)
.map { partial in
try partial.mapValue { data, response in
try HTTPResponseData(request: request, data: data, urlResponse: response)
}
}
.eraseToThrowingStream()
.redirect(
into: continuation,
mapError: { error in
StackError(error, with: .request(request))
},
onTermination: {
await instrument?.measureElapsedTime("URLSession")
})
}
)
} catch {
await instrument?.measureElapsedTime("\(Self.self)")
continuation.finish(throwing: error)
continuation.finish(throwing: StackError(error, with: .request(request)))
}
}
}
}

@Sendable func send(_ request: URLRequest) -> ResponseStream<(Data, URLResponse)> {
func send(_ request: URLRequest) -> ResponseStream<(Data, URLResponse)> {
ResponseStream<(Data, URLResponse)> { continuation in
Task {
do {
Expand Down Expand Up @@ -96,3 +101,15 @@ extension URLSession: NetworkingComponent {
}
}
}

// MARK: - Errors

extension StackError {
init(createURLRequestFailed request: HTTPRequestData) {
self.init(
info: .request(request),
kind: .createURLRequestFailed,
error: NoUnderlyingError()
)
}
}
21 changes: 19 additions & 2 deletions Sources/Networking/Core/HTTPResponseData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct HTTPResponseData: Sendable {
let httpUrlResponse = (urlResponse as? HTTPURLResponse),
let httpResponse = httpUrlResponse.httpResponse
else {
throw StackError.invalidURLResponse(request, data, urlResponse)
throw StackError(invalidURLResponse: urlResponse, request: request, data: data)
}
self.init(request: request, data: data, httpUrlResponse: httpUrlResponse, httpResponse: httpResponse)
}
Expand All @@ -54,7 +54,7 @@ public struct HTTPResponseData: Sendable {
let body = try transform(payload, self)
return body
} catch let error as DecodingError {
throw StackError.decodeResponse(self, error)
throw StackError(decodeResponse: self, error: error)
}
}
}
Expand Down Expand Up @@ -183,3 +183,20 @@ private func ~= (lhs: HTTPURLResponse, rhs: HTTPURLResponse) -> Bool {
&& lhs.textEncodingName == rhs.textEncodingName
&& lhs.suggestedFilename == rhs.suggestedFilename
}

// MARK: - Error Handling

extension StackError {

init(invalidURLResponse urlResponse: URLResponse?, request: HTTPRequestData, data: Data) {
self.init(
info: .request(request),
kind: .invalidURLResponse(data, urlResponse),
error: NoUnderlyingError()
)
}

init(decodeResponse response: HTTPResponseData, error: Error) {
self.init(info: .response(response), kind: .decodingResponse, error: error)
}
}
15 changes: 14 additions & 1 deletion Sources/Networking/Core/NetworkingComponent+Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension NetworkingComponent {
}
.first(beforeTimeout: duration, using: clock())
} catch is TimeoutError {
throw StackError.timeout(request)
throw StackError(timeout: request)
}
}

Expand All @@ -47,3 +47,16 @@ extension NetworkingComponent {
request, progress: updateProgress, timeout: .seconds(request.requestTimeoutInSeconds))
}
}

// MARK: - Error Handling

extension StackError {

init(timeout request: HTTPRequestData) {
self.init(
info: .request(request),
kind: .timeout,
error: NoUnderlyingError()
)
}
}
8 changes: 4 additions & 4 deletions Sources/Networking/Errors/Error+.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
extension Error {

public var asNetworkingError: (any NetworkingError)? {
(self as? any NetworkingError)
}

public var httpRequest: HTTPRequestData? {
asNetworkingError?.request
}
Expand All @@ -11,8 +15,4 @@ extension Error {
public var httpBodyStringRepresentation: String? {
asNetworkingError?.bodyStringRepresentation
}

private var asNetworkingError: (any NetworkingError)? {
(self as? any NetworkingError)
}
}
28 changes: 17 additions & 11 deletions Sources/Networking/Errors/NetworkingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import Helpers
public protocol NetworkingError: Error {
var request: HTTPRequestData { get }
var response: HTTPResponseData? { get }

var requestDidTimeout: HTTPRequestData? { get }
var isUnauthorizedResponse: HTTPResponseData? { get }
}

// MARK: - Conveniences
Expand All @@ -20,18 +23,21 @@ extension NetworkingError {
return String(decoding: response.data, as: UTF8.self)
}

public var isTimeoutError: Bool {
if let status = response?.status, status == .requestTimeout {
return true
public var requestDidTimeout: HTTPRequestData? {
if let response, response.status == .requestTimeout {
return response.request
} else if let request = (self as? StackError)?.requestDidTimeout {
return request
}
switch self {
case let stackError as StackError:
if case .timeout = stackError {
return true
}
return false
default:
return false
return nil
}

public var isUnauthorizedResponse: HTTPResponseData? {
if let response = (self as? StackError)?.isUnauthorizedResponse {
return response
} else if let response, response.status == .unauthorized {
return response
}
return nil
}
}
37 changes: 37 additions & 0 deletions Sources/Networking/Errors/StackError+NetworkingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

extension StackError: NetworkingError {
package var request: HTTPRequestData {
switch info {
case let .request(request):
return request
case let .response(response):
return response.request
}
}

package var response: HTTPResponseData? {
switch info {
case .request:
return nil
case let .response(response):
return response
}
}

package var requestDidTimeout: HTTPRequestData? {
guard case .timeout = kind, case let .request(request) = info else {
return nil
}
return request
}

package var isUnauthorizedResponse: HTTPResponseData? {
if case .unauthorized = kind, case let .response(response) = info {
return response
} else if let response, response.status == .unauthorized {
return response
}
return nil
}
}
Loading

0 comments on commit b75d1db

Please sign in to comment.