From c438e05476cf9c861a542aed96524af4b6ed2fed Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 21 Oct 2022 11:42:32 +0200 Subject: [PATCH] Migrate RegisterFeature to ReducerProtocol --- .../RegisterFeature/RegisterComponent.swift | 104 ++++++++++++++ .../RegisterFeature/RegisterFeature.swift | 127 ------------------ .../RegisterFeature/RegisterView.swift | 17 ++- ...sts.swift => RegisterComponentTests.swift} | 62 ++++----- 4 files changed, 141 insertions(+), 169 deletions(-) create mode 100644 Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift delete mode 100644 Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift rename Examples/xx-messenger/Tests/RegisterFeatureTests/{RegisterFeatureTests.swift => RegisterComponentTests.swift} (76%) diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift new file mode 100644 index 00000000..062080af --- /dev/null +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterComponent.swift @@ -0,0 +1,104 @@ +import AppCore +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct RegisterComponent: ReducerProtocol { + public struct State: Equatable { + public enum Error: Swift.Error, Equatable { + case usernameMismatch(registering: String, registered: String?) + } + + public enum Field: String, Hashable { + case username + } + + public init( + focusedField: Field? = nil, + username: String = "", + isRegistering: Bool = false, + failure: String? = nil + ) { + self.focusedField = focusedField + self.username = username + self.isRegistering = isRegistering + self.failure = failure + } + + @BindableState public var focusedField: Field? + @BindableState public var username: String + public var isRegistering: Bool + public var failure: String? + } + + public enum Action: Equatable, BindableAction { + case registerTapped + case failed(String) + case finished + case binding(BindingAction<State>) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.now) var now: () -> Date + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(_): + return .none + + case .registerTapped: + state.focusedField = nil + state.isRegistering = true + state.failure = nil + return .future { [username = state.username] fulfill in + do { + let db = try db() + try messenger.register(username: username) + let contact = try messenger.myContact() + let facts = try contact.getFacts() + try db.saveContact(Contact( + id: try contact.getId(), + marshaled: contact.data, + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, + createdAt: now() + )) + guard facts.get(.username)?.value == username else { + throw State.Error.usernameMismatch( + registering: username, + registered: facts.get(.username)?.value + ) + } + fulfill(.success(.finished)) + } + catch { + fulfill(.success(.failed(error.localizedDescription))) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .failed(let failure): + state.isRegistering = false + state.failure = failure + return .none + + case .finished: + state.isRegistering = false + return .none + } + } + } +} diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift deleted file mode 100644 index f8fdabef..00000000 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ /dev/null @@ -1,127 +0,0 @@ -import AppCore -import ComposableArchitecture -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient -import XXModels - -public struct RegisterState: Equatable { - public enum Error: Swift.Error, Equatable { - case usernameMismatch(registering: String, registered: String?) - } - - public enum Field: String, Hashable { - case username - } - - public init( - focusedField: Field? = nil, - username: String = "", - isRegistering: Bool = false, - failure: String? = nil - ) { - self.focusedField = focusedField - self.username = username - self.isRegistering = isRegistering - self.failure = failure - } - - @BindableState public var focusedField: Field? - @BindableState public var username: String - public var isRegistering: Bool - public var failure: String? -} - -public enum RegisterAction: Equatable, BindableAction { - case registerTapped - case failed(String) - case finished - case binding(BindingAction<RegisterState>) -} - -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> -} - -#if DEBUG -extension RegisterEnvironment { - public static let unimplemented = RegisterEnvironment( - messenger: .unimplemented, - db: .unimplemented, - now: XCTUnimplemented("\(Self.self).now"), - mainQueue: .unimplemented, - bgQueue: .unimplemented - ) -} -#endif - -public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvironment> -{ state, action, env in - switch action { - case .binding(_): - return .none - - case .registerTapped: - state.focusedField = nil - state.isRegistering = true - state.failure = nil - return .future { [username = state.username] fulfill in - do { - let db = try env.db() - try env.messenger.register(username: username) - let contact = try env.messenger.myContact() - let facts = try contact.getFacts() - try db.saveContact(Contact( - id: try contact.getId(), - marshaled: contact.data, - username: facts.get(.username)?.value, - email: facts.get(.email)?.value, - phone: facts.get(.phone)?.value, - createdAt: env.now() - )) - guard facts.get(.username)?.value == username else { - throw RegisterState.Error.usernameMismatch( - registering: username, - registered: facts.get(.username)?.value - ) - } - fulfill(.success(.finished)) - } - catch { - fulfill(.success(.failed(error.localizedDescription))) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .failed(let failure): - state.isRegistering = false - state.failure = failure - return .none - - case .finished: - state.isRegistering = false - return .none - } -} -.binding() diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift index 4b168c27..a41e16ab 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterView.swift @@ -2,22 +2,22 @@ import ComposableArchitecture import SwiftUI public struct RegisterView: View { - public init(store: Store<RegisterState, RegisterAction>) { + public init(store: StoreOf<RegisterComponent>) { self.store = store } - let store: Store<RegisterState, RegisterAction> - @FocusState var focusedField: RegisterState.Field? + let store: StoreOf<RegisterComponent> + @FocusState var focusedField: RegisterComponent.State.Field? struct ViewState: Equatable { - init(_ state: RegisterState) { + init(_ state: RegisterComponent.State) { focusedField = state.focusedField username = state.username isRegistering = state.isRegistering failure = state.failure } - var focusedField: RegisterState.Field? + var focusedField: RegisterComponent.State.Field? var username: String var isRegistering: Bool var failure: String? @@ -31,7 +31,7 @@ public struct RegisterView: View { TextField( text: viewStore.binding( get: \.username, - send: { RegisterAction.set(\.$username, $0) } + send: { RegisterComponent.Action.set(\.$username, $0) } ), prompt: Text("Enter username"), label: { Text("Username") } @@ -78,9 +78,8 @@ public struct RegisterView: View { public struct RegisterView_Previews: PreviewProvider { public static var previews: some View { RegisterView(store: Store( - initialState: RegisterState(), - reducer: .empty, - environment: () + initialState: RegisterComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift similarity index 76% rename from Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift rename to Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift index e0ffc5ef..0be4647e 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterComponentTests.swift @@ -6,7 +6,7 @@ import XXMessengerClient import XXModels @testable import RegisterFeature -final class RegisterFeatureTests: XCTestCase { +final class RegisterComponentTests: XCTestCase { func testRegister() throws { let now = Date() let username = "registering-username" @@ -26,24 +26,23 @@ final class RegisterFeatureTests: XCTestCase { var dbDidSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) - store.environment.now = { now } - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.register.run = { username in + store.dependencies.app.now = { now } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.messenger.myContact.run = { includeFacts in didGetMyContact.append(includeFacts) var contact = XXClient.Contact.unimplemented(myContactData) contact.getIdFromContact.run = { _ in myContactId } contact.getFactsFromContact.run = { _ in myContactFacts } return contact } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) @@ -87,17 +86,16 @@ final class RegisterFeatureTests: XCTestCase { let error = Error() let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) 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.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.dbManager.getDB.run = { throw error } store.send(.registerTapped) { $0.isRegistering = true @@ -117,18 +115,17 @@ final class RegisterFeatureTests: XCTestCase { let error = Error() let store = TestStore( - initialState: RegisterState(), - reducer: registerReducer, - environment: .unimplemented + initialState: RegisterComponent.State(), + reducer: RegisterComponent() ) let mainQueue = DispatchQueue.test let bgQueue = DispatchQueue.test - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.db.run = { .unimplemented } - store.environment.messenger.register.run = { _ in throw error } + store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler() + store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler() + store.dependencies.app.dbManager.getDB.run = { .unimplemented } + store.dependencies.app.messenger.register.run = { _ in throw error } store.send(.registerTapped) { $0.isRegistering = true @@ -162,26 +159,25 @@ final class RegisterFeatureTests: XCTestCase { var dbDidSaveContact: [XXModels.Contact] = [] let store = TestStore( - initialState: RegisterState( + initialState: RegisterComponent.State( username: username ), - reducer: registerReducer, - environment: .unimplemented + reducer: RegisterComponent() ) - store.environment.now = { now } - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messenger.register.run = { username in + store.dependencies.app.now = { now } + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.myContact.run = { includeFacts in + store.dependencies.app.messenger.myContact.run = { includeFacts in didGetMyContact.append(includeFacts) var contact = XXClient.Contact.unimplemented(myContactData) contact.getIdFromContact.run = { _ in myContactId } contact.getFactsFromContact.run = { _ in myContactFacts } return contact } - store.environment.db.run = { + store.dependencies.app.dbManager.getDB.run = { var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) @@ -207,7 +203,7 @@ final class RegisterFeatureTests: XCTestCase { ) ]) - let failure = RegisterState.Error.usernameMismatch( + let failure = RegisterComponent.State.Error.usernameMismatch( registering: username, registered: myContactUsername ) -- GitLab