Refactor static functions/entities in Library (#679)

Reduce the impact of hidden dependencies on BundleConfiguration and
Constants.shared

Fixes #656
This commit is contained in:
Davide 2024-10-04 09:58:42 +02:00 committed by GitHub
parent 4b0bc7f064
commit 5fb6f4f4d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 176 additions and 184 deletions

View File

@ -32,8 +32,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "2e61214462dcf6ad9e211d8fdbd611c6755845c4",
"version" : "0.8.0"
"revision" : "779910e268e79f1004a95285ac2485255d88bb21"
}
},
{

View File

@ -70,9 +70,10 @@ private extension PassepartoutApp {
registry: context.registry
)
.onLoad {
CommonLibrary.configureLogging(
PassepartoutConfiguration.shared.configureLogging(
to: BundleConfiguration.urlForAppLog,
parameters: Constants.shared.log
parameters: Constants.shared.log,
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
)
AppUI.configure(with: context)
}

View File

@ -30,8 +30,8 @@ let package = Package(
)
],
dependencies: [
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.8.0"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "2e61214462dcf6ad9e211d8fdbd611c6755845c4"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.8.0"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "779910e268e79f1004a95285ac2485255d88bb21"),
// .package(path: "../../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.8.0"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),

View File

@ -30,8 +30,6 @@ import PassepartoutKit
import UtilsLibrary
extension AppData {
// TODO: #656, make non-static
public static func cdProfileRepositoryV3(
registry: Registry,
coder: ProfileCoder,

View File

@ -26,11 +26,7 @@
import Foundation
import PassepartoutKit
// TODO: #656, make non-static
public struct AppUI {
private init() {
}
public enum AppUI {
public static func configure(with context: AppContext) {
assertMissingModuleImplementations()
}
@ -40,11 +36,11 @@ private extension AppUI {
static func assertMissingModuleImplementations() {
ModuleType.allCases.forEach { moduleType in
let module = moduleType.newModule()
guard module as? ModuleTypeProviding != nil else {
fatalError("\(moduleType): does not implement ModuleTypeProviding")
guard module is ModuleTypeProviding else {
fatalError("\(moduleType): is not ModuleTypeProviding")
}
guard module as? any ModuleViewProviding != nil else {
fatalError("\(moduleType): does not implement ModuleViewProviding")
guard module is any ModuleViewProviding else {
fatalError("\(moduleType): is not ModuleViewProviding")
}
}
}

View File

@ -1,5 +1,5 @@
//
// Issue+App.swift
// Issue+Metadata.swift
// Passepartout
//
// Created by Davide De Rosa on 9/18/24.
@ -28,23 +28,35 @@ import Foundation
import PassepartoutKit
extension Issue {
struct Metadata {
let configuration: PassepartoutConfiguration
// TODO: #656, make non-static
static func with(versionString: String, purchasedProducts: Set<AppProduct>, tunnel: Tunnel) async -> Self {
let appLog = CommonLibrary.currentLog(parameters: Constants.shared.log)
let versionString: String
let purchasedProducts: Set<AppProduct>
let tunnel: Tunnel
let urlForTunnelLog: URL
let parameters: Constants.Log
}
static func withMetadata(_ metadata: Metadata) async -> Issue {
let appLog = metadata.configuration.currentLog(parameters: metadata.parameters)
.joined(separator: "\n")
.data(using: .utf8)
let tunnelLog: Data?
// live tunnel log
if await tunnel.status != .inactive {
tunnelLog = await tunnel.currentLog(parameters: Constants.shared.log)
if await metadata.tunnel.status != .inactive {
tunnelLog = await metadata.tunnel.currentLog(parameters: metadata.parameters)
.joined(separator: "\n")
.data(using: .utf8)
}
// latest persisted tunnel log
else if let latestTunnelEntry = CommonLibrary.availableLogs(at: BundleConfiguration.urlForTunnelLog)
else if let latestTunnelEntry = metadata.configuration.availableLogs(at: metadata.urlForTunnelLog)
.max(by: { $0.key < $1.key }) {
tunnelLog = try? Data(contentsOf: latestTunnelEntry.value)
@ -55,13 +67,15 @@ extension Issue {
}
return Issue(
appLine: "\(Strings.Unlocalized.appName) \(versionString)",
purchasedProducts: purchasedProducts,
appLine: "\(Strings.Unlocalized.appName) \(metadata.versionString)",
purchasedProducts: metadata.purchasedProducts,
appLog: appLog,
tunnelLog: tunnelLog
)
}
}
extension Issue {
var to: String {
Constants.shared.emails.issues
}

View File

@ -29,22 +29,18 @@ import SwiftUI
import UtilsLibrary
extension DebugLogView {
// TODO: #656, make non-static
static func withApp(parameters: Constants.Log) -> DebugLogView {
DebugLogView {
CommonLibrary.currentLog(parameters: parameters)
PassepartoutConfiguration.shared.currentLog(parameters: parameters)
}
}
// TODO: #656, make non-static
static func withTunnel(_ tunnel: Tunnel, parameters: Constants.Log) -> DebugLogView {
DebugLogView {
await tunnel.currentLog(parameters: parameters)
}
}
// TODO: #656, make non-static
static func withURL(_ url: URL) -> DebugLogView {
DebugLogView {
do {

View File

@ -47,11 +47,11 @@ struct DiagnosticsView: View {
@EnvironmentObject
var iapManager: IAPManager
@AppStorage(AppPreference.logsPrivateData.key, store: .group)
@AppStorage(AppPreference.logsPrivateData.key, store: .appGroup)
private var logsPrivateData = false
var availableTunnelLogs: () -> [LogEntry] = {
CommonLibrary.availableLogs(at: BundleConfiguration.urlForTunnelLog)
PassepartoutConfiguration.shared.availableLogs(at: BundleConfiguration.urlForTunnelLog)
.sorted {
$0.key > $1.key
}

View File

@ -25,6 +25,7 @@
#if os(iOS)
import CommonLibrary
import PassepartoutKit
import SwiftUI
import UIKit
@ -33,24 +34,7 @@ import UtilsLibrary
extension ReportIssueButton: View {
var body: some View {
HStack {
Button(title) {
Task {
isPending = true
defer {
isPending = false
}
let issue = await Issue.with(
versionString: BundleConfiguration.mainVersionString,
purchasedProducts: purchasedProducts,
tunnel: tunnel
)
guard MailComposerView.canSendMail() else {
openMailTo(with: issue)
return
}
issueBeingReported = issue
}
}
Button(title, action: sendEmail)
if isPending {
Spacer()
ProgressView()
@ -75,6 +59,41 @@ extension ReportIssueButton: View {
}
}
private extension ReportIssueButton {
func sendEmail() {
Task {
isPending = true
defer {
isPending = false
}
let issue = await Issue.withMetadata(.init(
configuration: .shared,
versionString: BundleConfiguration.mainVersionString,
purchasedProducts: purchasedProducts,
tunnel: tunnel,
urlForTunnelLog: BundleConfiguration.urlForTunnelLog,
parameters: Constants.shared.log
))
guard MailComposerView.canSendMail() else {
openMailTo(with: issue)
return
}
issueBeingReported = issue
}
}
func openMailTo(with issue: Issue) {
guard let url = URL.mailto(to: issue.to, subject: issue.subject, body: issue.body) else {
return
}
guard UIApplication.shared.canOpenURL(url) else {
isUnableToEmail = true
return
}
UIApplication.shared.open(url)
}
}
private extension Issue {
var attachments: [MailComposerView.Attachment] {
var list: [MailComposerView.Attachment] = []
@ -89,17 +108,4 @@ private extension Issue {
}
}
private extension ReportIssueButton {
func openMailTo(with issue: Issue) {
guard let url = URL.mailto(to: issue.to, subject: issue.subject, body: issue.body) else {
return
}
guard UIApplication.shared.canOpenURL(url) else {
isUnableToEmail = true
return
}
UIApplication.shared.open(url)
}
}
#endif

View File

@ -25,26 +25,39 @@
#if os(macOS)
import CommonLibrary
import PassepartoutKit
import SwiftUI
extension ReportIssueButton: View {
var body: some View {
Button(title) {
Button(title, action: sendEmail)
.disabled(isPending)
}
}
private extension ReportIssueButton {
func sendEmail() {
Task {
guard let service = NSSharingService(named: .composeEmail) else {
isUnableToEmail = true
return
}
Task {
let issue = await Issue.with(
versionString: BundleConfiguration.mainVersionString,
purchasedProducts: purchasedProducts,
tunnel: tunnel
)
service.recipients = [issue.to]
service.subject = issue.subject
service.perform(withItems: issue.items)
isPending = true
defer {
isPending = false
}
let issue = await Issue.withMetadata(.init(
configuration: .shared,
versionString: BundleConfiguration.mainVersionString,
purchasedProducts: purchasedProducts,
tunnel: tunnel,
urlForTunnelLog: BundleConfiguration.urlForTunnelLog,
parameters: Constants.shared.log
))
service.recipients = [issue.to]
service.subject = issue.subject
service.perform(withItems: issue.items)
}
}
}

View File

@ -1,8 +1,8 @@
//
// CommonLibrary.swift
// PassepartoutConfiguration+Extensions.swift
// Passepartout
//
// Created by Davide De Rosa on 8/31/24.
// Created by Davide De Rosa on 10/4/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@ -26,24 +26,20 @@
import Foundation
import PassepartoutKit
// TODO: #656, make non-static
public struct CommonLibrary {
private init() {
}
public static func configureLogging(to url: URL, parameters: Constants.Log) {
extension PassepartoutConfiguration {
public func configureLogging(to url: URL, parameters: Constants.Log, logsPrivateData: Bool) {
pp_log(.common, .debug, "Log to: \(url)")
PassepartoutConfiguration.shared.setLocalLogger(options: .init(
setLocalLogger(options: .init(
url: url,
maxNumberOfLines: parameters.maxNumberOfLines,
maxLevel: parameters.maxLevel,
mapper: parameters.formatter.formattedLine
))
if UserDefaults.group.bool(forKey: AppPreference.logsPrivateData.key) {
PassepartoutConfiguration.shared.logsAddresses = true
PassepartoutConfiguration.shared.logsModules = true
if logsPrivateData {
logsAddresses = true
logsModules = true
}
if let maxAge = parameters.maxAge {
@ -51,15 +47,15 @@ public struct CommonLibrary {
}
}
public static func currentLog(parameters: Constants.Log) -> [String] {
PassepartoutConfiguration.shared.currentLogLines(
public func currentLog(parameters: Constants.Log) -> [String] {
currentLogLines(
sinceLast: parameters.sinceLast,
maxLevel: parameters.maxLevel
)
.map(parameters.formatter.formattedLine)
}
public static func availableLogs(at url: URL) -> [Date: URL] {
public func availableLogs(at url: URL) -> [Date: URL] {
let parent = url.deletingLastPathComponent()
let prefix = url.lastPathComponent
do {
@ -81,13 +77,13 @@ public struct CommonLibrary {
}
}
public static func flushLog() {
try? PassepartoutConfiguration.shared.saveLog()
public func flushLog() {
try? saveLog()
}
}
private extension CommonLibrary {
static func purgeLogs(at url: URL, beyond maxAge: TimeInterval) {
private extension PassepartoutConfiguration {
func purgeLogs(at url: URL, beyond maxAge: TimeInterval) {
let logs = availableLogs(at: url)
let minDate = Date().addingTimeInterval(-maxAge)
logs.forEach { date, url in

View File

@ -0,0 +1,49 @@
//
// BundleConfiguration+AppGroup.swift
// Passepartout
//
// Created by Davide De Rosa on 10/4/24.
// Copyright (c) 2024 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 Foundation
import PassepartoutKit
// WARNING: beware of Constants.shared dependency
extension BundleConfiguration {
public static var urlForAppLog: URL {
cachesURL.appending(path: Constants.shared.log.appPath)
}
public static var urlForTunnelLog: URL {
cachesURL.appending(path: Constants.shared.log.tunnelPath)
}
}
private extension BundleConfiguration {
static var cachesURL: URL {
let groupId = mainString(for: .groupId)
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
fatalError("Unable to access App Group container")
}
return url.appending(components: "Library", "Caches")
}
}

View File

@ -26,7 +26,8 @@
import Foundation
import PassepartoutKit
// TODO: #656, make non-static
// WARNING: beware of Constants.shared dependency
extension BundleConfiguration {
public enum BundleKey: String {
case appStoreId
@ -92,21 +93,13 @@ extension BundleConfiguration {
}
return url
}
public static var urlForAppLog: URL {
cachesURL.appending(path: Constants.shared.log.appPath)
}
public static var urlForTunnelLog: URL {
cachesURL.appending(path: Constants.shared.log.tunnelPath)
}
}
private extension BundleConfiguration {
// WARNING: fails from package itself, e.g. in previews
static var main: BundleConfiguration {
guard let bundle = BundleConfiguration(.main, key: Constants.shared.bundle) else {
guard let bundle = BundleConfiguration(.main, key: Constants.shared.bundleKey) else {
fatalError("Missing main bundle")
}
return bundle
@ -115,12 +108,4 @@ private extension BundleConfiguration {
static var isPreview: Bool {
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
static var cachesURL: URL {
let groupId = mainString(for: .groupId)
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else {
fatalError("Unable to access App Group container")
}
return url.appending(components: "Library", "Caches")
}
}

View File

@ -128,7 +128,7 @@ public struct Constants: Decodable, Sendable {
public let maxAge: TimeInterval?
}
public let bundle: String
public let bundleKey: String
public let websites: Websites

View File

@ -1,5 +1,5 @@
{
"bundle": "AppConfig",
"bundleKey": "AppConfig",
"websites": {
"home": "https://passepartoutvpn.app",
"subreddit": "https://www.reddit.com/r/passepartout/",

View File

@ -31,7 +31,7 @@ extension LoggerDestination {
}
extension UserDefaults {
public static let group: UserDefaults = {
public static let appGroup: UserDefaults = {
let appGroup = BundleConfiguration.mainString(for: .groupId)
guard let defaults = UserDefaults(suiteName: appGroup) else {
fatalError("No access to App Group: \(appGroup)")

View File

@ -219,7 +219,7 @@ private extension GenericCreditsView.LicenseView {
do {
let session = URLSession(configuration: .ephemeral)
let response = try await session.data(from: url)
let string = String(decoding: response.0, as: UTF8.self)
let string = String(data: response.0, encoding: .utf8)
withAnimation {
content = string
}

View File

@ -188,70 +188,8 @@ extension IAPManagerTests {
// MARK: Purchasable
// func test_givenNoPurchase_thenCanBuyFullAndPlatformVersion() {
// let reader = MockReceiptReader()
// reader.setReceipt(withBuild: defaultBuildNumber, products: [])
// let sut = IAPManager(receiptReader: reader)
//
//#if targetEnvironment(macCatalyst)
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion, .fullVersion_macOS])
//#else
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion, .fullVersion_iOS])
//#endif
// }
//
// func test_givenFullVersion_thenCannotPurchase() {
// let reader = MockReceiptReader()
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.fullVersion])
// let sut = IAPManager(receiptReader: reader)
//
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [])
// }
//
// func test_givenPlatformVersion_thenCannotPurchaseSamePlatform() {
// let reader = MockReceiptReader()
//
//#if targetEnvironment(macCatalyst)
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.fullVersion_macOS])
// let sut = IAPManager(receiptReader: reader)
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [])
//#else
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.fullVersion_iOS])
// let sut = IAPManager(receiptReader: reader)
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [])
//#endif
// }
//
// func test_givenOtherPlatformVersion_thenCanOnlyPurchaseMissingPlatform() {
// let reader = MockReceiptReader()
//
//#if targetEnvironment(macCatalyst)
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.fullVersion_iOS])
// let sut = IAPManager(receiptReader: reader)
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion_macOS])
//#else
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.fullVersion_macOS])
// let sut = IAPManager(receiptReader: reader)
// XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion_iOS])
//#endif
// }
//
// func test_givenAppleTV_whenDidNotPurchase_thenCanPurchase() {
// let reader = MockReceiptReader()
// reader.setReceipt(withBuild: defaultBuildNumber, products: [])
// let sut = IAPManager(receiptReader: reader)
//
// XCTAssertEqual(sut.purchasableProducts(withFeature: .appleTV), [.appleTV])
// }
//
// func test_givenAppleTV_whenDidPurchase_thenCannotPurchase() {
// let reader = MockReceiptReader()
// reader.setReceipt(withBuild: defaultBuildNumber, products: [.appleTV])
// let sut = IAPManager(receiptReader: reader)
//
// XCTAssertEqual(sut.purchasableProducts(withFeature: .appleTV), [])
// }
//
// TODO: #570, test app library
// MARK: App level
func test_givenBetaApp_thenIsRestricted() async {

View File

@ -31,9 +31,10 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private var fwd: NEPTPForwarder?
override func startTunnel(options: [String: NSObject]? = nil) async throws {
CommonLibrary.configureLogging(
PassepartoutConfiguration.shared.configureLogging(
to: BundleConfiguration.urlForTunnelLog,
parameters: Constants.shared.log
parameters: Constants.shared.log,
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
)
fwd = try await NEPTPForwarder(
provider: self,
@ -44,7 +45,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
do {
try await fwd?.startTunnel(options: options)
} catch {
CommonLibrary.flushLog()
PassepartoutConfiguration.shared.flushLog()
throw error
}
}
@ -52,11 +53,11 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
override func stopTunnel(with reason: NEProviderStopReason) async {
await fwd?.stopTunnel(with: reason)
fwd = nil
CommonLibrary.flushLog()
PassepartoutConfiguration.shared.flushLog()
}
override func cancelTunnelWithError(_ error: (any Error)?) {
CommonLibrary.flushLog()
PassepartoutConfiguration.shared.flushLog()
super.cancelTunnelWithError(error)
}