diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index f7e0d0399b2b67ef32c8bece58cfb102e76864a2..8f1e26f79d8f80884a0e60b4a81a249fb00f76d8 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -106,8 +106,10 @@ let package = Package( .target( name: "RegisterFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings ), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index d3cc134a7dbca79b4d9e75472eb26697f08a455b..19f1b7f5a750f278ca41ff031acf08409cccaa02 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -33,11 +33,14 @@ extension AppEnvironment { home: { HomeEnvironment( messenger: messenger, + db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, register: { RegisterEnvironment( messenger: messenger, + db: dbManager.getDB, + now: Date.init, mainQueue: mainQueue, bgQueue: bgQueue ) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 535d69fcbc3044d7698040497d5eba6020d5e426..582478d479dcc31b5e12a491e6843919bdd4aa43 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -68,7 +68,7 @@ extension AppEnvironment { let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in switch action { - case .start, .welcome(.finished), .restore(.finished): + case .start, .welcome(.finished), .restore(.finished), .home(.didDeleteAccount): state.screen = .loading return .run { subscriber in do { diff --git a/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift new file mode 100644 index 0000000000000000000000000000000000000000..a3347be43974d0e957ea1850369469360bb70311 --- /dev/null +++ b/Examples/xx-messenger/Sources/HomeFeature/Alerts.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture + +extension AlertState { + public static func confirmAccountDeletion() -> AlertState<HomeAction> { + AlertState<HomeAction>( + title: TextState("Delete Account"), + message: TextState("This will permanently delete your account and can't be undone."), + buttons: [ + .destructive(TextState("Delete"), action: .send(.deleteAccountConfirmed)), + .cancel(TextState("Cancel")) + ] + ) + } + + public static func accountDeletionFailed(_ error: Error) -> AlertState<HomeAction> { + AlertState<HomeAction>( + title: TextState("Error"), + message: TextState(error.localizedDescription), + buttons: [] + ) + } +} diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 29abfb6ba9d977e0e87b0cb31bf7eca167b15886..279a5628bd12f2e1a6e18beb6940b1388a92f455 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -1,3 +1,4 @@ +import AppCore import Combine import ComposableArchitecture import ComposablePresentation @@ -8,22 +9,28 @@ import XXMessengerClient public struct HomeState: Equatable { public init( - username: String? = nil, failure: String? = nil, - register: RegisterState? = nil + register: RegisterState? = nil, + alert: AlertState<HomeAction>? = nil, + isDeletingAccount: Bool = false ) { - self.username = username self.failure = failure self.register = register + self.alert = alert + self.isDeletingAccount = isDeletingAccount } - @BindableState public var username: String? @BindableState public var failure: String? @BindableState public var register: RegisterState? + @BindableState public var alert: AlertState<HomeAction>? + @BindableState public var isDeletingAccount: Bool } public enum HomeAction: Equatable, BindableAction { case start + case deleteAccountButtonTapped + case deleteAccountConfirmed + case didDeleteAccount case binding(BindingAction<HomeState>) case register(RegisterAction) } @@ -31,17 +38,20 @@ public enum HomeAction: Equatable, BindableAction { public struct HomeEnvironment { public init( messenger: Messenger, + db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment ) { self.messenger = messenger + self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register } public var messenger: Messenger + public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment @@ -50,6 +60,7 @@ public struct HomeEnvironment { extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, + db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented } @@ -76,14 +87,36 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> } try env.messenger.logIn() } + } catch { + subscriber.send(.set(\.$failure, error.localizedDescription)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .deleteAccountButtonTapped: + state.alert = .confirmAccountDeletion() + return .none - if let contact = env.messenger.e2e()?.getContact(), - let facts = try? contact.getFacts(), - let username = facts.first(where: { $0.type == 0 })?.fact { - subscriber.send(.set(\.$username, username)) + case .deleteAccountConfirmed: + state.isDeletingAccount = true + return .run { subscriber in + do { + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + let contact = try env.db().fetchContacts(.init(id: [contactId])).first + if let username = contact?.username { + let ud = try env.messenger.ud.tryGet() + try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0)) } + try env.messenger.destroy() + try env.db().drop() + subscriber.send(.didDeleteAccount) } catch { - subscriber.send(.set(\.$failure, error.localizedDescription)) + subscriber.send(.set(\.$isDeletingAccount, false)) + subscriber.send(.set(\.$alert, .accountDeletionFailed(error))) } subscriber.send(completion: .finished) return AnyCancellable {} @@ -92,6 +125,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> .receive(on: env.mainQueue) .eraseToEffect() + case .didDeleteAccount: + state.isDeletingAccount = false + return .none + case .register(.finished): state.register = nil return Effect(value: .start) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index e40bb1db3982e1673ca2c77d6b8b0e72fa697da9..f73f6b8a9ff2696763a995f3358032d3229d2ca3 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -11,12 +11,12 @@ public struct HomeView: View { let store: Store<HomeState, HomeAction> struct ViewState: Equatable { - var username: String? var failure: String? + var isDeletingAccount: Bool init(state: HomeState) { - username = state.username failure = state.failure + isDeletingAccount = state.isDeletingAccount } } @@ -24,14 +24,6 @@ public struct HomeView: View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in NavigationView { Form { - if let username = viewStore.username { - Section { - Text(username) - } header: { - Text("Username") - } - } - if let failure = viewStore.failure { Section { Text(failure) @@ -44,8 +36,29 @@ public struct HomeView: View { Text("Error") } } + + Section { + Button(role: .destructive) { + viewStore.send(.deleteAccountButtonTapped) + } label: { + HStack { + Text("Delete Account") + Spacer() + if viewStore.isDeletingAccount { + ProgressView() + } + } + } + .disabled(viewStore.isDeletingAccount) + } header: { + Text("Account") + } } .navigationTitle("Home") + .alert( + store.scope(state: \.alert), + dismiss: HomeAction.set(\.$alert, nil) + ) } .navigationViewStyle(.stack) .task { viewStore.send(.start) } diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index f929d18fd5bdc1a004cd05f4bb366b5c0d835c03..a8d3280c5e321cb78ade668570dcc40159f0b7b7 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -1,6 +1,9 @@ +import AppCore import ComposableArchitecture import SwiftUI +import XCTestDynamicOverlay import XXMessengerClient +import XXModels public struct RegisterState: Equatable { public enum Field: String, Hashable { @@ -33,15 +36,21 @@ public enum RegisterAction: Equatable, BindableAction { public struct RegisterEnvironment { public init( messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue> ) { self.messenger = messenger + self.db = db + self.now = now self.mainQueue = mainQueue self.bgQueue = bgQueue } public var messenger: Messenger + public var db: DBManagerGetDB + public var now: () -> Date public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> } @@ -49,6 +58,8 @@ public struct RegisterEnvironment { extension RegisterEnvironment { public static let unimplemented = RegisterEnvironment( messenger: .unimplemented, + db: .unimplemented, + now: XCTUnimplemented("\(Self.self).now"), mainQueue: .unimplemented, bgQueue: .unimplemented ) @@ -66,7 +77,15 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi state.failure = nil return .future { [username = state.username] fulfill in do { + let db = try env.db() try env.messenger.register(username: username) + let contact = env.messenger.e2e()!.getContact() + try db.saveContact(Contact( + id: try contact.getId(), + marshaled: contact.data, + username: username, + createdAt: env.now() + )) fulfill(.success(.finished)) } catch { diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift index 27bc0962780d7214095ff26592d66c9f6703ae52..195b655b4806b3156a4fe4353fb4cd2ec708c51b 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift @@ -46,14 +46,12 @@ public struct RegisterView: View { viewStore.send(.registerTapped) } label: { HStack { + Text("Register") + Spacer() if viewStore.isRegistering { - ProgressView().padding(.trailing) - Text("Registering...") - } else { - Text("Register") + ProgressView() } } - .frame(maxWidth: .infinity) } } diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift index d3686c5aa5f1b410cea89176feecdcf050dccb3a..1205c67eece34b9214ce16b3eb6e22df93d37c4b 100644 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift @@ -29,21 +29,18 @@ public struct WelcomeView: View { viewStore.send(.newAccountTapped) } label: { HStack { + Text("New Account") + Spacer() if viewStore.isCreatingAccount { - ProgressView().padding(.trailing) - Text("Creating Account...") - } else { - Text("New Account") + ProgressView() } } - .frame(maxWidth: .infinity) } Button { viewStore.send(.restoreTapped) } label: { Text("Restore from Backup") - .frame(maxWidth: .infinity) } } } diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index 5f0554922106e525c5f6c151c958b602dd8f5552..5ea968131af6851815f66f6b22edf2d2d139dc56 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -141,6 +141,36 @@ final class AppFeatureTests: XCTestCase { } } + func testHomeDidDeleteAccount() { + let store = TestStore( + initialState: AppState( + screen: .home(HomeState()) + ), + reducer: appReducer, + environment: .unimplemented + ) + + let mainQueue = DispatchQueue.test + let bgQueue = DispatchQueue.test + + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.dbManager.hasDB.run = { true } + store.environment.messenger.isLoaded.run = { false } + store.environment.messenger.isCreated.run = { false } + + store.send(.home(.didDeleteAccount)) { + $0.screen = .loading + } + + bgQueue.advance() + mainQueue.advance() + + store.receive(.set(\.$screen, .welcome(WelcomeState()))) { + $0.screen = .welcome(WelcomeState()) + } + } + func testWelcomeRestoreTapped() { let store = TestStore( initialState: AppState( diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index efdd867ff247317e7dd6dc379f38edfbc5ba7b3b..4fbad5b8c8a3d7775391db66359bd3c97f268ded 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -3,6 +3,7 @@ import RegisterFeature import XCTest import XXClient import XXMessengerClient +import XXModels @testable import HomeFeature final class HomeFeatureTests: XCTestCase { @@ -47,7 +48,6 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let username = "test_username" let bgQueue = DispatchQueue.test let mainQueue = DispatchQueue.test var messengerDidStartWithTimeout: [Int] = [] @@ -62,15 +62,6 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } - store.environment.messenger.e2e.get = { - var e2e = E2E.unimplemented - e2e.getContact.run = { - var contact = Contact.unimplemented(Data()) - contact.getFactsFromContact.run = { _ in [Fact(fact: username, type: 0)] } - return contact - } - return e2e - } store.send(.start) @@ -81,10 +72,6 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidLogIn, 1) mainQueue.advance() - - store.receive(.set(\.$username, username)) { - $0.username = username - } } func testRegisterFinished() { @@ -96,7 +83,6 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let username = "test_username" let bgQueue = DispatchQueue.test let mainQueue = DispatchQueue.test var messengerDidStartWithTimeout: [Int] = [] @@ -109,15 +95,6 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } - store.environment.messenger.e2e.get = { - var e2e = E2E.unimplemented - e2e.getContact.run = { - var contact = Contact.unimplemented(Data()) - contact.getFactsFromContact.run = { _ in [Fact(fact: username, type: 0)] } - return contact - } - return e2e - } store.send(.register(.finished)) { $0.register = nil @@ -131,10 +108,6 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidLogIn, 1) mainQueue.advance() - - store.receive(.set(\.$username, username)) { - $0.username = username - } } func testStartMessengerStartFailure() { @@ -229,4 +202,112 @@ final class HomeFeatureTests: XCTestCase { $0.failure = error.localizedDescription } } + + func testAccountDeletion() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + var dbDidFetchContacts: [XXModels.Contact.Query] = [] + var udDidPermanentDeleteAccount: [Fact] = [] + var messengerDidDestroy = 0 + var dbDidDrop = 0 + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .failing + db.fetchContacts.run = { query in + dbDidFetchContacts.append(query) + return [ + XXModels.Contact( + id: "contact-id".data(using: .utf8)!, + marshaled: "contact-data".data(using: .utf8)!, + username: "MyUsername" + ) + ] + } + db.drop.run = { + dbDidDrop += 1 + } + return db + } + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.permanentDeleteAccount.run = { usernameFact in + udDidPermanentDeleteAccount.append(usernameFact) + } + return ud + } + store.environment.messenger.destroy.run = { + messengerDidDestroy += 1 + } + + store.send(.deleteAccountButtonTapped) { + $0.alert = .confirmAccountDeletion() + } + + store.send(.set(\.$alert, nil)) { + $0.alert = nil + } + + store.send(.deleteAccountConfirmed) { + $0.isDeletingAccount = true + } + + XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])]) + XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)]) + XCTAssertNoDifference(messengerDidDestroy, 1) + XCTAssertNoDifference(dbDidDrop, 1) + + store.receive(.didDeleteAccount) { + $0.isDeletingAccount = false + } + } + + func testAccountDeletionFailure() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + struct Failure: Error {} + let error = Failure() + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in throw error } + return contact + } + return e2e + } + + store.send(.deleteAccountConfirmed) { + $0.isDeletingAccount = true + } + + store.receive(.set(\.$isDeletingAccount, false)) { + $0.isDeletingAccount = false + } + + store.receive(.set(\.$alert, .accountDeletionFailed(error))) { + $0.alert = .accountDeletionFailed(error) + } + } } diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift index 21d4da16412ab852ff11e1b40da58ac1a5fd5388..a2aad5bd7977dbd210a163d8cdac9001de239723 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift @@ -1,8 +1,10 @@ import ComposableArchitecture import XCTest +import XXClient +import XXMessengerClient +import XXModels @testable import RegisterFeature -@MainActor final class RegisterFeatureTests: XCTestCase { func testRegister() throws { let store = TestStore( @@ -11,15 +13,35 @@ final class RegisterFeatureTests: XCTestCase { environment: .unimplemented ) + let now = Date() let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test + var dbDidSaveContact: [XXModels.Contact] = [] var messengerDidRegisterUsername: [String] = [] + store.environment.now = { now } store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.bgQueue = bgQueue.eraseToAnyScheduler() store.environment.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .failing + db.saveContact.run = { contact in + dbDidSaveContact.append(contact) + return contact + } + return db + } store.send(.set(\.$username, "NewUser")) { $0.username = "NewUser" @@ -30,17 +52,56 @@ final class RegisterFeatureTests: XCTestCase { } XCTAssertNoDifference(messengerDidRegisterUsername, []) + XCTAssertNoDifference(dbDidSaveContact, []) bgQueue.advance() XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"]) + XCTAssertNoDifference(dbDidSaveContact, [ + XXModels.Contact( + id: "contact-id".data(using: .utf8)!, + marshaled: "contact-data".data(using: .utf8)!, + username: "NewUser", + createdAt: now + ) + ]) mainQueue.advance() store.receive(.finished) } - func testRegisterFailure() throws { + func testGetDbFailure() throws { + struct Error: Swift.Error, Equatable {} + let error = Error() + + let store = TestStore( + initialState: RegisterState(), + reducer: registerReducer, + environment: .unimplemented + ) + + let mainQueue = DispatchQueue.test + let bgQueue = DispatchQueue.test + + store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.db.run = { throw error } + + store.send(.registerTapped) { + $0.isRegistering = true + } + + bgQueue.advance() + mainQueue.advance() + + store.receive(.failed(error.localizedDescription)) { + $0.isRegistering = false + $0.failure = error.localizedDescription + } + } + + func testMessengerRegisterFailure() throws { struct Error: Swift.Error, Equatable {} let error = Error() @@ -55,6 +116,7 @@ final class RegisterFeatureTests: XCTestCase { store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.db.run = { .failing } store.environment.messenger.register.run = { _ in throw error } store.send(.registerTapped) { diff --git a/Sources/XXMessengerClient/Utils/Stored.swift b/Sources/XXMessengerClient/Utils/Stored.swift index 3f5d5fdb186db87d79872f4ee44d3ad1cf799979..052d638705c6dce4a619fbe18d0d55cf2159733c 100644 --- a/Sources/XXMessengerClient/Utils/Stored.swift +++ b/Sources/XXMessengerClient/Utils/Stored.swift @@ -31,6 +31,23 @@ private final class Memory<Value> { var value: Value } +extension Stored { + public struct MissingValueError: Error, Equatable { + public init(typeDescription: String) { + self.typeDescription = typeDescription + } + + public var typeDescription: String + } + + public func tryGet<T>() throws -> T where Value == Optional<T> { + guard let value = get() else { + throw MissingValueError(typeDescription: "\(Self.self)") + } + return value + } +} + extension Stored { public static func unimplemented(placeholder: Value) -> Stored<Value> { Stored<Value>(