Skip to content

Commit

Permalink
Reuse provider entities for more than VPN
Browse files Browse the repository at this point in the history
- Rename views
- Decouple provider templates and configurations
  • Loading branch information
keeshux committed Jan 27, 2025
1 parent ec2b213 commit e115bfe
Show file tree
Hide file tree
Showing 44 changed files with 763 additions and 792 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ final class CDVPNPresetV3: NSManagedObject {
NSFetchRequest<CDVPNPresetV3>(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?
Expand Down
24 changes: 12 additions & 12 deletions Packages/App/Sources/AppDataProviders/Domain/CoreDataMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,35 +42,35 @@ 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
entity.hostname = server.hostname
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
}
}
16 changes: 8 additions & 8 deletions Packages/App/Sources/AppDataProviders/Domain/DomainMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// VPNServerParameters+CoreData.swift
// ProviderServerParameters+CoreData.swift
// Passepartout
//
// Created by Davide De Rosa on 10/28/24.
Expand Down Expand Up @@ -40,7 +40,7 @@ extension ProviderID {
}
}

extension VPNSortField {
extension ProviderSortField {
var sortDescriptor: NSSortDescriptor {
switch self {
case .localizedCountry:
Expand All @@ -58,7 +58,7 @@ extension VPNSortField {
}
}

extension VPNFilters {
extension ProviderFilters {
func predicate(for providerId: ProviderID) -> NSPredicate {
var formats: [String] = []
var args: [Any] = []
Expand Down
173 changes: 173 additions & 0 deletions Packages/App/Sources/AppDataProviders/Strategy/CDAPIRepositoryV3.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
//

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

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<any NSFetchRequestResult>) {
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))
}
}
Loading

0 comments on commit e115bfe

Please sign in to comment.