Create ReaderAPI module.

This commit is contained in:
Brent Simmons 2024-04-06 13:06:24 -07:00
parent 552753abd2
commit 5555ae5adc
15 changed files with 152 additions and 72 deletions

View File

@ -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")

View File

@ -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<ParsedItem> {
guard let entries = entries else {
guard let entries else {
return Set<ParsedItem>()
}
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<ParsedAuthor>? {
let parsedItems: [ParsedItem] = entries.map { entry in
let streamID = entry.origin.streamId!
let authors: Set<ParsedAuthor>? = {
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 {

View File

@ -9,6 +9,7 @@
import Foundation
import Web
import Secrets
import ReaderAPI
enum CreateReaderAPISubscriptionResult {
case created(ReaderAPISubscription)

View File

@ -10,6 +10,7 @@ import AppKit
import Account
import Web
import Secrets
import ReaderAPI
class AccountsReaderAPIWindowController: NSWindowController {

View File

@ -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 = "<group>"; };
84CBDDAE1FD3674C005A61AA /* Technotes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Technotes; sourceTree = "<group>"; };
84CC88171FE59CBF00644329 /* SmartFeedsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedsController.swift; sourceTree = "<group>"; };
84CC98D92BC1DD25006A05C9 /* ReaderAPI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ReaderAPI; sourceTree = "<group>"; };
84D2200922B0BC4B0019E085 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStatusBarView.swift; sourceTree = "<group>"; };
84DCA50D2BAB643700792720 /* FoundationExtras */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FoundationExtras; sourceTree = "<group>"; };
@ -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;

8
ReaderAPI/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

28
ReaderAPI/Package.swift Normal file
View File

@ -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"]),
]
)

View File

@ -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"

View File

@ -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"

View File

@ -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
}

View File

@ -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"

View File

@ -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
}
}

View File

@ -11,6 +11,7 @@ import Account
import Secrets
import Web
import SafariServices
import ReaderAPI
class ReaderAPIAccountViewController: UITableViewController {