diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox-Package.xcscheme
new file mode 100644
index 00000000..33a3a0bb
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox-Package.xcscheme
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox.xcscheme
new file mode 100644
index 00000000..9b81af09
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropbox.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropboxObjC.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropboxObjC.xcscheme
new file mode 100644
index 00000000..f79f9c2a
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftyDropboxObjC.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/SwiftyDropbox/Shared/Handwritten/Errors.swift b/Source/SwiftyDropbox/Shared/Handwritten/Errors.swift
index 85e9ce52..c7a0f015 100644
--- a/Source/SwiftyDropbox/Shared/Handwritten/Errors.swift
+++ b/Source/SwiftyDropbox/Shared/Handwritten/Errors.swift
@@ -106,6 +106,32 @@ public enum CallError: Error, CustomStringConvertible {
return "\(err)"
}
}
+
+ // Used for global error handlers which cannot know the type of the boxed route error
+ public var typeErased: CallError {
+ switch self {
+ case .internalServerError(let code, let msg, let requestId):
+ return .internalServerError(code, msg, requestId)
+ case .badInputError(let msg, let requestId):
+ return .badInputError(msg, requestId)
+ case .rateLimitError(let error, let locMsg, let msg, let requestId):
+ return .rateLimitError(error, locMsg, msg, requestId)
+ case .httpError(let code, let msg, let requestId):
+ return .httpError(code, msg, requestId)
+ case .authError(let error, let locMsg, let msg, let requestId):
+ return .authError(error, locMsg, msg, requestId)
+ case .accessError(let error, let locMsg, let msg, let requestId):
+ return .accessError(error, locMsg, msg, requestId)
+ case .routeError(let boxedError, let locMsg, let msg, let requestId):
+ return .routeError(Box(boxedError.unboxed as Any), locMsg, msg, requestId)
+ case .serializationError(let error):
+ return .serializationError(error)
+ case .reconnectionError(let error):
+ return .reconnectionError(error)
+ case .clientError(let error):
+ return .clientError(error)
+ }
+ }
static func error(response: HTTPURLResponse, data: Data?, errorSerializer: ESerial) throws -> CallError {
let requestId = requestId(from: response)
diff --git a/Source/SwiftyDropbox/Shared/Handwritten/GlobalErrorResponseHandler.swift b/Source/SwiftyDropbox/Shared/Handwritten/GlobalErrorResponseHandler.swift
new file mode 100644
index 00000000..f2844b37
--- /dev/null
+++ b/Source/SwiftyDropbox/Shared/Handwritten/GlobalErrorResponseHandler.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+/// Similar to `DBGlobalErrorResponseHandler` in the Objc SDK
+/// It does not have special handling for route errors, which end up type-erased right now
+/// It also does not allow you to easily retry requests yet, like Objc's does.
+public class GlobalErrorResponseHandler {
+ static var shared = { GlobalErrorResponseHandler() }()
+
+ private struct Handler {
+ let callback: (CallError) -> Void
+ let queue: OperationQueue
+ }
+
+ // Locked state
+ private struct State {
+ var handlers: [Handler] = []
+ }
+
+ private var state = UnfairLock(value: State())
+
+ internal init() { }
+
+ func reportGlobalError(_ error: CallError) {
+ state.read { lockedState in
+ lockedState.handlers.forEach { handler in
+ handler.queue.addOperation {
+ handler.callback(error)
+ }
+ }
+ }
+ }
+
+ func registerGlobalErrorHandler(_ callback: @escaping (CallError) -> Void, queue: OperationQueue = .main) {
+ state.mutate { lockedState in
+ lockedState.handlers.append(Handler(callback: callback, queue: queue))
+ }
+ }
+}
diff --git a/Source/SwiftyDropbox/Shared/Handwritten/Request.swift b/Source/SwiftyDropbox/Shared/Handwritten/Request.swift
index 3ca67c42..15d690c1 100644
--- a/Source/SwiftyDropbox/Shared/Handwritten/Request.swift
+++ b/Source/SwiftyDropbox/Shared/Handwritten/Request.swift
@@ -72,6 +72,16 @@ public class Request {
}
func handleResponseError(networkTaskFailure: NetworkTaskFailure) -> CallError {
+ let callError = parseCallError(from: networkTaskFailure)
+
+ // We call the global error response handler to alert it to an error
+ // But unlike in the objc SDK we do not stop the SDK from calling the per-route completion handler as well.
+ GlobalErrorResponseHandler.shared.reportGlobalError(callError.typeErased)
+
+ return callError
+ }
+
+ private func parseCallError(from networkTaskFailure: NetworkTaskFailure) -> CallError {
switch networkTaskFailure {
case .badStatusCode(let data, _, let response):
return CallError(response, data: data, errorSerializer: errorSerializer)
diff --git a/Source/SwiftyDropbox/Shared/Handwritten/Utilities.swift b/Source/SwiftyDropbox/Shared/Handwritten/Utilities.swift
index 77d9b7ca..08047b06 100644
--- a/Source/SwiftyDropbox/Shared/Handwritten/Utilities.swift
+++ b/Source/SwiftyDropbox/Shared/Handwritten/Utilities.swift
@@ -124,3 +124,46 @@ public enum LogHelper {
log(backgroundSessionLogLevel, "bg session - \(message)")
}
}
+
+// MARK: Locking
+/// Wrapper around a value with a lock
+///
+/// Copied from https://github.com/home-assistant/HAKit/blob/main/Source/Internal/HAProtected.swift
+public class UnfairLock {
+ private var value: ValueType
+ private let lock: os_unfair_lock_t = {
+ let value = os_unfair_lock_t.allocate(capacity: 1)
+ value.initialize(to: os_unfair_lock())
+ return value
+ }()
+
+ /// Create a new protected value
+ /// - Parameter value: The initial value
+ public init(value: ValueType) {
+ self.value = value
+ }
+
+ deinit {
+ lock.deinitialize(count: 1)
+ lock.deallocate()
+ }
+
+ /// Get and optionally change the value
+ /// - Parameter handler: Will be invoked immediately with the current value as an inout parameter.
+ /// - Returns: The value returned by the handler block
+ @discardableResult
+ public func mutate(using handler: (inout ValueType) -> HandlerType) -> HandlerType {
+ os_unfair_lock_lock(lock)
+ defer { os_unfair_lock_unlock(lock) }
+ return handler(&value)
+ }
+
+ /// Read the value and get a result out of it
+ /// - Parameter handler: Will be invoked immediately with the current value.
+ /// - Returns: The value returned by the handler block
+ public func read(_ handler: (ValueType) -> T) -> T {
+ os_unfair_lock_lock(lock)
+ defer { os_unfair_lock_unlock(lock) }
+ return handler(value)
+ }
+}
diff --git a/Source/SwiftyDropboxUnitTests/GlobalErrorResponseHandlerTests.swift b/Source/SwiftyDropboxUnitTests/GlobalErrorResponseHandlerTests.swift
new file mode 100644
index 00000000..29b1087c
--- /dev/null
+++ b/Source/SwiftyDropboxUnitTests/GlobalErrorResponseHandlerTests.swift
@@ -0,0 +1,63 @@
+import XCTest
+@testable import SwiftyDropbox
+
+class GlobalErrorResponseHandlerTests: XCTestCase {
+ private var handler: GlobalErrorResponseHandler!
+ override func setUp() {
+ handler = GlobalErrorResponseHandler()
+ super.setUp()
+ }
+
+ func testGlobalHandlerReportsNonRouteError() {
+ let error = CallError.internalServerError(567, "abc", "def")
+ let expectation = XCTestExpectation(description: "Callback is called")
+ handler.registerGlobalErrorHandler { error in
+ guard case .internalServerError(let code, let message, let requestId) = error else {
+ return XCTFail("Expected error")
+ }
+ XCTAssertEqual(code, 567)
+ XCTAssertEqual(message, "abc")
+ XCTAssertEqual(requestId, "def")
+ expectation.fulfill()
+ }
+ handler.reportGlobalError(error.typeErased)
+ }
+
+ func testGlobalHandlerReportsRouteError() {
+ let error = CallError.routeError(Box("value"), LocalizedUserMessage(text: "ábc", locale: "EN-US"), "abc", "def")
+ let expectation = XCTestExpectation(description: "Callback is called")
+ handler.registerGlobalErrorHandler { error in
+ guard case .routeError(let boxedValue, let locMessage, let message, let requestId) = error else {
+ return XCTFail("Expected error")
+ }
+ XCTAssertEqual(boxedValue.unboxed as? String, "value")
+ XCTAssertEqual(locMessage?.text, "ábc")
+ XCTAssertEqual(locMessage?.locale, "EN-US")
+ XCTAssertEqual(message, "abc")
+ XCTAssertEqual(requestId, "def")
+ expectation.fulfill()
+ }
+ handler.reportGlobalError(error.typeErased)
+ }
+}
+
+class CallErrorTypeErasureTests: XCTestCase {
+ let cases: [CallError] = [
+ .internalServerError(567, "abc", "def"),
+ .badInputError("abc", "def"),
+ .rateLimitError(.init(reason: .tooManyRequests), LocalizedUserMessage(text: "ábc", locale: "EN-US"), "abc", "def"),
+ .httpError(456, "abc", "def"),
+ .authError(Auth.AuthError.userSuspended, LocalizedUserMessage(text: "ábc", locale: "EN-US"), "abc", "def"),
+ .accessError(Auth.AccessError.other, LocalizedUserMessage(text: "ábc", locale: "EN-US"), "abc", "def"),
+ .routeError(Box("value"), LocalizedUserMessage(text: "ábc", locale: "EN-US"), "abc", "def"),
+ .serializationError(SerializationError.missingResultData),
+ .reconnectionError(ReconnectionError(reconnectionErrorKind: .badPersistedStringFormat, taskDescription: "bad")),
+ .clientError(.unexpectedState),
+ ]
+ func testErrorTypeErasureProvidesSameDescription() {
+ for error in cases {
+ let typeErased = error.typeErased
+ XCTAssertEqual(typeErased.description, error.description)
+ }
+ }
+}