Refactoring

This commit is contained in:
Justin Mazzocchi 2020-08-02 00:02:03 -07:00
parent 949a2a8cd1
commit 24dd407caa
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
6 changed files with 82 additions and 76 deletions

View file

@ -56,7 +56,16 @@ extension IdentityDatabase {
} }
extension Identity { extension Identity {
static let development = try! IdentityDatabase.development.identity(id: devIdentityID)! static let development: Identity = {
var identity: Identity?
IdentityDatabase.development.identityObservation(id: devIdentityID)
.assertNoFailure()
.sink(receiveValue: { identity = $0 })
.store(in: &cancellables)
return identity!
}()
} }
extension SceneViewModel { extension SceneViewModel {

View file

@ -4,12 +4,12 @@ import Foundation
import Combine import Combine
extension Publisher { extension Publisher {
func assignErrorsToAlertItem<Root>( func assignErrorsToAlertItem<Root: AnyObject>(
to keyPath: ReferenceWritableKeyPath<Root, AlertItem?>, to keyPath: ReferenceWritableKeyPath<Root, AlertItem?>,
on object: Root) -> AnyPublisher<Output, Never> { on object: Root) -> AnyPublisher<Output, Never> {
self.catch { error -> AnyPublisher<Output, Never> in self.catch { [weak object] error -> AnyPublisher<Output, Never> in
DispatchQueue.main.async { DispatchQueue.main.async {
object[keyPath: keyPath] = AlertItem(error: error) object?[keyPath: keyPath] = AlertItem(error: error)
} }
return Empty().eraseToAnyPublisher() return Empty().eraseToAnyPublisher()

View file

@ -26,16 +26,12 @@ struct IdentityDatabase {
} }
extension IdentityDatabase { extension IdentityDatabase {
func createIdentity(id: String, url: URL) -> AnyPublisher<Identity, Error> { func createIdentity(id: String, url: URL) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher { databaseQueue.writePublisher(updates: StoredIdentity(id: id, url: url, instanceURI: nil).save)
try StoredIdentity(id: id, url: url, instanceURI: nil).save($0) .eraseToAnyPublisher()
return Identity(id: id, url: url, instance: nil, account: nil)
}
.eraseToAnyPublisher()
} }
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> { func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher { databaseQueue.writePublisher {
try Identity.Instance( try Identity.Instance(
uri: instance.uri, uri: instance.uri,
@ -46,15 +42,13 @@ extension IdentityDatabase {
try StoredIdentity try StoredIdentity
.filter(Column("id") == identityID) .filter(Column("id") == identityID)
.updateAll($0, Column("instanceURI").set(to: instance.uri)) .updateAll($0, Column("instanceURI").set(to: instance.uri))
return try Self.fetchIdentity(id: identityID, db: $0)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func updateAccount(_ account: Account, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> { func updateAccount(_ account: Account, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher { databaseQueue.writePublisher(
try Identity.Account( updates: Identity.Account(
id: account.id, id: account.id,
identityID: identityID, identityID: identityID,
username: account.username, username: account.username,
@ -63,15 +57,26 @@ extension IdentityDatabase {
avatarStatic: account.avatarStatic, avatarStatic: account.avatarStatic,
header: account.header, header: account.header,
headerStatic: account.headerStatic) headerStatic: account.headerStatic)
.save($0) .save)
.eraseToAnyPublisher()
return try Self.fetchIdentity(id: identityID, db: $0)
}
.eraseToAnyPublisher()
} }
func identity(id: String) throws -> Identity? { func identityObservation(id: String) -> AnyPublisher<Identity?, Error> {
try databaseQueue.read { try Self.fetchIdentity(id: id, db: $0) } ValueObservation.tracking(
StoredIdentity
.filter(Column("id") == id)
.including(optional: StoredIdentity.instance)
.including(optional: StoredIdentity.account)
.asRequest(of: IdentityResult.self)
.fetchOne)
.removeDuplicates()
.publisher(in: databaseQueue, scheduling: .immediate)
.map {
guard let result = $0 else { return nil }
return Identity(result: result)
}
.eraseToAnyPublisher()
} }
} }
@ -112,19 +117,6 @@ private extension IdentityDatabase {
try migrator.migrate(writer) try migrator.migrate(writer)
} }
private static func fetchIdentity(id: String, db: Database) throws -> Identity? {
if let result = try StoredIdentity
.filter(Column("id") == id)
.including(optional: StoredIdentity.instance)
.including(optional: StoredIdentity.account)
.asRequest(of: IdentityResult.self)
.fetchOne(db) {
return Identity(result: result)
}
return nil
}
} }
private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord, PersistableRecord { private struct StoredIdentity: Codable, Hashable, TableRecord, FetchableRecord, PersistableRecord {

View file

@ -8,15 +8,13 @@ class AddIdentityViewModel: ObservableObject {
@Published var urlFieldText = "" @Published var urlFieldText = ""
@Published var alertItem: AlertItem? @Published var alertItem: AlertItem?
@Published private(set) var loading = false @Published private(set) var loading = false
private(set) var addedIdentity: AnyPublisher<Identity, Never> @Published private(set) var addedIdentityID: String?
private let networkClient: HTTPClient private let networkClient: HTTPClient
private let identityDatabase: IdentityDatabase private let identityDatabase: IdentityDatabase
private let secrets: Secrets private let secrets: Secrets
private let webAuthenticationSessionType: WebAuthenticationSessionType.Type private let webAuthenticationSessionType: WebAuthenticationSessionType.Type
private let webAuthenticationSessionContextProvider = WebAuthenticationSessionContextProvider() private let webAuthenticationSessionContextProvider = WebAuthenticationSessionContextProvider()
private let addedIdentityInput = PassthroughSubject<Identity, Never>()
private var cancellables = Set<AnyCancellable>()
init( init(
networkClient: HTTPClient, networkClient: HTTPClient,
@ -27,7 +25,6 @@ class AddIdentityViewModel: ObservableObject {
self.identityDatabase = identityDatabase self.identityDatabase = identityDatabase
self.secrets = secrets self.secrets = secrets
self.webAuthenticationSessionType = webAuthenticationSessionType self.webAuthenticationSessionType = webAuthenticationSessionType
addedIdentity = addedIdentityInput.eraseToAnyPublisher()
} }
func goTapped() { func goTapped() {
@ -65,11 +62,12 @@ class AddIdentityViewModel: ObservableObject {
identityDatabase: identityDatabase, identityDatabase: identityDatabase,
secrets: secrets) secrets: secrets)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.receive(on: RunLoop.main)
.handleEvents( .handleEvents(
receiveSubscription: { [weak self] _ in self?.loading = true }, receiveSubscription: { [weak self] _ in self?.loading = true },
receiveCompletion: { [weak self] _ in self?.loading = false }) receiveCompletion: { [weak self] _ in self?.loading = false })
.sink(receiveValue: addedIdentityInput.send) .map { $0 as String? }
.store(in: &cancellables) .assign(to: &$addedIdentityID)
} }
} }
@ -196,13 +194,14 @@ private extension Publisher where Output == AccessToken {
id: String, id: String,
instanceURL: URL, instanceURL: URL,
identityDatabase: IdentityDatabase, identityDatabase: IdentityDatabase,
secrets: Secrets) -> AnyPublisher<Identity, Error> { secrets: Secrets) -> AnyPublisher<String, Error> {
tryMap { accessToken -> (String, URL) in tryMap { accessToken -> (String, URL) in
try secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id) try secrets.set(accessToken.accessToken, forItem: .accessToken, forIdentityID: id)
return (id, instanceURL) return (id, instanceURL)
} }
.flatMap(identityDatabase.createIdentity) .flatMap(identityDatabase.createIdentity)
.map { id }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

View file

@ -4,21 +4,7 @@ import Foundation
import Combine import Combine
class SceneViewModel: ObservableObject { class SceneViewModel: ObservableObject {
@Published private(set) var identity: Identity? { @Published private(set) var identity: Identity?
didSet {
if let identity = identity {
recentIdentityID = identity.id
networkClient.instanceURL = identity.url
do {
networkClient.accessToken = try secrets.item(.accessToken, forIdentityID: identity.id)
} catch {
alertItem = AlertItem(error: error)
}
}
}
}
@Published var alertItem: AlertItem? @Published var alertItem: AlertItem?
@Published var presentingSettings = false @Published var presentingSettings = false
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
@ -39,8 +25,7 @@ class SceneViewModel: ObservableObject {
self.userDefaults = userDefaults self.userDefaults = userDefaults
if let recentIdentityID = recentIdentityID { if let recentIdentityID = recentIdentityID {
identity = try? identityDatabase.identity(id: recentIdentityID) changeIdentity(id: recentIdentityID)
refreshIdentity()
} }
} }
} }
@ -54,7 +39,7 @@ extension SceneViewModel {
.map { ($0, identity.id) } .map { ($0, identity.id) }
.flatMap(identityDatabase.updateAccount) .flatMap(identityDatabase.updateAccount)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: \.identity, on: self) .sink(receiveValue: {})
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -62,7 +47,7 @@ extension SceneViewModel {
.map { ($0, identity.id) } .map { ($0, identity.id) }
.flatMap(identityDatabase.updateInstance) .flatMap(identityDatabase.updateInstance)
.assignErrorsToAlertItem(to: \.alertItem, on: self) .assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: \.identity, on: self) .sink(receiveValue: {})
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -72,8 +57,9 @@ extension SceneViewModel {
identityDatabase: identityDatabase, identityDatabase: identityDatabase,
secrets: secrets) secrets: secrets)
addAccountViewModel.addedIdentity addAccountViewModel.$addedIdentityID
.sink(receiveValue: addIdentity(_:)) .compactMap { $0 }
.sink(receiveValue: changeIdentity(id:))
.store(in: &cancellables) .store(in: &cancellables)
return addAccountViewModel return addAccountViewModel
@ -88,8 +74,23 @@ private extension SceneViewModel {
set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) } set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) }
} }
private func addIdentity(_ identity: Identity) { private func changeIdentity(id: String) {
self.identity = identity identityDatabase.identityObservation(id: id)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.handleEvents(receiveOutput: { [weak self] in
guard let self = self, let identity = $0 else { return }
self.recentIdentityID = identity.id
self.networkClient.instanceURL = identity.url
do {
self.networkClient.accessToken = try self.secrets.item(.accessToken, forIdentityID: identity.id)
} catch {
self.alertItem = AlertItem(error: error)
}
})
.assign(to: &$identity)
refreshIdentity() refreshIdentity()
} }
} }

View file

@ -23,23 +23,26 @@ class AddIdentityViewModelTests: XCTestCase {
identityDatabase: identityDatabase, identityDatabase: identityDatabase,
secrets: secrets, secrets: secrets,
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self) webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
let recorder = sut.addedIdentity.record() let addedIDRecorder = sut.$addedIdentityID.record()
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
sut.urlFieldText = "https://mastodon.social" sut.urlFieldText = "https://mastodon.social"
sut.goTapped() sut.goTapped()
let addedIdentity = try wait(for: recorder.next(), timeout: 1) let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record()
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
XCTAssertEqual(try identityDatabase.identity(id: addedIdentity.id), addedIdentity) XCTAssertEqual(addedIdentity.id, addedIdentityID)
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!) XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
XCTAssertEqual( XCTAssertEqual(
try secrets.item(.clientID, forIdentityID: addedIdentity.id) as String?, try secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_ID_STUB_VALUE") "AUTHORIZATION_CLIENT_ID_STUB_VALUE")
XCTAssertEqual( XCTAssertEqual(
try secrets.item(.clientSecret, forIdentityID: addedIdentity.id) as String?, try secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE") "AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
XCTAssertEqual( XCTAssertEqual(
try secrets.item(.accessToken, forIdentityID: addedIdentity.id) as String?, try secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?,
"ACCESS_TOKEN_STUB_VALUE") "ACCESS_TOKEN_STUB_VALUE")
} }
@ -49,14 +52,16 @@ class AddIdentityViewModelTests: XCTestCase {
identityDatabase: identityDatabase, identityDatabase: identityDatabase,
secrets: secrets, secrets: secrets,
webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self) webAuthenticationSessionType: SuccessfulStubbingWebAuthenticationSession.self)
let recorder = sut.addedIdentity.record() let addedIDRecorder = sut.$addedIdentityID.record()
_ = try wait(for: addedIDRecorder.next(), timeout: 1)
sut.urlFieldText = "mastodon.social" sut.urlFieldText = "mastodon.social"
sut.goTapped() sut.goTapped()
let addedIdentity = try wait(for: recorder.next(), timeout: 1) let addedIdentityID = try wait(for: addedIDRecorder.next(), timeout: 1)!
let identityRecorder = identityDatabase.identityObservation(id: addedIdentityID).record()
let addedIdentity = try wait(for: identityRecorder.next(), timeout: 1)!
XCTAssertEqual(try identityDatabase.identity(id: addedIdentity.id), addedIdentity)
XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!) XCTAssertEqual(addedIdentity.url, URL(string: "https://mastodon.social")!)
} }