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 {
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 {

View File

@ -4,12 +4,12 @@ import Foundation
import Combine
extension Publisher {
func assignErrorsToAlertItem<Root>(
func assignErrorsToAlertItem<Root: AnyObject>(
to keyPath: ReferenceWritableKeyPath<Root, AlertItem?>,
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 {
object[keyPath: keyPath] = AlertItem(error: error)
object?[keyPath: keyPath] = AlertItem(error: error)
}
return Empty().eraseToAnyPublisher()

View File

@ -26,16 +26,12 @@ struct IdentityDatabase {
}
extension IdentityDatabase {
func createIdentity(id: String, url: URL) -> AnyPublisher<Identity, Error> {
databaseQueue.writePublisher {
try StoredIdentity(id: id, url: url, instanceURI: nil).save($0)
return Identity(id: id, url: url, instance: nil, account: nil)
}
.eraseToAnyPublisher()
func createIdentity(id: String, url: URL) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher(updates: StoredIdentity(id: id, url: url, instanceURI: nil).save)
.eraseToAnyPublisher()
}
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> {
func updateInstance(_ instance: Instance, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher {
try Identity.Instance(
uri: instance.uri,
@ -46,15 +42,13 @@ extension IdentityDatabase {
try StoredIdentity
.filter(Column("id") == identityID)
.updateAll($0, Column("instanceURI").set(to: instance.uri))
return try Self.fetchIdentity(id: identityID, db: $0)
}
.eraseToAnyPublisher()
}
func updateAccount(_ account: Account, forIdentityID identityID: String) -> AnyPublisher<Identity?, Error> {
databaseQueue.writePublisher {
try Identity.Account(
func updateAccount(_ account: Account, forIdentityID identityID: String) -> AnyPublisher<Void, Error> {
databaseQueue.writePublisher(
updates: Identity.Account(
id: account.id,
identityID: identityID,
username: account.username,
@ -63,15 +57,26 @@ extension IdentityDatabase {
avatarStatic: account.avatarStatic,
header: account.header,
headerStatic: account.headerStatic)
.save($0)
return try Self.fetchIdentity(id: identityID, db: $0)
}
.eraseToAnyPublisher()
.save)
.eraseToAnyPublisher()
}
func identity(id: String) throws -> Identity? {
try databaseQueue.read { try Self.fetchIdentity(id: id, db: $0) }
func identityObservation(id: String) -> AnyPublisher<Identity?, Error> {
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)
}
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 {

View File

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

View File

@ -4,21 +4,7 @@ import Foundation
import Combine
class SceneViewModel: ObservableObject {
@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 private(set) var identity: Identity?
@Published var alertItem: AlertItem?
@Published var presentingSettings = false
var selectedTopLevelNavigation: TopLevelNavigation? = .timelines
@ -39,8 +25,7 @@ class SceneViewModel: ObservableObject {
self.userDefaults = userDefaults
if let recentIdentityID = recentIdentityID {
identity = try? identityDatabase.identity(id: recentIdentityID)
refreshIdentity()
changeIdentity(id: recentIdentityID)
}
}
}
@ -54,7 +39,7 @@ extension SceneViewModel {
.map { ($0, identity.id) }
.flatMap(identityDatabase.updateAccount)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: \.identity, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
}
@ -62,7 +47,7 @@ extension SceneViewModel {
.map { ($0, identity.id) }
.flatMap(identityDatabase.updateInstance)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.assign(to: \.identity, on: self)
.sink(receiveValue: {})
.store(in: &cancellables)
}
@ -72,8 +57,9 @@ extension SceneViewModel {
identityDatabase: identityDatabase,
secrets: secrets)
addAccountViewModel.addedIdentity
.sink(receiveValue: addIdentity(_:))
addAccountViewModel.$addedIdentityID
.compactMap { $0 }
.sink(receiveValue: changeIdentity(id:))
.store(in: &cancellables)
return addAccountViewModel
@ -88,8 +74,23 @@ private extension SceneViewModel {
set { userDefaults.set(newValue, forKey: Self.recentIdentityIDKey) }
}
private func addIdentity(_ identity: Identity) {
self.identity = identity
private func changeIdentity(id: String) {
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()
}
}

View File

@ -23,23 +23,26 @@ class AddIdentityViewModelTests: XCTestCase {
identityDatabase: identityDatabase,
secrets: secrets,
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.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(
try secrets.item(.clientID, forIdentityID: addedIdentity.id) as String?,
try secrets.item(.clientID, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_ID_STUB_VALUE")
XCTAssertEqual(
try secrets.item(.clientSecret, forIdentityID: addedIdentity.id) as String?,
try secrets.item(.clientSecret, forIdentityID: addedIdentityID) as String?,
"AUTHORIZATION_CLIENT_SECRET_STUB_VALUE")
XCTAssertEqual(
try secrets.item(.accessToken, forIdentityID: addedIdentity.id) as String?,
try secrets.item(.accessToken, forIdentityID: addedIdentityID) as String?,
"ACCESS_TOKEN_STUB_VALUE")
}
@ -49,14 +52,16 @@ class AddIdentityViewModelTests: XCTestCase {
identityDatabase: identityDatabase,
secrets: secrets,
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.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")!)
}