From 22bec5da52678e24bf71fd6f9adb38df0281a440 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 1 Apr 2023 23:11:30 -0400 Subject: [PATCH] Plugins: Add option to use YAML for lists YAML is a modern configuration format which is human readable and supports more conventions than JSON. However, some people may still prefer to write in JSON, so add the option for users to write legacy JSON if they want to and to provide backwards compatability with older plugins. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 17 ++++++++++ Ferrite/Models/PluginModels.swift | 1 + Ferrite/ViewModels/PluginManager.swift | 44 +++++++++++++++++++++----- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 6f2e599..d7ebec3 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; }; 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; }; 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; + 0C748EDA29D9256D0049B8BE /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 0C748ED929D9256D0049B8BE /* Yams */; }; 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; }; 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; }; 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; @@ -299,6 +300,7 @@ 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, + 0C748EDA29D9256D0049B8BE /* Yams in Frameworks */, 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -688,6 +690,7 @@ 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0CDDDE042935235E006810B1 /* BetterSafariView */, 0C448BE829A135F100F4E266 /* Introspect-Static */, + 0C748ED929D9256D0049B8BE /* Yams */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -725,6 +728,7 @@ 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -1152,6 +1156,14 @@ kind = branch; }; }; + 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jpsim/Yams"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 5.0.5; + }; + }; 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; @@ -1199,6 +1211,11 @@ package = 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + 0C748ED929D9256D0049B8BE /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/Ferrite/Models/PluginModels.swift b/Ferrite/Models/PluginModels.swift index dfac824..83ae8a5 100644 --- a/Ferrite/Models/PluginModels.swift +++ b/Ferrite/Models/PluginModels.swift @@ -29,6 +29,7 @@ extension PluginManager { enum PluginManagerError: Error { case ListAddition(description: String) case ActionAddition(description: String) + case PluginFetch(description: String) } struct AvailablePlugins { diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 97036b2..dfa0b06 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import Yams public class PluginManager: ObservableObject { var logManager: LoggingManager? @@ -102,7 +103,18 @@ public class PluginManager: ObservableObject { let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) let (data, _) = try await URLSession.shared.data(for: request) - let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + let pluginResponse: PluginListJson? + + // If the URL is a yaml file, decode as such. Otherwise assume legacy JSON + if url.pathExtension == "yaml" || url.pathExtension == "yml" { + pluginResponse = try YAMLDecoder().decode(PluginListJson.self, from: data) + } else { + pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + } + + guard let pluginResponse else { + throw PluginManagerError.PluginFetch(description: "Could not decode plugin list data") + } if let sources = pluginResponse.sources { // Faster and more performant to map instead of a for loop @@ -748,25 +760,41 @@ public class PluginManager: ObservableObject { // Adds a plugin list // Can move this to PersistenceController if needed - public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws { + public func addPluginList(_ urlString: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws { let backgroundContext = PersistenceController.shared.backgroundContext - if url.isEmpty || !url.starts(with: "https://") && !url.starts(with: "http://") { + if urlString.isEmpty || !urlString.starts(with: "https://") && !urlString.starts(with: "http://") { throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.") } - let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!)) - let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + guard let url = URL(string: urlString) else { + throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.") + } + + let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url)) + + let rawResponse: PluginListJson? + + // If the URL is a yaml file, decode as such. Otherwise assume legacy JSON + if url.pathExtension == "yaml" || url.pathExtension == "yml" { + rawResponse = try YAMLDecoder().decode(PluginListJson.self, from: data) + } else { + rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + } + + guard let rawResponse else { + throw PluginManagerError.ListAddition(description: "Could not decode the plugin list from URL \(urlString)") + } if let existingPluginList { - existingPluginList.urlString = url + existingPluginList.urlString = urlString existingPluginList.name = rawResponse.name existingPluginList.author = rawResponse.author try PersistenceController.shared.container.viewContext.save() } else { let pluginListRequest = PluginList.fetchRequest() - let urlPredicate = NSPredicate(format: "urlString == %@", url) + let urlPredicate = NSPredicate(format: "urlString == %@", urlString) let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name) pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) pluginListRequest.fetchLimit = 1 @@ -779,7 +807,7 @@ public class PluginManager: ObservableObject { let newPluginList = PluginList(context: backgroundContext) newPluginList.id = UUID() - newPluginList.urlString = url + newPluginList.urlString = urlString newPluginList.name = rawResponse.name newPluginList.author = rawResponse.author