From e115bfe9aae7ec3a1a0a664fde4ac53bb2bdcac3 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 16 Jan 2025 00:17:51 +0100 Subject: [PATCH] Reuse provider entities for more than VPN - Rename views - Decouple provider templates and configurations --- .../Domain/CDVPNPresetV3.swift | 2 +- .../Domain/CoreDataMapper.swift | 24 +- .../Domain/DomainMapper.swift | 16 +- ...> ProviderServerParameters+CoreData.swift} | 6 +- .../Strategy/CDAPIRepositoryV3.swift | 173 ++++++++++++++ .../Strategy/CDProviderRepositoryV3.swift | 179 ++++---------- .../CDVPNProviderServerRepositoryV3.swift | 96 -------- .../App/Sources/AppUIMain/AppUIMain.swift | 6 +- .../AppUIMain/Views/App/AddProfileMenu.swift | 4 +- .../AppUIMain/Views/App/AppCoordinator.swift | 26 +- .../Views/App/InstalledProfileView.swift | 2 +- .../Views/Diagnostics/ReportIssueButton.swift | 4 +- .../Extensions/OpenVPNModule+Extensions.swift | 23 +- .../WireGuardModule+Extensions.swift | 23 +- .../AppUIMain/Views/Modules/OpenVPNView.swift | 8 +- .../Views/Modules/WireGuardView.swift | 8 +- .../Views/Providers/APIContentModifier.swift | 226 ++++++++++++++++++ .../Providers/ProviderContentModifier.swift | 212 ++++------------ .../Providers/ProviderEntitySelector.swift | 51 ---- .../ProviderFiltersView+Model.swift} | 28 +-- .../ProviderFiltersView.swift} | 10 +- .../ProviderServerCoordinator+Module.swift | 57 +++++ .../ProviderServerCoordinator.swift} | 18 +- .../ProviderServerView.swift} | 62 ++--- .../iOS/ProviderServer+Container+iOS.swift} | 4 +- .../iOS/ProviderServer+Content+iOS.swift} | 30 +-- .../ProviderServer+Container+macOS.swift} | 4 +- .../macOS/ProviderServer+Content+macOS.swift} | 16 +- .../VPN/VPNProviderContentModifier.swift | 110 --------- .../Views/Profile/ActiveProfileView.swift | 10 +- .../UILibrary/Business/AppContext.swift | 18 +- .../Extensions/ModuleDraftEditing+UI.swift | 2 +- .../Extensions/View+Environment.swift | 2 +- .../UILibrary/L10n/PassepartoutKit+L10n.swift | 4 +- .../Previews/AppContext+Previews.swift | 14 +- ...ProviderServerCoordinatorSupporting.swift} | 13 +- .../Modules/OpenVPNView+Credentials.swift | 6 +- .../UI/RefreshInfrastructureButton.swift | 8 +- .../PassepartoutKit-Framework/Package.swift | 6 - .../Tests/TargetTests/Tests.swift | 10 - Packages/PassepartoutKit-Source | 2 +- .../App/Context/AppContext+Shared.swift | 10 +- .../App/Context/AppContext+Testing.swift | 8 +- .../App/Context/ProfileManager+Testing.swift | 14 +- 44 files changed, 763 insertions(+), 792 deletions(-) rename Packages/App/Sources/AppDataProviders/Domain/{VPNServerParameters+CoreData.swift => ProviderServerParameters+CoreData.swift} (96%) create mode 100644 Packages/App/Sources/AppDataProviders/Strategy/CDAPIRepositoryV3.swift delete mode 100644 Packages/App/Sources/AppDataProviders/Strategy/CDVPNProviderServerRepositoryV3.swift create mode 100644 Packages/App/Sources/AppUIMain/Views/Providers/APIContentModifier.swift delete mode 100644 Packages/App/Sources/AppUIMain/Views/Providers/ProviderEntitySelector.swift rename Packages/App/Sources/AppUIMain/Views/{VPN/VPNFiltersView+Model.swift => Providers/ProviderFiltersView+Model.swift} (84%) rename Packages/App/Sources/AppUIMain/Views/{VPN/VPNFiltersView.swift => Providers/ProviderFiltersView.swift} (94%) create mode 100644 Packages/App/Sources/AppUIMain/Views/Providers/ProviderServerCoordinator+Module.swift rename Packages/App/Sources/AppUIMain/Views/{VPN/VPNProviderServerCoordinator.swift => Providers/ProviderServerCoordinator.swift} (76%) rename Packages/App/Sources/AppUIMain/Views/{VPN/VPNProviderServerView.swift => Providers/ProviderServerView.swift} (74%) rename Packages/App/Sources/AppUIMain/Views/{VPN/iOS/VPNProviderServer+Container+iOS.swift => Providers/iOS/ProviderServer+Container+iOS.swift} (96%) rename Packages/App/Sources/AppUIMain/Views/{VPN/iOS/VPNProviderServer+Content+iOS.swift => Providers/iOS/ProviderServer+Content+iOS.swift} (84%) rename Packages/App/Sources/AppUIMain/Views/{VPN/macOS/VPNProviderServer+Container+macOS.swift => Providers/macOS/ProviderServer+Container+macOS.swift} (94%) rename Packages/App/Sources/AppUIMain/Views/{VPN/macOS/VPNProviderServer+Content+macOS.swift => Providers/macOS/ProviderServer+Content+macOS.swift} (88%) delete mode 100644 Packages/App/Sources/AppUIMain/Views/VPN/VPNProviderContentModifier.swift rename Packages/App/Sources/UILibrary/Strategy/{ProviderEntityViewProviding.swift => ProviderServerCoordinatorSupporting.swift} (78%) delete mode 100644 Packages/PassepartoutKit-Framework/Tests/TargetTests/Tests.swift diff --git a/Packages/App/Sources/AppDataProviders/Domain/CDVPNPresetV3.swift b/Packages/App/Sources/AppDataProviders/Domain/CDVPNPresetV3.swift index c9d18e06d..9fdcf9acd 100644 --- a/Packages/App/Sources/AppDataProviders/Domain/CDVPNPresetV3.swift +++ b/Packages/App/Sources/AppDataProviders/Domain/CDVPNPresetV3.swift @@ -32,9 +32,9 @@ final class CDVPNPresetV3: NSManagedObject { NSFetchRequest(entityName: "CDVPNPresetV3") } + @NSManaged var providerId: String? @NSManaged var presetId: String? @NSManaged var presetDescription: String? - @NSManaged var providerId: String? @NSManaged var endpoints: Data? @NSManaged var configurationId: String? @NSManaged var configuration: Data? diff --git a/Packages/App/Sources/AppDataProviders/Domain/CoreDataMapper.swift b/Packages/App/Sources/AppDataProviders/Domain/CoreDataMapper.swift index 7d17c9444..d02dd0ed2 100644 --- a/Packages/App/Sources/AppDataProviders/Domain/CoreDataMapper.swift +++ b/Packages/App/Sources/AppDataProviders/Domain/CoreDataMapper.swift @@ -42,7 +42,7 @@ struct CoreDataMapper { } @discardableResult - func cdServer(from server: VPNServer) throws -> CDVPNServerV3 { + func cdServer(from server: ProviderServer) throws -> CDVPNServerV3 { let entity = CDVPNServerV3(context: context) let encoder = JSONEncoder() entity.serverId = server.serverId @@ -50,27 +50,27 @@ struct CoreDataMapper { entity.ipAddresses = try server.ipAddresses.map { try encoder.encode($0) } - entity.providerId = server.provider.id.rawValue - entity.countryCode = server.provider.countryCode - entity.categoryName = server.provider.categoryName - entity.localizedCountry = server.provider.countryCode.localizedAsRegionCode - entity.otherCountryCodes = server.provider.otherCountryCodes?.joined(separator: ",") - entity.area = server.provider.area - entity.supportedConfigurationIds = server.provider.supportedConfigurationIdentifiers?.joined(separator: ",") - entity.supportedPresetIds = server.provider.supportedPresetIds?.joined(separator: ",") + entity.providerId = server.metadata.providerId.rawValue + entity.countryCode = server.metadata.countryCode + entity.categoryName = server.metadata.categoryName + entity.localizedCountry = server.metadata.countryCode.localizedAsRegionCode + entity.otherCountryCodes = server.metadata.otherCountryCodes?.joined(separator: ",") + entity.area = server.metadata.area + entity.supportedConfigurationIds = server.metadata.supportedConfigurationIdentifiers?.joined(separator: ",") + entity.supportedPresetIds = server.metadata.supportedPresetIds?.joined(separator: ",") return entity } @discardableResult - func cdPreset(from preset: AnyVPNPreset) throws -> CDVPNPresetV3 { + func cdPreset(from preset: AnyProviderPreset) throws -> CDVPNPresetV3 { let entity = CDVPNPresetV3(context: self.context) let encoder = JSONEncoder() - entity.presetId = preset.presetId entity.providerId = preset.providerId.rawValue + entity.presetId = preset.presetId entity.presetDescription = preset.description entity.endpoints = try encoder.encode(preset.endpoints) entity.configurationId = preset.configurationIdentifier - entity.configuration = preset.configuration + entity.configuration = preset.template return entity } } diff --git a/Packages/App/Sources/AppDataProviders/Domain/DomainMapper.swift b/Packages/App/Sources/AppDataProviders/Domain/DomainMapper.swift index 0c9634d1d..b8b4429f5 100644 --- a/Packages/App/Sources/AppDataProviders/Domain/DomainMapper.swift +++ b/Packages/App/Sources/AppDataProviders/Domain/DomainMapper.swift @@ -63,12 +63,12 @@ struct DomainMapper { } } - func preset(from entity: CDVPNPresetV3) throws -> AnyVPNPreset? { + func preset(from entity: CDVPNPresetV3) throws -> AnyProviderPreset? { guard let presetId = entity.presetId, let presetDescription = entity.presetDescription, let providerId = entity.providerId, let configurationId = entity.configurationId, - let configuration = entity.configuration else { + let template = entity.configuration else { return nil } @@ -77,17 +77,17 @@ struct DomainMapper { try decoder.decode([EndpointProtocol].self, from: $0) } ?? [] - return AnyVPNPreset( + return AnyProviderPreset( providerId: .init(rawValue: providerId), presetId: presetId, description: presetDescription, endpoints: endpoints, configurationIdentifier: configurationId, - configuration: configuration + template: template ) } - func server(from entity: CDVPNServerV3) throws -> VPNServer? { + func server(from entity: CDVPNServerV3) throws -> ProviderServer? { guard let serverId = entity.serverId, let providerId = entity.providerId, let categoryName = entity.categoryName, @@ -105,8 +105,8 @@ struct DomainMapper { let otherCountryCodes = entity.otherCountryCodes?.components(separatedBy: ",") let area = entity.area - let provider = VPNServer.Provider( - id: .init(rawValue: providerId), + let metadata = ProviderServer.Metadata( + providerId: .init(rawValue: providerId), serverId: serverId, supportedConfigurationIdentifiers: supportedConfigurationIds, supportedPresetIds: supportedPresetIds, @@ -115,6 +115,6 @@ struct DomainMapper { otherCountryCodes: otherCountryCodes, area: area ) - return VPNServer(provider: provider, hostname: hostname, ipAddresses: ipAddresses) + return ProviderServer(metadata: metadata, hostname: hostname, ipAddresses: ipAddresses) } } diff --git a/Packages/App/Sources/AppDataProviders/Domain/VPNServerParameters+CoreData.swift b/Packages/App/Sources/AppDataProviders/Domain/ProviderServerParameters+CoreData.swift similarity index 96% rename from Packages/App/Sources/AppDataProviders/Domain/VPNServerParameters+CoreData.swift rename to Packages/App/Sources/AppDataProviders/Domain/ProviderServerParameters+CoreData.swift index a24154301..d6d46572f 100644 --- a/Packages/App/Sources/AppDataProviders/Domain/VPNServerParameters+CoreData.swift +++ b/Packages/App/Sources/AppDataProviders/Domain/ProviderServerParameters+CoreData.swift @@ -1,5 +1,5 @@ // -// VPNServerParameters+CoreData.swift +// ProviderServerParameters+CoreData.swift // Passepartout // // Created by Davide De Rosa on 10/28/24. @@ -40,7 +40,7 @@ extension ProviderID { } } -extension VPNSortField { +extension ProviderSortField { var sortDescriptor: NSSortDescriptor { switch self { case .localizedCountry: @@ -58,7 +58,7 @@ extension VPNSortField { } } -extension VPNFilters { +extension ProviderFilters { func predicate(for providerId: ProviderID) -> NSPredicate { var formats: [String] = [] var args: [Any] = [] diff --git a/Packages/App/Sources/AppDataProviders/Strategy/CDAPIRepositoryV3.swift b/Packages/App/Sources/AppDataProviders/Strategy/CDAPIRepositoryV3.swift new file mode 100644 index 000000000..bf9289925 --- /dev/null +++ b/Packages/App/Sources/AppDataProviders/Strategy/CDAPIRepositoryV3.swift @@ -0,0 +1,173 @@ +// +// CDAPIRepositoryV3.swift +// Passepartout +// +// Created by Davide De Rosa on 10/26/24. +// Copyright (c) 2025 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import AppData +import Combine +import CommonUtils +import CoreData +import Foundation +import PassepartoutKit + +extension AppData { + public static func cdAPIRepositoryV3(context: NSManagedObjectContext) -> APIRepository { + CDAPIRepositoryV3(context: context) + } +} + +private final class CDAPIRepositoryV3: NSObject, APIRepository { + private nonisolated let context: NSManagedObjectContext + + private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never> + + private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never> + + private nonisolated let providersController: NSFetchedResultsController + + init(context: NSManagedObjectContext) { + self.context = context + providersSubject = CurrentValueSubject([]) + lastUpdateSubject = CurrentValueSubject([:]) + + let request = CDProviderV3.fetchRequest() + request.sortDescriptors = [ + NSSortDescriptor(key: "providerId", ascending: true) + ] + providersController = .init( + fetchRequest: request, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil + ) + + super.init() + + Task { + try await context.perform { [weak self] in + self?.providersController.delegate = self + try self?.providersController.performFetch() + } + } + } + + nonisolated var indexPublisher: AnyPublisher<[Provider], Never> { + providersSubject + .removeDuplicates() + .eraseToAnyPublisher() + } + + nonisolated var lastUpdatePublisher: AnyPublisher<[ProviderID: Date], Never> { + lastUpdateSubject + .removeDuplicates() + .eraseToAnyPublisher() + } + + func store(_ index: [Provider]) async throws { + try await context.perform { [weak self] in + guard let self else { + return + } + do { + // fetch existing for last update and deletion + let request = CDProviderV3.fetchRequest() + let results = try request.execute() + let lastUpdatesByProvider = results.reduce(into: [:]) { + $0[$1.providerId] = $1.lastUpdate + } + results.forEach(context.delete) + + // replace but retain last update + let mapper = CoreDataMapper(context: context) + try index.forEach { + let lastUpdate = lastUpdatesByProvider[$0.id.rawValue] + try mapper.cdProvider(from: $0, lastUpdate: lastUpdate) + } + + try context.save() + } catch { + context.rollback() + throw error + } + } + } + + func store(_ infrastructure: ProviderInfrastructure, for providerId: ProviderID) async throws { + try await context.perform { [weak self] in + guard let self else { + return + } + do { + let predicate = providerId.predicate + + // signal update of related provider + let providerRequest = CDProviderV3.fetchRequest() + providerRequest.predicate = predicate + let providers = try providerRequest.execute() + if let provider = providers.first { + provider.lastUpdate = infrastructure.lastUpdate + } + + // delete all provider entities + let serverRequest = CDVPNServerV3.fetchRequest() + serverRequest.predicate = predicate + let servers = try serverRequest.execute() + servers.forEach(context.delete) + + let presetRequest = CDVPNPresetV3.fetchRequest() + presetRequest.predicate = predicate + let presets = try presetRequest.execute() + presets.forEach(context.delete) + + // create new entities + let mapper = CoreDataMapper(context: context) + try infrastructure.servers.forEach { + try mapper.cdServer(from: $0) + } + try infrastructure.presets.forEach { + try mapper.cdPreset(from: $0) + } + + try context.save() + } catch { + context.rollback() + throw error + } + } + } + + nonisolated func providerRepository(for providerId: ProviderID) -> ProviderRepository { + CDProviderRepositoryV3(context: context, providerId: providerId) + } +} + +extension CDAPIRepositoryV3: NSFetchedResultsControllerDelegate { + nonisolated func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + guard let entities = controller.fetchedObjects as? [CDProviderV3] else { + return + } + let mapper = DomainMapper() + providersSubject.send(entities.compactMap(mapper.provider(from:))) + lastUpdateSubject.send(mapper.lastUpdate(from: entities)) + } +} diff --git a/Packages/App/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift b/Packages/App/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift index 19faa324e..c8c2802b7 100644 --- a/Packages/App/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift +++ b/Packages/App/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift @@ -24,150 +24,73 @@ // import AppData -import Combine import CommonUtils import CoreData import Foundation import PassepartoutKit -extension AppData { - public static func cdProviderRepositoryV3(context: NSManagedObjectContext) -> ProviderRepository { - CDProviderRepositoryV3(context: context) - } -} - -private final class CDProviderRepositoryV3: NSObject, ProviderRepository { - private nonisolated let context: NSManagedObjectContext - - private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never> - - private nonisolated let lastUpdateSubject: CurrentValueSubject<[ProviderID: Date], Never> +final class CDProviderRepositoryV3: ProviderRepository { + private let context: NSManagedObjectContext - private nonisolated let providersController: NSFetchedResultsController + let providerId: ProviderID - init(context: NSManagedObjectContext) { + init(context: NSManagedObjectContext, providerId: ProviderID) { self.context = context - providersSubject = CurrentValueSubject([]) - lastUpdateSubject = CurrentValueSubject([:]) - - let request = CDProviderV3.fetchRequest() - request.sortDescriptors = [ - NSSortDescriptor(key: "providerId", ascending: true) - ] - providersController = .init( - fetchRequest: request, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil - ) - - super.init() - - Task { - try await context.perform { [weak self] in - self?.providersController.delegate = self - try self?.providersController.performFetch() - } - } - } - - nonisolated var indexPublisher: AnyPublisher<[Provider], Never> { - providersSubject - .removeDuplicates() - .eraseToAnyPublisher() - } - - nonisolated var lastUpdatePublisher: AnyPublisher<[ProviderID: Date], Never> { - lastUpdateSubject - .removeDuplicates() - .eraseToAnyPublisher() + self.providerId = providerId } - func store(_ index: [Provider]) async throws { - try await context.perform { [weak self] in - guard let self else { - return - } - do { - // fetch existing for last update and deletion - let request = CDProviderV3.fetchRequest() - let results = try request.execute() - let lastUpdatesByProvider = results.reduce(into: [:]) { - $0[$1.providerId] = $1.lastUpdate - } - results.forEach(context.delete) - - // replace but retain last update - let mapper = CoreDataMapper(context: context) - try index.forEach { - let lastUpdate = lastUpdatesByProvider[$0.id.rawValue] - try mapper.cdProvider(from: $0, lastUpdate: lastUpdate) + func availableOptions