Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TablePro/Core/Plugins/PluginManager+Registration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,6 @@ extension PluginManager {
}

nonisolated static func registryPlugin(forTypeId pluginTypeId: String, in manifest: RegistryManifest?) -> RegistryPlugin? {
manifest?.plugins.first { $0.databaseTypeIds?.contains(pluginTypeId) == true }
manifest?.plugins.first { $0.databaseTypeIds.contains(pluginTypeId) }
}
}
6 changes: 5 additions & 1 deletion TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ final class PluginManager: ObservableObject {
let displayName = registrySnapshot?.displayName
?? bundleId.split(separator: ".").last.map(String.init)
?? bundleId
let pluginIconName = registrySnapshot?.iconName ?? "puzzlepiece"
let pluginIconName = Self.iconName(forPrimaryTypeId: primaryTypeId)
let defaultPort = registrySnapshot?.defaultPort
let pluginDescription = registrySnapshot?.connection.tagline ?? ""

Expand Down Expand Up @@ -309,6 +309,10 @@ final class PluginManager: ObservableObject {
Self.logger.debug("Registered lazy plugin '\(bundleId)': drivers=\(manifest.providedDatabaseTypeIds), exports=\(manifest.providedExportFormatIds), imports=\(manifest.providedImportFormatIds)")
}

nonisolated static func iconName(forPrimaryTypeId typeId: String?) -> String {
typeId.flatMap { PluginMetadataRegistry.shared.snapshot(forRegisteredTypeId: $0)?.iconName } ?? "puzzlepiece"
}

/// Takes back everything `registerLazyManifest` published for this bundle, so a plugin the app
/// would refuse to activate stops being offered as an installed one.
internal func withdrawPlugin(at url: URL, reason: Error) {
Expand Down
5 changes: 2 additions & 3 deletions TablePro/Core/Plugins/Registry/RegistryModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ extension RegistryManifest: Decodable {
internal struct RegistryPlugin: Decodable, Sendable, Identifiable {
internal let id: String
internal let name: String
internal let iconName: String?
internal let databaseTypeIds: [String]?
internal let exportFormatIds: [String]?
internal let databaseTypeIds: [String]
internal let exportFormatIds: [String]
internal let releases: [RegistryRelease]
}

Expand Down
18 changes: 3 additions & 15 deletions TablePro/Core/Services/Export/ExportFormatCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,17 @@ internal enum ExportFormatCatalog {
"csv", "json", "sql", "xlsx", "md", "parquet", "mql"
]

// scripts/release/registry.py writes no exportFormatIds yet, so Parquet stays offered by hand until it does.
private static let unlistedInstallable = [
InstallableExportFormat(
formatId: "parquet",
displayName: "Parquet",
registryPluginId: "com.TablePro.ParquetExportPlugin"
)
]

internal static func installable(in manifest: RegistryManifest?) -> [InstallableExportFormat] {
let listed = (manifest?.plugins ?? []).flatMap { plugin in
(plugin.exportFormatIds ?? []).map { formatId in
(manifest?.plugins ?? []).flatMap { plugin in
plugin.exportFormatIds.map { formatId in
InstallableExportFormat(formatId: formatId, displayName: plugin.name, registryPluginId: plugin.id)
}
}
let listedFormatIds = Set(listed.map(\.formatId))
return listed + unlistedInstallable.filter { !listedFormatIds.contains($0.formatId) }
}

@MainActor
internal static func notInstalled(
installedFormatIds: Set<String>,
manifest: RegistryManifest? = RegistryClient.shared.manifest
manifest: RegistryManifest?
) -> [InstallableExportFormat] {
installable(in: manifest).filter { !installedFormatIds.contains($0.formatId) }
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Export/ExportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UniformTypeIdentifiers

struct ExportDialog: View {
@ObservedObject private var pluginManager = PluginManager.shared
@ObservedObject private var registryClient = RegistryClient.shared
private static let logger = Logger(subsystem: "com.TablePro", category: "ExportDialog")

@Binding var isPresented: Bool
Expand Down Expand Up @@ -219,7 +220,7 @@ struct ExportDialog: View {

private var formatsToOffer: [InstallableExportFormat] {
let installed = Set(pluginManager.allExportPlugins().map { type(of: $0).formatId })
return ExportFormatCatalog.notInstalled(installedFormatIds: installed)
return ExportFormatCatalog.notInstalled(installedFormatIds: installed, manifest: registryClient.manifest)
}

private static func offerTag(for format: InstallableExportFormat) -> String {
Expand Down
10 changes: 1 addition & 9 deletions TablePro/Views/Settings/Plugins/InstalledPluginsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ struct InstalledPluginsView: View {
@ViewBuilder
private func rejectedPluginRow(_ plugin: RejectedPlugin, action: RejectedPluginAction) -> some View {
HStack(spacing: 8) {
PluginIconView(name: rejectedIconName(for: plugin))
PluginIconView(name: PluginManager.iconName(forPrimaryTypeId: plugin.providedDatabaseTypeIds.first))
.font(.title3)
.frame(width: 24, height: 24)
.foregroundStyle(.tertiary)
Expand Down Expand Up @@ -198,14 +198,6 @@ struct InstalledPluginsView: View {
}
}

private func rejectedIconName(for plugin: RejectedPlugin) -> String {
registryEntry(for: plugin)?.iconName ?? "puzzlepiece"
}

private func registryEntry(for plugin: RejectedPlugin) -> RegistryPlugin? {
pluginManager.registryPlugin(for: plugin)
}

private func removeRejectedPlugin(_ plugin: RejectedPlugin) {
let fm = FileManager.default
if fm.fileExists(atPath: plugin.url.path) {
Expand Down
47 changes: 27 additions & 20 deletions TableProTests/Core/Export/ExportFormatCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,40 +117,47 @@ struct ExportFormatCatalogTests {
RegistryManifest(schemaVersion: RegistryManifest.supportedSchemaVersion, plugins: plugins)
}

private func registryPlugin(id: String, name: String, exportFormatIds: [String]?) -> RegistryPlugin {
private func registryPlugin(id: String, name: String, exportFormatIds: [String]) -> RegistryPlugin {
RegistryPlugin(
id: id,
name: name,
iconName: nil,
databaseTypeIds: [],
exportFormatIds: exportFormatIds,
releases: []
)
}

private let handParquet = InstallableExportFormat(
formatId: "parquet",
displayName: "Parquet",
registryPluginId: "com.TablePro.ParquetExportPlugin"
)

@Test("Parquet is offered while the manifest names no export format")
@MainActor
func parquetOfferedWithoutManifestFormats() {
@Test("No manifest, or one that lists no export format, offers no install")
func noManifestOffersNoInstall() {
let unlisted = manifest([
registryPlugin(id: "com.TablePro.OracleDriver", name: "Oracle Driver", exportFormatIds: nil),
registryPlugin(id: "com.TablePro.ParquetExportPlugin", name: "Parquet Export", exportFormatIds: nil)
registryPlugin(id: "com.TablePro.OracleDriver", name: "Oracle Driver", exportFormatIds: [])
])

#expect(ExportFormatCatalog.installable(in: nil) == [handParquet])
#expect(ExportFormatCatalog.installable(in: unlisted) == [handParquet])
#expect(ExportFormatCatalog.notInstalled(installedFormatIds: [], manifest: nil) == [handParquet])
#expect(ExportFormatCatalog.notInstalled(installedFormatIds: ["parquet"], manifest: nil).isEmpty)
#expect(ExportFormatCatalog.installable(in: nil).isEmpty)
#expect(ExportFormatCatalog.installable(in: unlisted).isEmpty)
#expect(ExportFormatCatalog.notInstalled(installedFormatIds: [], manifest: nil).isEmpty)
}

@Test("A manifest entry's exportFormatIds decode into an install offer")
func decodedManifestOffersItsFormats() throws {
let json = """
{"schemaVersion": 3, "retainFromPluginKit": 39, "plugins": [{
"id": "com.TablePro.ParquetExportPlugin", "name": "Parquet Export",
"databaseTypeIds": [], "exportFormatIds": ["parquet"], "releases": []
}]}
"""
let decoded = try JSONDecoder().decode(RegistryManifest.self, from: Data(json.utf8))
let parquet = InstallableExportFormat(
formatId: "parquet",
displayName: "Parquet Export",
registryPluginId: "com.TablePro.ParquetExportPlugin"
)

#expect(ExportFormatCatalog.installable(in: decoded) == [parquet])
}

@Test("A format the manifest lists is offered once, from the manifest")
@MainActor
func manifestFormatsReplaceTheHandEntry() {
@Test("Each format the manifest lists is offered under its plugin's name")
func manifestFormatsAreOffered() {
let listed = manifest([
registryPlugin(id: "com.TablePro.ParquetExportPlugin", name: "Parquet Export", exportFormatIds: ["parquet"]),
registryPlugin(id: "com.example.AvroExport", name: "Avro Export", exportFormatIds: ["avro"])
Expand Down
2 changes: 2 additions & 0 deletions TableProTests/Core/Plugins/DriverUnavailabilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ struct DriverUnavailabilityTests {
"id": "com.example.future",
"name": "Future Driver",
"databaseTypeIds": ["\(typeId)"],
"exportFormatIds": [],
"releases": []
}]
}
Expand Down Expand Up @@ -222,6 +223,7 @@ struct DriverUnavailabilityTests {
"id": "com.example.eager",
"name": "Eager Driver",
"databaseTypeIds": ["EagerDB"],
"exportFormatIds": [],
"releases": []
}]
}
Expand Down
11 changes: 10 additions & 1 deletion TableProTests/Core/Plugins/PluginLazyRegistrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,19 @@ struct PluginLazyRegistrationTests {
#expect(manager.lazyDriverURLs[DatabaseType.oracle.rawValue]?.lastPathComponent == url.lastPathComponent)
#expect(manager.isDriverInstalled(for: .oracle))
#expect(manager.driverPlugins[DatabaseType.oracle.rawValue] == nil)
#expect(manager.plugins.first { $0.id == "com.TablePro.OracleDriver" }?.databaseTypeId == DatabaseType.oracle.rawValue)
let entry = manager.plugins.first { $0.id == "com.TablePro.OracleDriver" }
#expect(entry?.databaseTypeId == DatabaseType.oracle.rawValue)
#expect(entry?.pluginIconName == "oracle-icon")
#expect(manager.rejectedPlugins.isEmpty)
}

@Test("A plugin's icon is its primary type's snapshot icon, or the puzzle piece without one")
func iconComesFromThePrimaryTypeSnapshot() {
#expect(PluginManager.iconName(forPrimaryTypeId: DatabaseType.oracle.rawValue) == "oracle-icon")
#expect(PluginManager.iconName(forPrimaryTypeId: "NoSuchDB") == "puzzlepiece")
#expect(PluginManager.iconName(forPrimaryTypeId: nil) == "puzzlepiece")
}

@Test("A driver whose Info.plist declares no ids is loaded at launch")
func unstampedDriverLoadsAtLaunch() async throws {
let fixture = try makeFixture()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,8 @@ struct PluginRegistryUpdatePassTests {
RegistryPlugin(
id: id,
name: "Test Plugin",
iconName: nil,
databaseTypeIds: nil,
exportFormatIds: nil,
databaseTypeIds: [],
exportFormatIds: [],
releases: releases
)
}
Expand Down Expand Up @@ -252,7 +251,7 @@ struct PluginRegistryUpdatePassTests {
let manifestURL = try #require(URL(string: "https://registry.invalid/\(UUID().uuidString)/plugins/v3.json"))
let plugins = pluginIds.map { id in
"""
{"id": "\(id)", "name": "Driver", "releases": [{
{"id": "\(id)", "name": "Driver", "databaseTypeIds": [], "exportFormatIds": [], "releases": [{
"version": "1.0", "build": \(build), "channel": "stable",
"pluginKit": \(PluginManager.currentPluginKitVersion),
"minimumSystemVersion": "13.0", "url": "https://x", "sha256": "deadbeef"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ struct RegistryClientFreshnessTests {
"id": "com.test.freshness-driver",
"name": "Freshness Test Driver",
"databaseTypeIds": ["\(Self.testTypeId)"],
"exportFormatIds": [],
"releases": [{
"version": "0.82.0", "build": 820099, "channel": "stable", "pluginKit": \(kit),
"minimumSystemVersion": "13.0", "url": "https://x", "sha256": "deadbeef"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ struct RegistryReleaseSelectionTests {
RegistryPlugin(
id: "com.example.driver",
name: "Example Driver",
iconName: nil,
databaseTypeIds: ["Example"],
exportFormatIds: nil,
exportFormatIds: [],
releases: releases
)
}
Expand Down Expand Up @@ -72,7 +71,7 @@ struct RegistryReleaseSelectionTests {
func unknownChannelSkipped() throws {
let json = """
{"schemaVersion": 3, "retainFromPluginKit": 39, "plugins": [{
"id": "com.example.driver", "name": "Example Driver", "databaseTypeIds": ["Example"],
"id": "com.example.driver", "name": "Example Driver", "databaseTypeIds": ["Example"], "exportFormatIds": [],
"releases": [
{"version": "0.83.0-nightly.1", "build": 830050, "channel": "nightly", "pluginKit": 39,
"minimumSystemVersion": "13.0", "url": "https://example.com/n.zip", "sha256": "\(String(repeating: "b", count: 64))"},
Expand Down
1 change: 1 addition & 0 deletions changelog.d/3515.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `com.TablePro.customRegistryURL` setting for a custom plugin registry.
Loading