Remove CoreData.Instance Entity Classes (IOS-271)

This commit is contained in:
Marcus Kida 2024-06-25 11:36:04 +02:00
parent 3db058800b
commit 972426fa50
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
8 changed files with 47 additions and 387 deletions

View File

@ -1,80 +0,0 @@
//
// Instance.swift
// CoreDataStack
//
// Created by Cirno MainasuK on 2021-10-9.
//
import Foundation
import CoreData
@available(*, deprecated, message: "Please use `MastodonAuthentication.InstanceConfiguration` instead.")
public final class Instance: NSManagedObject {
@NSManaged public var domain: String
@NSManaged public var version: String?
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var configurationRaw: Data?
@NSManaged public private(set) var configurationV2Raw: Data?
// MARK: one-to-many relationships
@NSManaged public var authentications: Set<MastodonAuthenticationLegacy>
}
extension Instance {
public override func awakeFromInsert() {
super.awakeFromInsert()
let now = Date()
setPrimitiveValue(now, forKey: #keyPath(Instance.createdAt))
setPrimitiveValue(now, forKey: #keyPath(Instance.updatedAt))
}
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
property: Property
) -> Instance {
let instance: Instance = context.insertObject()
instance.domain = property.domain
instance.version = property.version
return instance
}
public func update(configurationRaw: Data?) {
self.configurationRaw = configurationRaw
}
public func update(configurationV2Raw: Data?) {
self.configurationV2Raw = configurationV2Raw
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension Instance {
public struct Property {
public let domain: String
public let version: String?
public init(domain: String, version: String?) {
self.domain = domain
self.version = version
}
}
}
extension Instance: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \Instance.createdAt, ascending: false)]
}
}
extension Instance {
public static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(Instance.domain), domain)
}
}

View File

@ -29,11 +29,7 @@ final public class MastodonAuthenticationLegacy: NSManagedObject {
@NSManaged public private(set) var activedAt: Date
// one-to-one relationship
@NSManaged public private(set) var user: MastodonUser
// many-to-one relationship
@NSManaged public private(set) var instance: Instance?
@NSManaged public private(set) var user: MastodonUser
}
extension MastodonAuthenticationLegacy {
@ -101,12 +97,6 @@ extension MastodonAuthenticationLegacy {
}
}
public func update(instance: Instance) {
if self.instance != instance {
self.instance = instance
}
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}

View File

@ -23,6 +23,7 @@ public class AuthenticationServiceProvider: ObservableObject {
}
}
@MainActor
@discardableResult
func updating(instanceV1 instance: Mastodon.Entity.Instance, for domain: String) -> Self {
authentications = authentications.map { authentication in
@ -32,6 +33,7 @@ public class AuthenticationServiceProvider: ObservableObject {
return self
}
@MainActor
@discardableResult
func updating(instanceV2 instance: Mastodon.Entity.V2.Instance, for domain: String) -> Self {
authentications = authentications.map { authentication in
@ -41,6 +43,7 @@ public class AuthenticationServiceProvider: ObservableObject {
return self
}
@MainActor
@discardableResult
func updating(translationLanguages: TranslationLanguages, for domain: String) -> Self {
authentications = authentications.map { authentication in

View File

@ -1,56 +0,0 @@
//
// Instance.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-10-9.
//
import UIKit
import CoreDataStack
import MastodonSDK
extension Instance {
public var configuration: Mastodon.Entity.Instance.Configuration? {
guard let configurationRaw = configurationRaw else { return nil }
guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.Instance.Configuration.self, from: configurationRaw) else {
return nil
}
return configuration
}
static func encode(configuration: Mastodon.Entity.Instance.Configuration) -> Data? {
return try? JSONEncoder().encode(configuration)
}
}
extension Instance {
public var configurationV2: Mastodon.Entity.V2.Instance.Configuration? {
guard
let configurationRaw = configurationV2Raw,
let configuration = try? JSONDecoder().decode(
Mastodon.Entity.V2.Instance.Configuration.self,
from: configurationRaw
)
else {
return nil
}
return configuration
}
static func encodeV2(configuration: Mastodon.Entity.V2.Instance.Configuration) -> Data? {
return try? JSONEncoder().encode(configuration)
}
}
extension String {
public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool {
guard
let majorVersionString = split(separator: ".").first,
let majorVersionInt = Int(majorVersionString)
else { return false }
return majorVersionInt >= comparedVersion
}
}

View File

@ -133,10 +133,12 @@ public struct MastodonAuthentication: Codable, Hashable, UserIdentifier {
MastodonUserIdentifier(domain: domain, userID: userID)
}
@MainActor
func updating(instanceV1 instance: Mastodon.Entity.Instance) -> Self {
return copy(instanceConfiguration: .v1(instance))
}
@MainActor
func updating(instanceV2 instance: Mastodon.Entity.V2.Instance) -> Self {
guard
let instanceConfiguration = self.instanceConfiguration,
@ -147,6 +149,7 @@ public struct MastodonAuthentication: Codable, Hashable, UserIdentifier {
return copy(instanceConfiguration: .v2(instance, translationLanguages))
}
@MainActor
func updating(translationLanguages: TranslationLanguages) -> Self {
switch self.instanceConfiguration {
case .v1(let instance):

View File

@ -1,75 +0,0 @@
//
// APIService+CoreData+Instance.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-10-9.
//
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService.CoreData {
static func createOrMergeInstance(
into managedObjectContext: NSManagedObjectContext,
domain: String,
entity: Mastodon.Entity.Instance,
networkDate: Date
) -> (instance: Instance, isCreated: Bool) {
// fetch old mastodon user
let old: Instance? = {
let request = Instance.sortedFetchRequest
request.predicate = Instance.predicate(domain: domain)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()
if let old = old {
// merge old
APIService.CoreData.merge(
instance: old,
entity: entity,
domain: domain,
networkDate: networkDate
)
return (old, false)
} else {
let instance = Instance.insert(
into: managedObjectContext,
property: Instance.Property(domain: domain, version: entity.version)
)
let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
instance.update(configurationRaw: configurationRaw)
return (instance, true)
}
}
}
extension APIService.CoreData {
static func merge(
instance: Instance,
entity: Mastodon.Entity.Instance,
domain: String,
networkDate: Date
) {
guard networkDate > instance.updatedAt else { return }
let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
instance.update(configurationRaw: configurationRaw)
instance.version = entity.version
instance.didUpdate(at: networkDate)
}
}

View File

@ -1,77 +0,0 @@
import Foundation
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService.CoreData {
public struct PersistContext {
public let domain: String
public let entity: Mastodon.Entity.V2.Instance
public let networkDate: Date
public init(
domain: String,
entity: Mastodon.Entity.V2.Instance,
networkDate: Date
) {
self.domain = domain
self.entity = entity
self.networkDate = networkDate
}
}
static func createOrMergeInstance(
in managedObjectContext: NSManagedObjectContext,
context: PersistContext
) -> (instance: Instance, isCreated: Bool) {
// fetch old mastodon user
let old: Instance? = {
let request = Instance.sortedFetchRequest
request.predicate = Instance.predicate(domain: context.domain)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
return try managedObjectContext.fetch(request).first
} catch {
assertionFailure(error.localizedDescription)
return nil
}
}()
if let old = old {
APIService.CoreData.merge(
instance: old,
context: context
)
return (old, false)
} else {
let instance = Instance.insert(
into: managedObjectContext,
property: Instance.Property(domain: context.domain, version: context.entity.version)
)
let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
instance.update(configurationV2Raw: configurationRaw)
return (instance, true)
}
}
}
extension APIService.CoreData {
static func merge(
instance: Instance,
context: PersistContext
) {
guard context.networkDate > instance.updatedAt else { return }
let configurationRaw = context.entity.configuration.flatMap { Instance.encodeV2(configuration: $0) }
instance.update(configurationV2Raw: configurationRaw)
instance.version = context.entity.version
instance.didUpdate(at: context.networkDate)
}
}

View File

@ -34,9 +34,9 @@ public final class InstanceService {
.receive(on: DispatchQueue.main)
.compactMap { $0.first?.domain }
.removeDuplicates() // prevent infinity loop
.sink { [weak self] domain in
guard let self = self else { return }
self.updateInstance(domain: domain)
.asyncMap { [weak self] in await self?.updateInstance(domain: $0) }
.sink { _ in
// no-op
}
.store(in: &disposeBag)
}
@ -44,102 +44,54 @@ public final class InstanceService {
}
extension InstanceService {
func updateInstance(domain: String) {
@MainActor
func updateInstance(domain: String) async {
guard let apiService else { return }
apiService.instance(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first)
.flatMap { [unowned self] response -> AnyPublisher<Void, Error> in
if response.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true {
return apiService.instanceV2(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first)
.flatMap { return self.updateInstanceV2(domain: domain, response: $0) }
.eraseToAnyPublisher()
} else {
return self.updateInstance(domain: domain, response: response)
}
let response = try? await apiService.instance(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first)
.singleOutput()
if response?.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true {
guard let instanceV2 = try? await apiService.instanceV2(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first).singleOutput() else {
return
}
.sink { [weak self] completion in
switch completion {
case .finished:
self?.updateTranslationLanguages(domain: domain)
case .failure:
break
}
} receiveValue: { [weak self] response in
guard let _ = self else { return }
// do nothing
self.updateInstanceV2(domain: domain, response: instanceV2)
if let translationResponse = try? await apiService.translationLanguages(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first).singleOutput() {
updateTranslationLanguages(domain: domain, response: translationResponse)
}
.store(in: &disposeBag)
} else if let response {
self.updateInstance(domain: domain, response: response)
}
}
func updateTranslationLanguages(domain: String) {
apiService?.translationLanguages(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first)
.sink(receiveCompletion: { completion in
// no-op
}, receiveValue: { [weak self] response in
self?.updateTranslationLanguages(domain: domain, response: response)
})
.store(in: &disposeBag)
}
@MainActor
private func updateTranslationLanguages(domain: String, response: Mastodon.Response.Content<TranslationLanguages>) {
AuthenticationServiceProvider.shared
.updating(translationLanguages: response.value, for: domain)
}
private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
let instanceEntity = response.value
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: instanceEntity,
networkDate: response.networkDate
)
// update instance
AuthenticationServiceProvider.shared
.updating(instanceV1: instanceEntity, for: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
@MainActor
private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) {
AuthenticationServiceProvider.shared
.updating(instanceV1: response.value, for: domain)
}
private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
let instanceEntity = response.value
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
in: managedObjectContext,
context: .init(
domain: domain,
entity: instanceEntity,
networkDate: response.networkDate
)
)
// update instance
@MainActor
private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) {
AuthenticationServiceProvider.shared
.updating(instanceV2: instanceEntity, for: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
.updating(instanceV2: response.value, for: domain)
}
}
public extension String {
func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool {
guard
let majorVersionString = split(separator: ".").first,
let majorVersionInt = Int(majorVersionString)
else { return false }
return majorVersionInt >= comparedVersion
}
}