diff --git a/Account/Package.swift b/Account/Package.swift index d1bddac2e..85712e46a 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -19,7 +19,8 @@ let package = Package( .package(path: "../Database"), .package(path: "../SyncDatabase"), .package(path: "../Core"), - .package(path: "../CloudKitExtras") + .package(path: "../CloudKitExtras"), + .package(path: "../ReaderAPI") ], targets: [ .target( @@ -33,7 +34,8 @@ let package = Package( "SyncDatabase", "Database", "Core", - "CloudKitExtras" + "CloudKitExtras", + "ReaderAPI" ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 6cf0ac367..dd0e9389d 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -14,6 +14,7 @@ import os.log import Secrets import Database import Core +import ReaderAPI public enum ReaderAPIAccountDelegateError: LocalizedError { case unknown @@ -815,22 +816,23 @@ private extension ReaderAPIAccountDelegate { func mapEntriesToParsedItems(account: Account, entries: [ReaderAPIEntry]?) -> Set { - guard let entries = entries else { + guard let entries else { return Set() } - let parsedItems: [ParsedItem] = entries.compactMap { entry in - guard let streamID = entry.origin.streamId else { - return nil - } + let entriesWithOriginStreamIDs = entries.filter { $0.origin.streamId != nil } - var authors: Set? { + let parsedItems: [ParsedItem] = entries.map { entry in + + let streamID = entry.origin.streamId! + + let authors: Set? = { guard let name = entry.author else { return nil } return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)]) - } - + }() + return ParsedItem(syncServiceID: entry.uniqueID(variant: variant), uniqueID: entry.uniqueID(variant: variant), feedURL: streamID, @@ -851,7 +853,6 @@ private extension ReaderAPIAccountDelegate { } return Set(parsedItems) - } func syncArticleReadState(account: Account, articleIDs: [String]?) async throws { diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index 5a824b8b8..97a73dbce 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -9,6 +9,7 @@ import Foundation import Web import Secrets +import ReaderAPI enum CreateReaderAPISubscriptionResult { case created(ReaderAPISubscription) diff --git a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift index 43896377f..9d62d5bbe 100644 --- a/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift @@ -10,6 +10,7 @@ import AppKit import Account import Web import Secrets +import ReaderAPI class AccountsReaderAPIWindowController: NSWindowController { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index c3dffdad5..bcdfdefd2 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -579,6 +579,8 @@ 840958632201629A002C1579 /* Subscribe to Feed.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840BEE4021D70E64009BBAFA /* CrashReportWindowController.swift */; }; 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840D617E2029031C009BC708 /* AppDelegate.swift */; }; + 8410C4A32BC1E27A00D4F799 /* ReaderAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 8410C4A22BC1E27A00D4F799 /* ReaderAPI */; }; + 8410C4A52BC1E28200D4F799 /* ReaderAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 8410C4A42BC1E28200D4F799 /* ReaderAPI */; }; 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */; }; 841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; }; 841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; }; @@ -1423,6 +1425,7 @@ 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRequestOperation.swift; sourceTree = ""; }; 84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = ""; }; 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = ""; }; + 84CC98D92BC1DD25006A05C9 /* ReaderAPI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ReaderAPI; sourceTree = ""; }; 84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = ""; }; 84DCA50D2BAB643700792720 /* FoundationExtras */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FoundationExtras; sourceTree = ""; }; @@ -1587,6 +1590,7 @@ 84DCA5202BABB7A200792720 /* UIKitExtras in Frameworks */, 179D280B26F6F93D003B2E0A /* Zip in Frameworks */, 84DCA51E2BABB79900792720 /* FoundationExtras in Frameworks */, + 8410C4A52BC1E28200D4F799 /* ReaderAPI in Frameworks */, 84C1A8582BBBA5BD006E3E96 /* Web in Frameworks */, 516B695F24D2F33B00B5702F /* Account in Frameworks */, 845611742BBD145D00507B73 /* ParserObjC in Frameworks */, @@ -1613,6 +1617,7 @@ 5132775E2590FC640064F1E7 /* Articles in Frameworks */, 84DCA5252BABBB5A00792720 /* Core in Frameworks */, 8479ABE32B9E906E00F84C4D /* Database in Frameworks */, + 8410C4A32BC1E27A00D4F799 /* ReaderAPI in Frameworks */, 84DCA5122BABB75600792720 /* FoundationExtras in Frameworks */, 513277612590FC640064F1E7 /* ArticlesDatabase in Frameworks */, 51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */, @@ -2350,6 +2355,7 @@ 849C64611ED37A5D003D8FC0 /* Products */, 51C452B22265141B00C03939 /* Frameworks */, 51CD32C624D2DEF9009ABAEF /* Account */, + 84CC98D92BC1DD25006A05C9 /* ReaderAPI */, 51CD32C424D2CF1D009ABAEF /* Articles */, 51CD32C324D2CD57009ABAEF /* ArticlesDatabase */, 51CD32C724D2E06C009ABAEF /* Secrets */, @@ -2968,6 +2974,7 @@ 84C1A8572BBBA5BD006E3E96 /* Web */, 845611702BBD145D00507B73 /* Parser */, 845611732BBD145D00507B73 /* ParserObjC */, + 8410C4A42BC1E28200D4F799 /* ReaderAPI */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -3014,6 +3021,7 @@ 841CECD72BAD04B20001EE72 /* Tree */, 8456116A2BBD145200507B73 /* Parser */, 8456116D2BBD145200507B73 /* ParserObjC */, + 8410C4A22BC1E27A00D4F799 /* ReaderAPI */, ); productName = NetNewsWire; productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */; @@ -4863,6 +4871,14 @@ isa = XCSwiftPackageProductDependency; productName = Account; }; + 8410C4A22BC1E27A00D4F799 /* ReaderAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = ReaderAPI; + }; + 8410C4A42BC1E28200D4F799 /* ReaderAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = ReaderAPI; + }; 841CECD72BAD04B20001EE72 /* Tree */ = { isa = XCSwiftPackageProductDependency; productName = Tree; diff --git a/ReaderAPI/.gitignore b/ReaderAPI/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/ReaderAPI/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/ReaderAPI/Package.swift b/ReaderAPI/Package.swift new file mode 100644 index 000000000..a13ea45ae --- /dev/null +++ b/ReaderAPI/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "ReaderAPI", + platforms: [.macOS(.v14), .iOS(.v17)], + products: [ + .library( + name: "ReaderAPI", + targets: ["ReaderAPI"]), + ], + dependencies: [ + .package(path: "../FoundationExtras") + ], + targets: [ + .target( + name: "ReaderAPI", + dependencies: ["FoundationExtras"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ] + ), + .testTarget( + name: "ReaderAPITests", + dependencies: ["ReaderAPI"]), + ] +) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPIEntry.swift similarity index 70% rename from Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPIEntry.swift index ebb391f8b..f7b99ed01 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIEntry.swift +++ b/ReaderAPI/Sources/ReaderAPI/ReaderAPIEntry.swift @@ -7,14 +7,13 @@ // import Foundation -import Parser -struct ReaderAPIEntryWrapper: Codable { - let id: String - let updated: Int - let entries: [ReaderAPIEntry] - - +public struct ReaderAPIEntryWrapper: Codable, Sendable { + + public let id: String + public let updated: Int + public let entries: [ReaderAPIEntry] + enum CodingKeys: String, CodingKey { case id = "id" case updated = "updated" @@ -46,20 +45,21 @@ struct ReaderAPIEntryWrapper: Codable { } } */ -struct ReaderAPIEntry: Codable { - let articleID: String - let title: String? - let author: String? +public struct ReaderAPIEntry: Codable, Sendable { - let publishedTimestamp: Double? - let crawledTimestamp: String? - let timestampUsec: String? - - let summary: ReaderAPIArticleSummary - let alternates: [ReaderAPIAlternateLocation]? - let categories: [String] - let origin: ReaderAPIEntryOrigin + public let articleID: String + public let title: String? + public let author: String? + + public let publishedTimestamp: Double? + public let crawledTimestamp: String? + public let timestampUsec: String? + + public let summary: ReaderAPIArticleSummary + public let alternates: [ReaderAPIAlternateLocation]? + public let categories: [String] + public let origin: ReaderAPIEntryOrigin enum CodingKeys: String, CodingKey { case articleID = "id" @@ -74,14 +74,14 @@ struct ReaderAPIEntry: Codable { case timestampUsec = "timestampUsec" } - func parseDatePublished() -> Date? { + public func parseDatePublished() -> Date? { guard let unixTime = publishedTimestamp else { return nil } return Date(timeIntervalSince1970: unixTime) } - func uniqueID(variant: ReaderAPIVariant) -> String { + public func uniqueID(variant: ReaderAPIVariant) -> String { // Should look something like "tag:google.com,2005:reader/item/00058b10ce338909" // REGEX feels heavy, I should be able to just split on / and take the last element @@ -103,25 +103,28 @@ struct ReaderAPIEntry: Codable { } -struct ReaderAPIArticleSummary: Codable { - let content: String? +public struct ReaderAPIArticleSummary: Codable, Sendable { + + public let content: String? enum CodingKeys: String, CodingKey { case content = "content" } } -struct ReaderAPIAlternateLocation: Codable { - let url: String? - +public struct ReaderAPIAlternateLocation: Codable, Sendable { + + public let url: String? + enum CodingKeys: String, CodingKey { case url = "href" } } -struct ReaderAPIEntryOrigin: Codable { - let streamId: String? - let title: String? +public struct ReaderAPIEntryOrigin: Codable, Sendable { + + public let streamId: String? + public let title: String? enum CodingKeys: String, CodingKey { case streamId = "streamId" diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPISubscription.swift similarity index 69% rename from Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPISubscription.swift index 9c76dc1ed..5296bef25 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift +++ b/ReaderAPI/Sources/ReaderAPI/ReaderAPISubscription.swift @@ -7,7 +7,7 @@ // import Foundation -import Parser +import FoundationExtras /* @@ -18,10 +18,11 @@ import Parser */ -struct ReaderAPIQuickAddResult: Codable { - let numResults: Int - let error: String? - let streamId: String? +public struct ReaderAPIQuickAddResult: Codable { + + public let numResults: Int + public let error: String? + public let streamId: String? enum CodingKeys: String, CodingKey { case numResults = "numResults" @@ -30,9 +31,10 @@ struct ReaderAPIQuickAddResult: Codable { } } -struct ReaderAPISubscriptionContainer: Codable { - let subscriptions: [ReaderAPISubscription] +public struct ReaderAPISubscriptionContainer: Codable, Sendable { + public let subscriptions: [ReaderAPISubscription] + enum CodingKeys: String, CodingKey { case subscriptions = "subscriptions" } @@ -54,13 +56,14 @@ struct ReaderAPISubscriptionContainer: Codable { } */ -struct ReaderAPISubscription: Codable { - let feedID: String - let name: String? - let categories: [ReaderAPICategory] - let feedURL: String? - let homePageURL: String? - let iconURL: String? +public struct ReaderAPISubscription: Codable, Sendable { + + public let feedID: String + public let name: String? + public let categories: [ReaderAPICategory] + public let feedURL: String? + public let homePageURL: String? + public let iconURL: String? enum CodingKeys: String, CodingKey { case feedID = "id" @@ -71,7 +74,7 @@ struct ReaderAPISubscription: Codable { case iconURL = "iconUrl" } - var url: String { + public var url: String { if let feedURL = feedURL { return feedURL } else { @@ -80,9 +83,10 @@ struct ReaderAPISubscription: Codable { } } -struct ReaderAPICategory: Codable { - let categoryId: String - let categoryLabel: String +public struct ReaderAPICategory: Codable, Sendable { + + public let categoryId: String + public let categoryLabel: String enum CodingKeys: String, CodingKey { case categoryId = "id" diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPITag.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPITag.swift similarity index 67% rename from Account/Sources/Account/ReaderAPI/ReaderAPITag.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPITag.swift index d64462701..ae765eb7e 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPITag.swift +++ b/ReaderAPI/Sources/ReaderAPI/ReaderAPITag.swift @@ -8,25 +8,26 @@ import Foundation -struct ReaderAPITagContainer: Codable { - let tags: [ReaderAPITag] - +public struct ReaderAPITagContainer: Codable, Sendable { + + public let tags: [ReaderAPITag] + enum CodingKeys: String, CodingKey { case tags = "tags" } } -struct ReaderAPITag: Codable { - - let tagID: String - let type: String? +public struct ReaderAPITag: Codable, Sendable { + public let tagID: String + public let type: String? + enum CodingKeys: String, CodingKey { case tagID = "id" case type = "type" } - var folderName: String? { + public var folderName: String? { guard let range = tagID.range(of: "/label/") else { return nil } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPITagging.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPITagging.swift similarity index 100% rename from Account/Sources/Account/ReaderAPI/ReaderAPITagging.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPITagging.swift diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPIUnreadEntry.swift similarity index 61% rename from Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPIUnreadEntry.swift index 4fe3fde89..7db725dc2 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift +++ b/ReaderAPI/Sources/ReaderAPI/ReaderAPIUnreadEntry.swift @@ -8,18 +8,20 @@ import Foundation -struct ReaderAPIReferenceWrapper: Codable { - let itemRefs: [ReaderAPIReference]? - let continuation: String? - +public struct ReaderAPIReferenceWrapper: Codable, Sendable { + + public let itemRefs: [ReaderAPIReference]? + public let continuation: String? + enum CodingKeys: String, CodingKey { case itemRefs = "itemRefs" case continuation = "continuation" } } -struct ReaderAPIReference: Codable { - let itemId: String? +public struct ReaderAPIReference: Codable, Sendable { + + public let itemId: String? enum CodingKeys: String, CodingKey { case itemId = "id" diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift b/ReaderAPI/Sources/ReaderAPI/ReaderAPIVariant.swift similarity index 100% rename from Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift rename to ReaderAPI/Sources/ReaderAPI/ReaderAPIVariant.swift diff --git a/ReaderAPI/Tests/ReaderAPITests/ReaderAPITests.swift b/ReaderAPI/Tests/ReaderAPITests/ReaderAPITests.swift new file mode 100644 index 000000000..977927415 --- /dev/null +++ b/ReaderAPI/Tests/ReaderAPITests/ReaderAPITests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import ReaderAPI + +final class ReaderAPITests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/iOS/Account/ReaderAPIAccountViewController.swift b/iOS/Account/ReaderAPIAccountViewController.swift index b34e889b7..aa73f88e2 100644 --- a/iOS/Account/ReaderAPIAccountViewController.swift +++ b/iOS/Account/ReaderAPIAccountViewController.swift @@ -11,6 +11,7 @@ import Account import Secrets import Web import SafariServices +import ReaderAPI class ReaderAPIAccountViewController: UITableViewController {