From 48f287bace55bbca77f9e2f079a91b120ec335c7 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Tue, 19 Sep 2023 04:19:30 +0300 Subject: [PATCH 01/23] test: invalid expansionState --- Data/OPML/category_invalidExpansionState.opml | 11 +++++++++++ Tests/SyndiKitTests/OPMLTests.swift | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 Data/OPML/category_invalidExpansionState.opml diff --git a/Data/OPML/category_invalidExpansionState.opml b/Data/OPML/category_invalidExpansionState.opml new file mode 100644 index 0000000..21e6ba6 --- /dev/null +++ b/Data/OPML/category_invalidExpansionState.opml @@ -0,0 +1,11 @@ + + + + Illustrating the category attribute + Mon, 31 Oct 2005 19:23:00 GMT + one, two, three + + + + + diff --git a/Tests/SyndiKitTests/OPMLTests.swift b/Tests/SyndiKitTests/OPMLTests.swift index d653963..b14aa8a 100644 --- a/Tests/SyndiKitTests/OPMLTests.swift +++ b/Tests/SyndiKitTests/OPMLTests.swift @@ -86,6 +86,24 @@ internal final class OPMLTests: XCTestCase { XCTAssertEqual(isBreakpointOutline?.isBreakpoint, true) } + internal func testInvalidExpansionStateType() throws { + XCTAssertThrowsError(try Content.opml["category_invalidExpansionState"]?.get()) { error in + guard case .typeMismatch(let type, let context) = error as? DecodingError else { + XCTFail("Expected typeMismatch error.") + return + } + + XCTAssertTrue(type is Int.Type) + XCTAssertTrue( + context.codingPath.contains( + where: { + $0.stringValue == OPML.Head.CodingKeys.expansionStates.stringValue + } + ) + ) + } + } + internal func testType() throws { var opml = try Content.opml["subscriptionList"]?.get() From d1ba1ea8aa4d760c50fa91e6d87c0fd096f0a925 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Thu, 21 Sep 2023 16:13:32 +0300 Subject: [PATCH 02/23] add: new Podcast of Phase 1 & Phase 2 --- .../Formats/Media/Podcast/PodcastChapters.swift | 11 +++++++++++ .../Media/{ => Podcast}/PodcastEpisode.swift | 0 .../Formats/Media/Podcast/PodcastFunding.swift | 15 +++++++++++++++ .../Formats/Media/Podcast/PodcastLocation.swift | 15 +++++++++++++++ .../Formats/Media/Podcast/PodcastLocked.swift | 13 +++++++++++++ .../Formats/Media/Podcast/PodcastSeason.swift | 13 +++++++++++++ .../Formats/Media/Podcast/PodcastSoundbite.swift | 15 +++++++++++++++ .../Formats/Media/Podcast/PodcastTranscript.swift | 15 +++++++++++++++ 8 files changed, 97 insertions(+) create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift rename Sources/SyndiKit/Formats/Media/{ => Podcast}/PodcastEpisode.swift (100%) create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift new file mode 100644 index 0000000..793b9f0 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct PodcastChapters: Codable { + public let url: URL + public let type: String + + enum CodingKeys: String, CodingKey { + case url + case type + } +} diff --git a/Sources/SyndiKit/Formats/Media/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift similarity index 100% rename from Sources/SyndiKit/Formats/Media/PodcastEpisode.swift rename to Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift new file mode 100644 index 0000000..a76312d --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PodcastFunding: Codable { + public let startTime: TimeInterval + public let duration: TimeInterval + + public let value: String? + + enum CodingKeys: String, CodingKey { + case startTime + case duration + + case value = "" + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift new file mode 100644 index 0000000..82bf336 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PodcastLocation: Codable { + public let geo: String + public let osm: String + + public let value: String + + enum CodingKeys: String, CodingKey { + case geo + case osm + + case value = "" + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift new file mode 100644 index 0000000..5f43095 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct PodcastLocked: Codable { + public let owner: String? + + public let value: String + + enum CodingKeys: String, CodingKey { + case owner + + case value = "" + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift new file mode 100644 index 0000000..2fcf432 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct PodcastSeason: Codable { + public let name: String? + + public let value: Int + + enum CodingKeys: String, CodingKey { + case name + + case value = "" + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift new file mode 100644 index 0000000..4e9a906 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PodcastSoundbite: Codable { + public let startTime: TimeInterval + public let duration: TimeInterval + + public let value: String? + + enum CodingKeys: String, CodingKey { + case startTime + case duration + + case value = "" + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift new file mode 100644 index 0000000..dd90759 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PodcastTranscript: Codable { + public let url: URL + public let type: String + public let language: String? + public let rel: String? + + enum CodingKeys: String, CodingKey { + case url + case type + case language + case rel + } +} From 32569990becea8e1a5d8dc3a6bbca24c9260b33e Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Fri, 22 Sep 2023 21:59:52 +0300 Subject: [PATCH 03/23] update: integrate podcast types into RSSChannel & RSSItem --- .../Formats/Feeds/RSS/RSSChannel.swift | 8 ++++ .../SyndiKit/Formats/Feeds/RSS/RSSItem.swift | 45 ++++++++++++++++--- .../Media/Podcast/PodcastChapters.swift | 1 + .../Media/Podcast/PodcastEpisode.swift | 2 +- .../Media/Podcast/PodcastFunding.swift | 13 +++--- .../Formats/Media/Podcast/PodcastLocked.swift | 3 +- .../Formats/Media/Podcast/PodcastPerson.swift | 18 +++++--- .../Formats/Media/Podcast/PodcastSeason.swift | 8 ++-- .../Media/Podcast/PodcastSoundbite.swift | 6 ++- .../Media/Podcast/PodcastTranscript.swift | 6 +++ Tests/SyndiKitTests/RSSCodedTests.swift | 10 ++--- 11 files changed, 86 insertions(+), 34 deletions(-) diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift index 331ba2b..5becd19 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift @@ -45,6 +45,11 @@ public struct RSSChannel: Codable { public let wpBaseSiteURL: URL? public let wpBaseBlogURL: URL? + public let podcastLocked: PodcastLocked? + public let podcastFundings: [PodcastFunding]? + public let podcastPersons: [PodcastPerson]? + + enum CodingKeys: String, CodingKey { case title case link @@ -65,6 +70,9 @@ public struct RSSChannel: Codable { case wpTags = "wp:tag" case wpBaseSiteURL = "wp:baseSiteUrl" case wpBaseBlogURL = "wp:baseBlogUrl" + case podcastLocked = "podcast:locked" + case podcastFundings = "podcast:funding" + case podcastPersons = "podcast:person" } } diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift index 18a6052..97d776b 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift @@ -18,7 +18,11 @@ public struct RSSItem: Codable { public let itunesExplicit: String? public let itunesDuration: iTunesDuration? public let itunesImage: iTunesImage? - public let podcastPerson: [PodcastPerson]? + public let podcastPersons: [PodcastPerson]? + public let podcastTranscripts: [PodcastTranscript]? + public let podcastChapters: PodcastChapters? + public let podcastSoundbites: [PodcastSoundbite]? + public let podcastSeason: PodcastSeason? public let enclosure: Enclosure? public let creators: [String] public let wpCommentStatus: CData? @@ -58,7 +62,11 @@ public struct RSSItem: Codable { itunesExplicit: String? = nil, itunesDuration: TimeInterval? = nil, itunesImage: iTunesImage? = nil, - podcastPerson: [PodcastPerson]? = nil, + podcastPersons: [PodcastPerson]? = nil, + podcastTranscripts: [PodcastTranscript]? = nil, + podcastChapters: PodcastChapters? = nil, + podcastSoundbites: [PodcastSoundbite]? = nil, + podcastSeason: PodcastSeason? = nil, enclosure: Enclosure? = nil, creators: [String] = [], wpCommentStatus: String? = nil, @@ -96,7 +104,11 @@ public struct RSSItem: Codable { self.itunesExplicit = itunesExplicit self.itunesDuration = itunesDuration.map(iTunesDuration.init) self.itunesImage = itunesImage - self.podcastPerson = podcastPerson + self.podcastPersons = podcastPersons + self.podcastTranscripts = podcastTranscripts + self.podcastChapters = podcastChapters + self.podcastSoundbites = podcastSoundbites + self.podcastSeason = podcastSeason self.enclosure = enclosure self.creators = creators self.wpCommentStatus = wpCommentStatus.map(CData.init) @@ -143,9 +155,26 @@ public struct RSSItem: Codable { ) itunesImage = try container.decodeIfPresent(iTunesImage.self, forKey: .itunesImage) - podcastPerson = try container.decodeIfPresent( + podcastPersons = try container.decodeIfPresent( [PodcastPerson].self, - forKey: .podcastPerson + forKey: .podcastPersons + ) + podcastTranscripts = try container.decodeIfPresent( + [PodcastTranscript].self, + forKey: .podcastTranscripts + ) + podcastChapters = try container.decodeIfPresent( + PodcastChapters.self, + forKey: .podcastChapters + ) + podcastSoundbites = try container.decodeIfPresent( + [PodcastSoundbite].self, + forKey: .podcastSoundbites + ) + + podcastSeason = try container.decodeIfPresent( + PodcastSeason.self, + forKey: .podcastSeason ) enclosure = try container.decodeIfPresent(Enclosure.self, forKey: .enclosure) @@ -231,7 +260,11 @@ public struct RSSItem: Codable { case itunesSubtitle = "itunes:subtitle" case itunesSummary = "itunes:summary" case itunesExplicit = "itunes:explicit" - case podcastPerson = "podcast:person" + case podcastPersons = "podcast:person" + case podcastTranscripts = "podcast:transcript" + case podcastChapters = "podcast:chapters" + case podcastSoundbites = "podcast:soundbite" + case podcastSeason = "podcast:season" case itunesDuration = "itunes:duration" case itunesImage = "itunes:image" case creators = "dc:creator" diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index 793b9f0..e9a72c6 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -1,5 +1,6 @@ import Foundation +/// public struct PodcastChapters: Codable { public let url: URL public let type: String diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift index 97bca65..b1ea63a 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift @@ -37,6 +37,6 @@ struct PodcastEpisodeProperties: PodcastEpisode { duration = rssItem.itunesDuration?.value image = rssItem.itunesImage self.enclosure = enclosure - people = rssItem.podcastPerson ?? [] + people = rssItem.podcastPersons ?? [] } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift index a76312d..e221e52 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift @@ -1,15 +1,12 @@ import Foundation +/// Support the show!g public struct PodcastFunding: Codable { - public let startTime: TimeInterval - public let duration: TimeInterval - - public let value: String? + public let url: URL + public let description: String? enum CodingKeys: String, CodingKey { - case startTime - case duration - - case value = "" + case url + case description = "" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 5f43095..b088ba6 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -1,13 +1,12 @@ import Foundation +/// no public struct PodcastLocked: Codable { public let owner: String? - public let value: String enum CodingKeys: String, CodingKey { case owner - case value = "" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 45494ab..567921d 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -1,18 +1,24 @@ import Foundation +/// Alice Brown public struct PodcastPerson: Codable { - public let email: String? - public let role: String - public let href: String + public let role: String? + public let group: String? + public let href: String? public let img: URL? - public let name: String + public let fullname: String enum CodingKeys: String, CodingKey { - case email case role + case group case href case img - case name = "" + case fullname = "" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift index 2fcf432..c0d67f5 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift @@ -1,13 +1,13 @@ import Foundation +/// 5 +/// 3 public struct PodcastSeason: Codable { public let name: String? - - public let value: Int + public let number: Int enum CodingKeys: String, CodingKey { case name - - case value = "" + case number = "" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift index 4e9a906..d699136 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift @@ -1,15 +1,17 @@ import Foundation +/// Why the Podcast Namespace Matters +/// public struct PodcastSoundbite: Codable { public let startTime: TimeInterval public let duration: TimeInterval - public let value: String? + public let title: String? enum CodingKeys: String, CodingKey { case startTime case duration - case value = "" + case title = "" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index dd90759..c2e9488 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -1,5 +1,11 @@ import Foundation +/// public struct PodcastTranscript: Codable { public let url: URL public let type: String diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 0faafe5..40abe84 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -260,7 +260,7 @@ public final class SyndiKitTests: XCTestCase { return } - XCTAssertNil(item.podcastPerson) + XCTAssertNil(item.podcastPersons) } func testEpisodesWithHostAndGuestPersons() { @@ -282,10 +282,10 @@ public final class SyndiKitTests: XCTestCase { XCTAssertFalse(items.isEmpty) for item in items { - let host = item.podcastPerson?.first(where: { $0.role.lowercased() == "host" }) + let host = item.podcastPersons?.first(where: { $0.role?.lowercased() == "host" }) XCTAssertNotNil(host) - XCTAssertEqual(host?.name, "Leo Dion") + XCTAssertEqual(host?.fullname, "Leo Dion") XCTAssertEqual(host?.href, "https://brightdigit.com") XCTAssertEqual( host?.img, @@ -293,10 +293,10 @@ public final class SyndiKitTests: XCTestCase { ) // Both podcasts have the same guest - let guest = item.podcastPerson?.first(where: { $0.role.lowercased() == "guest" }) + let guest = item.podcastPersons?.first(where: { $0.role?.lowercased() == "guest" }) XCTAssertNotNil(guest) - XCTAssertEqual(guest?.name, "CompileSwift") + XCTAssertEqual(guest?.fullname, "CompileSwift") XCTAssertEqual(guest?.href, "https://compileswift.com") XCTAssertEqual( guest?.img, From 1b013620e212be9f9b88ef20e8cc1be565b3c089 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Fri, 22 Sep 2023 22:57:16 +0300 Subject: [PATCH 04/23] resolve: review threads --- .../Formats/Media/Podcast/PodcastLocked.swift | 8 +++++++- .../Media/Podcast/PodcastTranscript.swift | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index b088ba6..64d105b 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -3,10 +3,16 @@ import Foundation /// no public struct PodcastLocked: Codable { public let owner: String? - public let value: String + public let value: Bool enum CodingKeys: String, CodingKey { case owner case value = "" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.owner = try container.decodeIfPresent(String.self, forKey: .owner) + self.value = try container.decode(String.self, forKey: .value) == "yes" + } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index c2e9488..d90914d 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -7,10 +7,23 @@ import Foundation /// rel="captions" /// /> public struct PodcastTranscript: Codable { + public enum TranscriptType: String, Codable { + case pain = "text/plain" + case html = "text/html" + case srt = "text/srt" + case vtt = "text/vtt" + case json = "application/json" + case subrip = "application/x-subrip" + } + + public enum TranscriptRel: String, Codable { + case rel = "captions" + } + public let url: URL - public let type: String + public let type: TranscriptType public let language: String? - public let rel: String? + public let rel: TranscriptRel? enum CodingKeys: String, CodingKey { case url From e78fc9c7b78847adca90a9c390d1a1e7cd5ed4c5 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Fri, 22 Sep 2023 22:58:35 +0300 Subject: [PATCH 05/23] update: PodcastLocation value to name --- Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index 82bf336..9383192 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -4,12 +4,12 @@ public struct PodcastLocation: Codable { public let geo: String public let osm: String - public let value: String + public let name: String enum CodingKeys: String, CodingKey { case geo case osm - case value = "" + case name = "" } } From 6ddd57a2d5e7b484941279b8d263355d36417601 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sat, 23 Sep 2023 00:04:09 +0300 Subject: [PATCH 06/23] update: PodcastPerson role as enum with supported values of Transistor.fm --- .../Formats/Media/Podcast/PodcastPerson.swift | 27 +++++++++++++- Tests/SyndiKitTests/RSSCodedTests.swift | 37 +++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 567921d..f883a88 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -7,9 +7,19 @@ import Foundation /// img="http://example.com/images/alicebrown.jpg" /// >Alice Brown public struct PodcastPerson: Codable { - public let role: String? + public enum PersonRole: String, Codable { + case guest + case host + case editor + case writer + case designer + case composer + case producer + } + + public let role: PersonRole? public let group: String? - public let href: String? + public let href: URL? public let img: URL? public let fullname: String @@ -21,4 +31,17 @@ public struct PodcastPerson: Codable { case img case fullname = "" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.role = try container.decodeIfPresent(PersonRole.self, forKey: .role) + self.group = try container.decodeIfPresent(String.self, forKey: .group) + self.fullname = try container.decode(String.self, forKey: .fullname) + + let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? "" + self.href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) + + let imgUrl = try container.decodeIfPresent(String.self, forKey: .img) ?? "" + self.img = imgUrl.isEmpty ? nil : URL(string: imgUrl) + } } diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 40abe84..207a428 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -176,6 +176,35 @@ public final class SyndiKitTests: XCTestCase { } } + func testChannelPodcastElements() { + guard let feed = try? Content.xmlFeeds["empowerapps-show-cdata_summary"]?.get() else { + XCTFail("Missing Podcast \(name)") + return + } + + guard let rss = feed as? RSSFeed else { + XCTFail("Wrong Type \(name)") + return + } + + XCTAssertEqual(rss.channel.podcastLocked?.owner, "leogdion@brightdigit.com") + XCTAssertEqual(rss.channel.podcastLocked?.value, false) + + XCTAssertEqual(rss.channel.podcastFundings?.count, 1) + + let funding = rss.channel.podcastFundings?[0] + XCTAssertEqual(funding?.description, "Support this podcast on Patreon") + XCTAssertEqual(funding?.url, URL(strict: "https://www.patreon.com/empowerappsshow")) + + XCTAssertEqual(rss.channel.podcastPersons?.count, 1) + + let person = rss.channel.podcastPersons?[0] + XCTAssertEqual(person?.fullname, "Leo Dion") + XCTAssertEqual(person?.role, .host) + XCTAssertEqual(person?.href, URL(strict: "https://brightdigit.com")) + XCTAssertEqual(person?.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg")) + } + func testPodcastEpisodes() { let missingEpisodes = ["it-guy": [76, 56, 45]] let podcasts = [ @@ -282,22 +311,22 @@ public final class SyndiKitTests: XCTestCase { XCTAssertFalse(items.isEmpty) for item in items { - let host = item.podcastPersons?.first(where: { $0.role?.lowercased() == "host" }) + let host = item.podcastPersons?.first(where: { $0.role == .host }) XCTAssertNotNil(host) XCTAssertEqual(host?.fullname, "Leo Dion") - XCTAssertEqual(host?.href, "https://brightdigit.com") + XCTAssertEqual(host?.href, URL(strict: "https://brightdigit.com")) XCTAssertEqual( host?.img, URL(string: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg") ) // Both podcasts have the same guest - let guest = item.podcastPersons?.first(where: { $0.role?.lowercased() == "guest" }) + let guest = item.podcastPersons?.first(where: { $0.role == .guest }) XCTAssertNotNil(guest) XCTAssertEqual(guest?.fullname, "CompileSwift") - XCTAssertEqual(guest?.href, "https://compileswift.com") + XCTAssertEqual(guest?.href, URL(strict: "https://compileswift.com")) XCTAssertEqual( guest?.img, URL(string: "https://images.transistor.fm/file/transistor/images/person/e36ebf22-69fa-4e4f-a79b-1348c4d39267/1668262451-image.jpg") From a6bce9aed8088a9313c3f16c02e230af2a43aad6 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sat, 23 Sep 2023 00:12:42 +0300 Subject: [PATCH 07/23] resolve: review comments --- .../Formats/Feeds/RSS/RSSChannel.swift | 6 ++-- .../SyndiKit/Formats/Feeds/RSS/RSSItem.swift | 12 ++++---- .../Media/Podcast/PodcastEpisode.swift | 2 +- .../Formats/Media/Podcast/PodcastLocked.swift | 6 ++-- .../Media/Podcast/PodcastTranscript.swift | 10 +++---- Tests/SyndiKitTests/RSSCodedTests.swift | 30 +++++++++---------- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift index 5becd19..7ab29d1 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift @@ -46,8 +46,8 @@ public struct RSSChannel: Codable { public let wpBaseBlogURL: URL? public let podcastLocked: PodcastLocked? - public let podcastFundings: [PodcastFunding]? - public let podcastPersons: [PodcastPerson]? + public let podcastFundings: [PodcastFunding] + public let podcastPeople: [PodcastPerson] enum CodingKeys: String, CodingKey { @@ -72,7 +72,7 @@ public struct RSSChannel: Codable { case wpBaseBlogURL = "wp:baseBlogUrl" case podcastLocked = "podcast:locked" case podcastFundings = "podcast:funding" - case podcastPersons = "podcast:person" + case podcastPeople = "podcast:person" } } diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift index 97d776b..91c1c9f 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift @@ -18,7 +18,7 @@ public struct RSSItem: Codable { public let itunesExplicit: String? public let itunesDuration: iTunesDuration? public let itunesImage: iTunesImage? - public let podcastPersons: [PodcastPerson]? + public let podcastPeople: [PodcastPerson]? public let podcastTranscripts: [PodcastTranscript]? public let podcastChapters: PodcastChapters? public let podcastSoundbites: [PodcastSoundbite]? @@ -62,7 +62,7 @@ public struct RSSItem: Codable { itunesExplicit: String? = nil, itunesDuration: TimeInterval? = nil, itunesImage: iTunesImage? = nil, - podcastPersons: [PodcastPerson]? = nil, + podcastPeople: [PodcastPerson]? = nil, podcastTranscripts: [PodcastTranscript]? = nil, podcastChapters: PodcastChapters? = nil, podcastSoundbites: [PodcastSoundbite]? = nil, @@ -104,7 +104,7 @@ public struct RSSItem: Codable { self.itunesExplicit = itunesExplicit self.itunesDuration = itunesDuration.map(iTunesDuration.init) self.itunesImage = itunesImage - self.podcastPersons = podcastPersons + self.podcastPeople = podcastPeople self.podcastTranscripts = podcastTranscripts self.podcastChapters = podcastChapters self.podcastSoundbites = podcastSoundbites @@ -155,9 +155,9 @@ public struct RSSItem: Codable { ) itunesImage = try container.decodeIfPresent(iTunesImage.self, forKey: .itunesImage) - podcastPersons = try container.decodeIfPresent( + podcastPeople = try container.decodeIfPresent( [PodcastPerson].self, - forKey: .podcastPersons + forKey: .podcastPeople ) podcastTranscripts = try container.decodeIfPresent( [PodcastTranscript].self, @@ -260,7 +260,7 @@ public struct RSSItem: Codable { case itunesSubtitle = "itunes:subtitle" case itunesSummary = "itunes:summary" case itunesExplicit = "itunes:explicit" - case podcastPersons = "podcast:person" + case podcastPeople = "podcast:person" case podcastTranscripts = "podcast:transcript" case podcastChapters = "podcast:chapters" case podcastSoundbites = "podcast:soundbite" diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift index b1ea63a..52c30ed 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift @@ -37,6 +37,6 @@ struct PodcastEpisodeProperties: PodcastEpisode { duration = rssItem.itunesDuration?.value image = rssItem.itunesImage self.enclosure = enclosure - people = rssItem.podcastPersons ?? [] + people = rssItem.podcastPeople ?? [] } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 64d105b..3d54c4f 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -3,16 +3,16 @@ import Foundation /// no public struct PodcastLocked: Codable { public let owner: String? - public let value: Bool + public let isLocked: Bool enum CodingKeys: String, CodingKey { case owner - case value = "" + case isLocked = "" } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.owner = try container.decodeIfPresent(String.self, forKey: .owner) - self.value = try container.decode(String.self, forKey: .value) == "yes" + self.isLocked = try container.decode(String.self, forKey: .isLocked).lowercased() == "yes" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index d90914d..c00a69f 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -7,7 +7,7 @@ import Foundation /// rel="captions" /// /> public struct PodcastTranscript: Codable { - public enum TranscriptType: String, Codable { + public enum MimeType: String, Codable { case pain = "text/plain" case html = "text/html" case srt = "text/srt" @@ -16,14 +16,14 @@ public struct PodcastTranscript: Codable { case subrip = "application/x-subrip" } - public enum TranscriptRel: String, Codable { - case rel = "captions" + public enum Relationship: String, Codable { + case captions } public let url: URL - public let type: TranscriptType + public let type: MimeType public let language: String? - public let rel: TranscriptRel? + public let rel: Relationship? enum CodingKeys: String, CodingKey { case url diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 207a428..f66ba58 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -188,21 +188,21 @@ public final class SyndiKitTests: XCTestCase { } XCTAssertEqual(rss.channel.podcastLocked?.owner, "leogdion@brightdigit.com") - XCTAssertEqual(rss.channel.podcastLocked?.value, false) - - XCTAssertEqual(rss.channel.podcastFundings?.count, 1) + XCTAssertEqual(rss.channel.podcastLocked?.isLocked, false) - let funding = rss.channel.podcastFundings?[0] - XCTAssertEqual(funding?.description, "Support this podcast on Patreon") - XCTAssertEqual(funding?.url, URL(strict: "https://www.patreon.com/empowerappsshow")) + XCTAssertEqual(rss.channel.podcastFundings.count, 1) - XCTAssertEqual(rss.channel.podcastPersons?.count, 1) + let funding = rss.channel.podcastFundings[0] + XCTAssertEqual(funding.description, "Support this podcast on Patreon") + XCTAssertEqual(funding.url, URL(strict: "https://www.patreon.com/empowerappsshow")) - let person = rss.channel.podcastPersons?[0] - XCTAssertEqual(person?.fullname, "Leo Dion") - XCTAssertEqual(person?.role, .host) - XCTAssertEqual(person?.href, URL(strict: "https://brightdigit.com")) - XCTAssertEqual(person?.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg")) + XCTAssertEqual(rss.channel.podcastPeople.count, 1) + + let person = rss.channel.podcastPeople[0] + XCTAssertEqual(person.fullname, "Leo Dion") + XCTAssertEqual(person.role, .host) + XCTAssertEqual(person.href, URL(strict: "https://brightdigit.com")) + XCTAssertEqual(person.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg")) } func testPodcastEpisodes() { @@ -289,7 +289,7 @@ public final class SyndiKitTests: XCTestCase { return } - XCTAssertNil(item.podcastPersons) + XCTAssertNil(item.podcastPeople) } func testEpisodesWithHostAndGuestPersons() { @@ -311,7 +311,7 @@ public final class SyndiKitTests: XCTestCase { XCTAssertFalse(items.isEmpty) for item in items { - let host = item.podcastPersons?.first(where: { $0.role == .host }) + let host = item.podcastPeople?.first(where: { $0.role == .host }) XCTAssertNotNil(host) XCTAssertEqual(host?.fullname, "Leo Dion") @@ -322,7 +322,7 @@ public final class SyndiKitTests: XCTestCase { ) // Both podcasts have the same guest - let guest = item.podcastPersons?.first(where: { $0.role == .guest }) + let guest = item.podcastPeople?.first(where: { $0.role == .guest }) XCTAssertNotNil(guest) XCTAssertEqual(guest?.fullname, "CompileSwift") From fcdf0ba0705d338430aba9ecd8eeca91aa6a140d Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sat, 23 Sep 2023 01:03:49 +0300 Subject: [PATCH 08/23] fix: all arrays should not be optional --- .../SyndiKit/Formats/Feeds/RSS/RSSItem.swift | 24 +++++++++---------- .../Media/Podcast/PodcastEpisode.swift | 2 +- .../Media/Wordpress/WordPressPost.swift | 2 +- Tests/SyndiKitTests/RSSCodedTests.swift | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift index 91c1c9f..647bebb 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift @@ -18,10 +18,10 @@ public struct RSSItem: Codable { public let itunesExplicit: String? public let itunesDuration: iTunesDuration? public let itunesImage: iTunesImage? - public let podcastPeople: [PodcastPerson]? - public let podcastTranscripts: [PodcastTranscript]? + public let podcastPeople: [PodcastPerson] + public let podcastTranscripts: [PodcastTranscript] public let podcastChapters: PodcastChapters? - public let podcastSoundbites: [PodcastSoundbite]? + public let podcastSoundbites: [PodcastSoundbite] public let podcastSeason: PodcastSeason? public let enclosure: Enclosure? public let creators: [String] @@ -39,7 +39,7 @@ public struct RSSItem: Codable { public let wpModifiedDateGMT: Date? public let wpPostName: CData? public let wpPostType: CData? - public let wpPostMeta: [WordPressElements.PostMeta]? + public let wpPostMeta: [WordPressElements.PostMeta] public let wpAttachmentURL: URL? public let mediaContent: AtomMedia? public let mediaThumbnail: AtomMedia? @@ -62,10 +62,10 @@ public struct RSSItem: Codable { itunesExplicit: String? = nil, itunesDuration: TimeInterval? = nil, itunesImage: iTunesImage? = nil, - podcastPeople: [PodcastPerson]? = nil, - podcastTranscripts: [PodcastTranscript]? = nil, + podcastPeople: [PodcastPerson] = [], + podcastTranscripts: [PodcastTranscript] = [], podcastChapters: PodcastChapters? = nil, - podcastSoundbites: [PodcastSoundbite]? = nil, + podcastSoundbites: [PodcastSoundbite] = [], podcastSeason: PodcastSeason? = nil, enclosure: Enclosure? = nil, creators: [String] = [], @@ -83,7 +83,7 @@ public struct RSSItem: Codable { wpModifiedDateGMT: Date? = nil, wpPostName: String? = nil, wpPostType: String? = nil, - wpPostMeta: [WordPressElements.PostMeta]? = nil, + wpPostMeta: [WordPressElements.PostMeta] = [], wpAttachmentURL: URL? = nil, mediaContent: AtomMedia? = nil, mediaThumbnail: AtomMedia? = nil @@ -158,11 +158,11 @@ public struct RSSItem: Codable { podcastPeople = try container.decodeIfPresent( [PodcastPerson].self, forKey: .podcastPeople - ) + ) ?? [] podcastTranscripts = try container.decodeIfPresent( [PodcastTranscript].self, forKey: .podcastTranscripts - ) + ) ?? [] podcastChapters = try container.decodeIfPresent( PodcastChapters.self, forKey: .podcastChapters @@ -170,7 +170,7 @@ public struct RSSItem: Codable { podcastSoundbites = try container.decodeIfPresent( [PodcastSoundbite].self, forKey: .podcastSoundbites - ) + ) ?? [] podcastSeason = try container.decodeIfPresent( PodcastSeason.self, @@ -232,7 +232,7 @@ public struct RSSItem: Codable { wpPostMeta = try container.decodeIfPresent( [WordPressElements.PostMeta].self, forKey: .wpPostMeta - ) + ) ?? [] wpCommentStatus = try container.decodeIfPresent(CData.self, forKey: .wpCommentStatus) wpPingStatus = try container.decodeIfPresent(CData.self, forKey: .wpPingStatus) wpStatus = try container.decodeIfPresent(CData.self, forKey: .wpStatus) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift index 52c30ed..637834d 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift @@ -37,6 +37,6 @@ struct PodcastEpisodeProperties: PodcastEpisode { duration = rssItem.itunesDuration?.value image = rssItem.itunesImage self.enclosure = enclosure - people = rssItem.podcastPeople ?? [] + people = rssItem.podcastPeople } } diff --git a/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost.swift b/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost.swift index dd4fc19..10daded 100644 --- a/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost.swift +++ b/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost.swift @@ -153,7 +153,7 @@ public extension WordPressPost { let title = item.title let link = item.link let categoryTerms = item.categoryTerms - let meta = item.wpPostMeta ?? [] + let meta = item.wpPostMeta let pubDate = item.pubDate let categoryDictionary = Dictionary(grouping: categoryTerms, by: { diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index f66ba58..d714cc5 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -311,7 +311,7 @@ public final class SyndiKitTests: XCTestCase { XCTAssertFalse(items.isEmpty) for item in items { - let host = item.podcastPeople?.first(where: { $0.role == .host }) + let host = item.podcastPeople.first(where: { $0.role == .host }) XCTAssertNotNil(host) XCTAssertEqual(host?.fullname, "Leo Dion") @@ -322,7 +322,7 @@ public final class SyndiKitTests: XCTestCase { ) // Both podcasts have the same guest - let guest = item.podcastPeople?.first(where: { $0.role == .guest }) + let guest = item.podcastPeople.first(where: { $0.role == .guest }) XCTAssertNotNil(guest) XCTAssertEqual(guest?.fullname, "CompileSwift") From a0c23d0d7cc610f29bf93743454e39bf96a7d36e Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sat, 23 Sep 2023 02:54:58 +0300 Subject: [PATCH 09/23] update: make WordPressPost.role insensitive and add unknown --- .../Formats/Media/Podcast/PodcastPerson.swift | 55 ++++++++++++++++++- Tests/SyndiKitTests/RSSCodedTests.swift | 2 +- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index f883a88..fd2e6a3 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -7,7 +7,7 @@ import Foundation /// img="http://example.com/images/alicebrown.jpg" /// >Alice Brown public struct PodcastPerson: Codable { - public enum PersonRole: String, Codable { + public enum PersonRole: Codable, Equatable { case guest case host case editor @@ -15,6 +15,51 @@ public struct PodcastPerson: Codable { case designer case composer case producer + + case unknown(String) + + public var rawValue: String { + switch self { + case .guest: return "guest" + case .host: return "host" + case .editor: return "editor" + case .writer: return "writer" + case .designer: return "designer" + case .composer: return "composer" + case .producer: return "producer" + case .unknown(let string): return "\(string)" + } + } + + public init(value: String) { + guard let role = PersonRole(rawValue: value.lowercased()) else { + self = Self.unknown(value) + return + } + + self = role + } + + public init?(rawValue: String) { + switch rawValue { + case Self.guest.rawValue: + self = .guest + case Self.host.rawValue: + self = .host + case Self.editor.rawValue: + self = .editor + case Self.writer.rawValue: + self = .writer + case Self.designer.rawValue: + self = .designer + case Self.composer.rawValue: + self = .composer + case Self.producer.rawValue: + self = .producer + default: + return nil + } + } } public let role: PersonRole? @@ -34,10 +79,16 @@ public struct PodcastPerson: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.role = try container.decodeIfPresent(PersonRole.self, forKey: .role) self.group = try container.decodeIfPresent(String.self, forKey: .group) self.fullname = try container.decode(String.self, forKey: .fullname) + if let roleStr = try container.decodeIfPresent(String.self, forKey: .role) { + self.role = PersonRole(value: roleStr) + } else { + self.role = nil + } + + let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? "" self.href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index d714cc5..9a71a82 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -289,7 +289,7 @@ public final class SyndiKitTests: XCTestCase { return } - XCTAssertNil(item.podcastPeople) + XCTAssertTrue(item.podcastPeople.isEmpty) } func testEpisodesWithHostAndGuestPersons() { From aeda2183b70c1637f4ab1e85437b8d71a52a2f73 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Sat, 23 Sep 2023 12:31:42 -0400 Subject: [PATCH 10/23] refactoring Role --- .../Media/Podcast/PodcastPerson+Role.swift | 80 +++++++++++++++++++ .../Formats/Media/Podcast/PodcastPerson.swift | 58 +------------- 2 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift new file mode 100644 index 0000000..8a26db4 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift @@ -0,0 +1,80 @@ +// +// File.swift +// +// +// Created by Leo Dion on 9/23/23. +// + +import Foundation + +extension PodcastPerson { + private enum KnownRole : String { + case guest + case host + case editor + case writer + case designer + case composer + case producer + + init?(caseInsensitive: String) { + self.init(rawValue: caseInsensitive.lowercased()) + } + + init?(role: Role) { + switch role { + + case .guest: self = .guest + case .host: self = .host + case .editor: self = .editor + case .writer: self = .writer + case .designer: self = .designer + case .composer: self = .composer + case .producer: self = .producer + case .unknown: return nil + } + } + } + + public enum Role: Codable, Equatable, RawRepresentable { + public var rawValue: String { + if let knownRole = KnownRole(role: self) { + return knownRole.rawValue + } else if case let .unknown(string) = self { + return string + } else { + fatalError() + } + } + + public init(caseInsensitive: String) { + if let knownRole = KnownRole(caseInsensitive: caseInsensitive) { + self = .init(knownRole: knownRole) + } else { + self = .unknown(caseInsensitive) + } + } + + private init(knownRole : KnownRole) { + self.init(rawValue: knownRole.rawValue)! + } + + public init?(rawValue: String) { + if let knownRole = KnownRole(rawValue: rawValue) { + self = .init(knownRole: knownRole) + } else { + self = .unknown(rawValue) + } + } + + + case guest + case host + case editor + case writer + case designer + case composer + case producer + case unknown(String) + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index fd2e6a3..28a1b3e 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -7,62 +7,8 @@ import Foundation /// img="http://example.com/images/alicebrown.jpg" /// >Alice Brown public struct PodcastPerson: Codable { - public enum PersonRole: Codable, Equatable { - case guest - case host - case editor - case writer - case designer - case composer - case producer - case unknown(String) - - public var rawValue: String { - switch self { - case .guest: return "guest" - case .host: return "host" - case .editor: return "editor" - case .writer: return "writer" - case .designer: return "designer" - case .composer: return "composer" - case .producer: return "producer" - case .unknown(let string): return "\(string)" - } - } - - public init(value: String) { - guard let role = PersonRole(rawValue: value.lowercased()) else { - self = Self.unknown(value) - return - } - - self = role - } - - public init?(rawValue: String) { - switch rawValue { - case Self.guest.rawValue: - self = .guest - case Self.host.rawValue: - self = .host - case Self.editor.rawValue: - self = .editor - case Self.writer.rawValue: - self = .writer - case Self.designer.rawValue: - self = .designer - case Self.composer.rawValue: - self = .composer - case Self.producer.rawValue: - self = .producer - default: - return nil - } - } - } - - public let role: PersonRole? + public let role: Role? public let group: String? public let href: URL? public let img: URL? @@ -83,7 +29,7 @@ public struct PodcastPerson: Codable { self.fullname = try container.decode(String.self, forKey: .fullname) if let roleStr = try container.decodeIfPresent(String.self, forKey: .role) { - self.role = PersonRole(value: roleStr) + self.role = Role(caseInsensitive: roleStr) } else { self.role = nil } From f4b52a232c2dbdbd1d1f9fbe9e1ea323cc0dd2a6 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sun, 24 Sep 2023 10:28:21 +0300 Subject: [PATCH 11/23] fix: Recursion happening Role.init?(rawValue: String) and Role.init(knownRole: KnownRole) --- .../Media/Podcast/PodcastPerson+Role.swift | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift index 8a26db4..b3ce7a9 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift @@ -23,27 +23,36 @@ extension PodcastPerson { init?(role: Role) { switch role { - - case .guest: self = .guest - case .host: self = .host - case .editor: self = .editor - case .writer: self = .writer - case .designer: self = .designer - case .composer: self = .composer - case .producer: self = .producer - case .unknown: return nil + + case .guest: self = .guest + case .host: self = .host + case .editor: self = .editor + case .writer: self = .writer + case .designer: self = .designer + case .composer: self = .composer + case .producer: self = .producer + case .unknown: return nil } } } public enum Role: Codable, Equatable, RawRepresentable { + case guest + case host + case editor + case writer + case designer + case composer + case producer + case unknown(String) + public var rawValue: String { if let knownRole = KnownRole(role: self) { return knownRole.rawValue } else if case let .unknown(string) = self { return string } else { - fatalError() + fatalError("This should never happen!") } } @@ -55,10 +64,6 @@ extension PodcastPerson { } } - private init(knownRole : KnownRole) { - self.init(rawValue: knownRole.rawValue)! - } - public init?(rawValue: String) { if let knownRole = KnownRole(rawValue: rawValue) { self = .init(knownRole: knownRole) @@ -66,15 +71,17 @@ extension PodcastPerson { self = .unknown(rawValue) } } - - - case guest - case host - case editor - case writer - case designer - case composer - case producer - case unknown(String) + + private init(knownRole: KnownRole) { + switch knownRole { + case .guest: self = .guest + case .host: self = .host + case .editor: self = .editor + case .writer: self = .writer + case .designer: self = .designer + case .composer: self = .composer + case .producer: self = .producer + } + } } } From 6771862ee910b06ca8556deda4b8cfd5a5503064 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sun, 24 Sep 2023 10:59:24 +0300 Subject: [PATCH 12/23] test: podcast elements of RSSChannel & RSSItem --- .../Formats/Feeds/RSS/RSSChannel.swift | 1 - .../Podcast/PodcastChapters+MimeType.swift | 55 ++++++++++++++ .../Media/Podcast/PodcastChapters.swift | 2 +- .../Media/Podcast/PodcastLocation.swift | 1 + .../Formats/Media/Podcast/PodcastLocked.swift | 4 +- .../Media/Podcast/PodcastPerson+Role.swift | 62 +++++++-------- .../Formats/Media/Podcast/PodcastPerson.swift | 14 ++-- .../Podcast/PodcastTranscript+MimeType.swift | 75 +++++++++++++++++++ .../Media/Podcast/PodcastTranscript.swift | 9 --- .../Formats/Media/Wordpress/WPCategory.swift | 8 +- .../Formats/Media/Wordpress/WPPostMeta.swift | 2 +- .../Formats/Media/Wordpress/WPTag.swift | 5 +- Sources/SyndiKit/Formats/OPML/OPMLHead.swift | 26 +++---- .../SyndiKit/Formats/OPML/OPMLOutline.swift | 32 ++++---- .../Content.ResultDictionary.swift | 6 +- Tests/SyndiKitTests/OPMLTests.swift | 12 +-- Tests/SyndiKitTests/RSSCodedTests.swift | 42 ++++++++++- .../SyndiKitTests/RSSItemCategoryTests.swift | 3 +- .../WordPressElementsTests.swift | 4 +- Tests/SyndiKitTests/WordpressTests.swift | 18 ++--- 20 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift index 7ab29d1..26a4327 100644 --- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift +++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift @@ -49,7 +49,6 @@ public struct RSSChannel: Codable { public let podcastFundings: [PodcastFunding] public let podcastPeople: [PodcastPerson] - enum CodingKeys: String, CodingKey { case title case link diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift new file mode 100644 index 0000000..b6c37ed --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift @@ -0,0 +1,55 @@ +import Foundation + +extension PodcastChapters { + private enum KnownMimeType: String, Codable { + case json = "application/json+chapters" + + init?(caseInsensitive: String) { + self.init(rawValue: caseInsensitive) + } + + init?(mimeType: MimeType) { + switch mimeType { + case .json: self = .json + case .unknown: return nil + } + } + } + + public enum MimeType: Codable, Equatable, RawRepresentable { + case json + case unknown(String) + + public var rawValue: String { + if let knownMimeType = KnownMimeType(mimeType: self) { + return knownMimeType.rawValue + } else if case let .unknown(string) = self { + return string + } else { + fatalError("This should never happen!") + } + } + + public init?(rawValue: String) { + if let knownMimeType = KnownMimeType(rawValue: rawValue) { + self = .init(knownMimeType: knownMimeType) + } else { + self = .unknown(rawValue) + } + } + + public init(caseInsensitive: String) { + if let knownMimeType = KnownMimeType(caseInsensitive: caseInsensitive) { + self = .init(knownMimeType: knownMimeType) + } else { + self = .unknown(caseInsensitive) + } + } + + private init(knownMimeType: KnownMimeType) { + switch knownMimeType { + case .json: self = .json + } + } + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index e9a72c6..8fdfcc2 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -3,7 +3,7 @@ import Foundation /// public struct PodcastChapters: Codable { public let url: URL - public let type: String + public let type: MimeType enum CodingKeys: String, CodingKey { case url diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index 9383192..fa7461c 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -1,5 +1,6 @@ import Foundation +/// Austin, TX public struct PodcastLocation: Codable { public let geo: String public let osm: String diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 3d54c4f..90afdcd 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -12,7 +12,7 @@ public struct PodcastLocked: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.owner = try container.decodeIfPresent(String.self, forKey: .owner) - self.isLocked = try container.decode(String.self, forKey: .isLocked).lowercased() == "yes" + owner = try container.decodeIfPresent(String.self, forKey: .owner) + isLocked = try container.decode(String.self, forKey: .isLocked).lowercased() == "yes" } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift index b3ce7a9..79efebc 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift @@ -1,14 +1,7 @@ -// -// File.swift -// -// -// Created by Leo Dion on 9/23/23. -// - import Foundation extension PodcastPerson { - private enum KnownRole : String { + private enum KnownRole: String { case guest case host case editor @@ -16,26 +9,25 @@ extension PodcastPerson { case designer case composer case producer - + init?(caseInsensitive: String) { self.init(rawValue: caseInsensitive.lowercased()) } - + init?(role: Role) { switch role { - - case .guest: self = .guest - case .host: self = .host - case .editor: self = .editor - case .writer: self = .writer - case .designer: self = .designer - case .composer: self = .composer - case .producer: self = .producer - case .unknown: return nil + case .guest: self = .guest + case .host: self = .host + case .editor: self = .editor + case .writer: self = .writer + case .designer: self = .designer + case .composer: self = .composer + case .producer: self = .producer + case .unknown: return nil } } } - + public enum Role: Codable, Equatable, RawRepresentable { case guest case host @@ -55,32 +47,32 @@ extension PodcastPerson { fatalError("This should never happen!") } } - - public init(caseInsensitive: String) { - if let knownRole = KnownRole(caseInsensitive: caseInsensitive) { + + public init?(rawValue: String) { + if let knownRole = KnownRole(rawValue: rawValue) { self = .init(knownRole: knownRole) } else { - self = .unknown(caseInsensitive) + self = .unknown(rawValue) } } - - public init?(rawValue: String) { - if let knownRole = KnownRole(rawValue: rawValue) { + + public init(caseInsensitive: String) { + if let knownRole = KnownRole(caseInsensitive: caseInsensitive) { self = .init(knownRole: knownRole) } else { - self = .unknown(rawValue) + self = .unknown(caseInsensitive) } } private init(knownRole: KnownRole) { switch knownRole { - case .guest: self = .guest - case .host: self = .host - case .editor: self = .editor - case .writer: self = .writer - case .designer: self = .designer - case .composer: self = .composer - case .producer: self = .producer + case .guest: self = .guest + case .host: self = .host + case .editor: self = .editor + case .writer: self = .writer + case .designer: self = .designer + case .composer: self = .composer + case .producer: self = .producer } } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 28a1b3e..68b6f72 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -7,7 +7,6 @@ import Foundation /// img="http://example.com/images/alicebrown.jpg" /// >Alice Brown public struct PodcastPerson: Codable { - public let role: Role? public let group: String? public let href: URL? @@ -25,20 +24,19 @@ public struct PodcastPerson: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.group = try container.decodeIfPresent(String.self, forKey: .group) - self.fullname = try container.decode(String.self, forKey: .fullname) + group = try container.decodeIfPresent(String.self, forKey: .group) + fullname = try container.decode(String.self, forKey: .fullname) if let roleStr = try container.decodeIfPresent(String.self, forKey: .role) { - self.role = Role(caseInsensitive: roleStr) + role = Role(caseInsensitive: roleStr) } else { - self.role = nil + role = nil } - let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? "" - self.href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) + href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) let imgUrl = try container.decodeIfPresent(String.self, forKey: .img) ?? "" - self.img = imgUrl.isEmpty ? nil : URL(string: imgUrl) + img = imgUrl.isEmpty ? nil : URL(string: imgUrl) } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift new file mode 100644 index 0000000..e3e9ec2 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift @@ -0,0 +1,75 @@ +import Foundation + +extension PodcastTranscript { + private enum KnownMimeType: String, Codable { + case plain = "text/plain" + case html = "text/html" + case srt = "text/srt" + case vtt = "text/vtt" + case json = "application/json" + case subrip = "application/x-subrip" + + init?(caseInsensitive: String) { + self.init(rawValue: caseInsensitive) + } + + init?(mimeType: MimeType) { + switch mimeType { + case .plain: self = .plain + case .html: self = .html + case .srt: self = .srt + case .vtt: self = .vtt + case .json: self = .json + case .subrip: self = .subrip + case .unknown: return nil + } + } + } + + public enum MimeType: Codable, Equatable, RawRepresentable { + case plain + case html + case srt + case vtt + case json + case subrip + case unknown(String) + + public var rawValue: String { + if let knownMimeType = KnownMimeType(mimeType: self) { + return knownMimeType.rawValue + } else if case let .unknown(string) = self { + return string + } else { + fatalError("This should never happen!") + } + } + + public init?(rawValue: String) { + if let knownMimeType = KnownMimeType(rawValue: rawValue) { + self = .init(knownMimeType: knownMimeType) + } else { + self = .unknown(rawValue) + } + } + + public init(caseInsensitive: String) { + if let knownMimeType = KnownMimeType(caseInsensitive: caseInsensitive) { + self = .init(knownMimeType: knownMimeType) + } else { + self = .unknown(caseInsensitive) + } + } + + private init(knownMimeType: KnownMimeType) { + switch knownMimeType { + case .plain: self = .plain + case .html: self = .html + case .srt: self = .srt + case .vtt: self = .vtt + case .json: self = .json + case .subrip: self = .subrip + } + } + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index c00a69f..4885393 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -7,15 +7,6 @@ import Foundation /// rel="captions" /// /> public struct PodcastTranscript: Codable { - public enum MimeType: String, Codable { - case pain = "text/plain" - case html = "text/html" - case srt = "text/srt" - case vtt = "text/vtt" - case json = "application/json" - case subrip = "application/x-subrip" - } - public enum Relationship: String, Codable { case captions } diff --git a/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift b/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift index e36a0bd..e516bc9 100644 --- a/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift +++ b/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift @@ -13,7 +13,7 @@ public extension WordPressElements { case parent = "wp:categoryParent" case name = "wp:catName" } - + public init(termID: Int, niceName: CData, parent: CData, name: String) { self.termID = termID self.niceName = niceName @@ -26,8 +26,8 @@ public extension WordPressElements { extension WordPressElements.Category: Equatable { public static func == (lhs: WordPressElements.Category, rhs: WordPressElements.Category) -> Bool { lhs.termID == rhs.termID - && lhs.niceName == rhs.niceName - && lhs.parent == rhs.parent - && lhs.name == rhs.name + && lhs.niceName == rhs.niceName + && lhs.parent == rhs.parent + && lhs.name == rhs.name } } diff --git a/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift b/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift index 80e34cd..c553381 100644 --- a/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift +++ b/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift @@ -20,6 +20,6 @@ public extension WordPressElements { extension WordPressElements.PostMeta: Equatable { public static func == (lhs: WordPressElements.PostMeta, rhs: WordPressElements.PostMeta) -> Bool { lhs.key == rhs.key - && lhs.value == rhs.value + && lhs.value == rhs.value } } diff --git a/Sources/SyndiKit/Formats/Media/Wordpress/WPTag.swift b/Sources/SyndiKit/Formats/Media/Wordpress/WPTag.swift index ffd5c85..85f574e 100644 --- a/Sources/SyndiKit/Formats/Media/Wordpress/WPTag.swift +++ b/Sources/SyndiKit/Formats/Media/Wordpress/WPTag.swift @@ -23,8 +23,7 @@ public extension WordPressElements { extension WordPressElements.Tag: Equatable { public static func == (lhs: WordPressElements.Tag, rhs: WordPressElements.Tag) -> Bool { lhs.termID == rhs.termID - && lhs.slug == rhs.slug - && lhs.name == rhs.name + && lhs.slug == rhs.slug + && lhs.name == rhs.name } } - diff --git a/Sources/SyndiKit/Formats/OPML/OPMLHead.swift b/Sources/SyndiKit/Formats/OPML/OPMLHead.swift index cc6db84..b5d995d 100644 --- a/Sources/SyndiKit/Formats/OPML/OPMLHead.swift +++ b/Sources/SyndiKit/Formats/OPML/OPMLHead.swift @@ -34,20 +34,20 @@ public extension OPML { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.title = try container.decodeIfPresent(String.self, forKey: .title) - self.dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated) - self.dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified) - self.ownerName = try container.decodeIfPresent(String.self, forKey: .ownerName) - self.ownerEmail = try container.decodeIfPresent(String.self, forKey: .ownerEmail) - self.ownerId = try container.decodeIfPresent(String.self, forKey: .ownerId) - self.docs = try container.decodeIfPresent(String.self, forKey: .docs) - self.vertScrollState = try container.decodeIfPresent(Int.self, forKey: .vertScrollState) - self.windowTop = try container.decodeIfPresent(Int.self, forKey: .windowTop) - self.windowLeft = try container.decodeIfPresent(Int.self, forKey: .windowLeft) - self.windowBottom = try container.decodeIfPresent(Int.self, forKey: .windowBottom) - self.windowRight = try container.decodeIfPresent(Int.self, forKey: .windowRight) + title = try container.decodeIfPresent(String.self, forKey: .title) + dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated) + dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified) + ownerName = try container.decodeIfPresent(String.self, forKey: .ownerName) + ownerEmail = try container.decodeIfPresent(String.self, forKey: .ownerEmail) + ownerId = try container.decodeIfPresent(String.self, forKey: .ownerId) + docs = try container.decodeIfPresent(String.self, forKey: .docs) + vertScrollState = try container.decodeIfPresent(Int.self, forKey: .vertScrollState) + windowTop = try container.decodeIfPresent(Int.self, forKey: .windowTop) + windowLeft = try container.decodeIfPresent(Int.self, forKey: .windowLeft) + windowBottom = try container.decodeIfPresent(Int.self, forKey: .windowBottom) + windowRight = try container.decodeIfPresent(Int.self, forKey: .windowRight) - self.expansionStates = try container + expansionStates = try container .decodeIfPresent(String.self, forKey: .expansionStates)? .components(separatedBy: ", ") .filter { $0.isEmpty == false } diff --git a/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift b/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift index 498ae43..1021fb4 100644 --- a/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift +++ b/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift @@ -39,22 +39,22 @@ public extension OPML { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.text = try container.decode(String.self, forKey: .text) - self.title = try container.decodeIfPresent(String.self, forKey: .title) - self.description = try container.decodeIfPresent(String.self, forKey: .description) - self.type = try container.decodeIfPresent(OutlineType.self, forKey: .type) - self.url = try container.decodeIfPresent(URL.self, forKey: .url) - self.htmlUrl = try container.decodeIfPresent(URL.self, forKey: .htmlUrl) - self.xmlUrl = try container.decodeIfPresent(URL.self, forKey: .xmlUrl) - self.language = try container.decodeIfPresent(String.self, forKey: .language) - self.created = try container.decodeIfPresent(String.self, forKey: .created) - self.isComment = try container.decodeIfPresent(Bool.self, forKey: .isComment) - self.isBreakpoint = try container.decodeIfPresent(Bool.self, forKey: .isBreakpoint) - self.version = try container.decodeIfPresent(String.self, forKey: .version) - - self.outlines = try container.decodeIfPresent([Outline].self, forKey: .outlines) - - self.categories = try container + text = try container.decode(String.self, forKey: .text) + title = try container.decodeIfPresent(String.self, forKey: .title) + description = try container.decodeIfPresent(String.self, forKey: .description) + type = try container.decodeIfPresent(OutlineType.self, forKey: .type) + url = try container.decodeIfPresent(URL.self, forKey: .url) + htmlUrl = try container.decodeIfPresent(URL.self, forKey: .htmlUrl) + xmlUrl = try container.decodeIfPresent(URL.self, forKey: .xmlUrl) + language = try container.decodeIfPresent(String.self, forKey: .language) + created = try container.decodeIfPresent(String.self, forKey: .created) + isComment = try container.decodeIfPresent(Bool.self, forKey: .isComment) + isBreakpoint = try container.decodeIfPresent(Bool.self, forKey: .isBreakpoint) + version = try container.decodeIfPresent(String.self, forKey: .version) + + outlines = try container.decodeIfPresent([Outline].self, forKey: .outlines) + + categories = try container .decodeIfPresent(String.self, forKey: .categories)? .components(separatedBy: ",") } diff --git a/Tests/SyndiKitTests/Content.ResultDictionary.swift b/Tests/SyndiKitTests/Content.ResultDictionary.swift index 2942f45..8232287 100644 --- a/Tests/SyndiKitTests/Content.ResultDictionary.swift +++ b/Tests/SyndiKitTests/Content.ResultDictionary.swift @@ -1,6 +1,6 @@ import Foundation -import XMLCoder @testable import SyndiKit +import XMLCoder enum Content { typealias ResultDictionary = [String: Result] @@ -26,7 +26,7 @@ enum Content { by: Self.synDecoder.decode(_:) ) static let jsonFeeds = try! Content.resultDictionaryFrom( - directoryURL: Directories.JSON, + directoryURL: Directories.JSON, by: Self.synDecoder.decode(_:) ) static let opml = try! Content.resultDictionaryFrom( @@ -42,6 +42,6 @@ enum Content { extension XMLDecoder { func decodeOPML(_ data: Data) throws -> OPML { - try self.decode(OPML.self, from: data) + try decode(OPML.self, from: data) } } diff --git a/Tests/SyndiKitTests/OPMLTests.swift b/Tests/SyndiKitTests/OPMLTests.swift index b14aa8a..70fff2c 100644 --- a/Tests/SyndiKitTests/OPMLTests.swift +++ b/Tests/SyndiKitTests/OPMLTests.swift @@ -31,14 +31,14 @@ internal final class OPMLTests: XCTestCase { let usOutline = opml?.body.outlines.first XCTAssertEqual(usOutline?.text, "United States") - + XCTAssertEqual(usOutline?.outlines?.count, 8) - + let farWestOutline = usOutline?.outlines?.first XCTAssertEqual(farWestOutline?.text, "Far West") XCTAssertEqual(farWestOutline?.outlines?.count, 6) - + let nevadaOutline = farWestOutline?.outlines?[3] XCTAssertEqual(nevadaOutline?.outlines?.count, 4) } @@ -74,7 +74,7 @@ internal final class OPMLTests: XCTestCase { XCTAssertEqual(opml?.head.expansionStates?.count, 3) XCTAssertEqual(opml?.head.expansionStates?[0], 1) XCTAssertEqual(opml?.head.expansionStates?[2], 4) - + XCTAssertEqual(opml?.body.outlines.count, 4) let isCommentOutline = opml?.body.outlines.first @@ -88,7 +88,7 @@ internal final class OPMLTests: XCTestCase { internal func testInvalidExpansionStateType() throws { XCTAssertThrowsError(try Content.opml["category_invalidExpansionState"]?.get()) { error in - guard case .typeMismatch(let type, let context) = error as? DecodingError else { + guard case let .typeMismatch(type, context) = error as? DecodingError else { XCTFail("Expected typeMismatch error.") return } @@ -122,7 +122,7 @@ internal final class OPMLTests: XCTestCase { where: { $0.text == "Florida" } ) - XCTAssertEqual(floridaOutline?.type,.include) + XCTAssertEqual(floridaOutline?.type, .include) XCTAssertNotNil(floridaOutline?.url) } } diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 9a71a82..e11cdcb 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -205,6 +205,46 @@ public final class SyndiKitTests: XCTestCase { XCTAssertEqual(person.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg")) } + func testItemPodcastElements() { + guard let feed = try? Content.xmlFeeds["empowerapps-show-cdata_summary"]?.get() else { + XCTFail("Missing Podcast \(name)") + return + } + + guard let rss = feed as? RSSFeed else { + XCTFail("Wrong Type \(name)") + return + } + + guard let item = rss.channel.items.first else { + XCTFail("Missing Item \(name)") + return + } + + let host = item.podcastPeople[0] + XCTAssertEqual(host.fullname, "Leo Dion") + XCTAssertEqual(host.role, .host) + XCTAssertEqual(host.href, URL(strict: "https://brightdigit.com")) + XCTAssertEqual(host.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/401f05b8-f63f-4b96-803f-c7ac9233b459/1664979700-image.jpg")) + + let guest = item.podcastPeople[1] + XCTAssertEqual(guest.fullname, "CompileSwift") + XCTAssertEqual(guest.role, .guest) + XCTAssertEqual(guest.href, URL(strict: "https://compileswift.com")) + XCTAssertEqual(guest.img, URL(strict: "https://images.transistor.fm/file/transistor/images/person/e36ebf22-69fa-4e4f-a79b-1348c4d39267/1668262451-image.jpg")) + + XCTAssertEqual(item.podcastTranscripts.count, 1) + + let transcript = item.podcastTranscripts[0] + XCTAssertEqual(transcript.url, URL(strict: "https://share.transistor.fm/s/336118a1/transcript.srt")!) + XCTAssertEqual(transcript.type, .srt) + XCTAssertEqual(transcript.rel, .captions) + + let chapters = item.podcastChapters + XCTAssertEqual(chapters?.url, URL(strict: "https://share.transistor.fm/s/336118a1/chapters.json")!) + XCTAssertEqual(chapters?.type, .json) + } + func testPodcastEpisodes() { let missingEpisodes = ["it-guy": [76, 56, 45]] let podcasts = [ @@ -284,7 +324,7 @@ public final class SyndiKitTests: XCTestCase { let itemTitle = "My Taylor Deep Dish Swift Heroes World Tour" - guard let item = rss.channel.items.first(where: { $0.title == itemTitle } ) else { + guard let item = rss.channel.items.first(where: { $0.title == itemTitle }) else { XCTFail("Expected to find episode of title: \(itemTitle)") return } diff --git a/Tests/SyndiKitTests/RSSItemCategoryTests.swift b/Tests/SyndiKitTests/RSSItemCategoryTests.swift index 94d4f70..50482dd 100644 --- a/Tests/SyndiKitTests/RSSItemCategoryTests.swift +++ b/Tests/SyndiKitTests/RSSItemCategoryTests.swift @@ -1,8 +1,7 @@ -import XCTest @testable import SyndiKit +import XCTest final class RSSItemCategoryTests: XCTestCase { - func testTwoEqualCategories() { let c1 = RSSItemCategory( value: "Top Menu", diff --git a/Tests/SyndiKitTests/WordPressElementsTests.swift b/Tests/SyndiKitTests/WordPressElementsTests.swift index 624607c..4b07a07 100644 --- a/Tests/SyndiKitTests/WordPressElementsTests.swift +++ b/Tests/SyndiKitTests/WordPressElementsTests.swift @@ -1,8 +1,7 @@ -import XCTest @testable import SyndiKit +import XCTest final class WordPressElementsTests: XCTestCase { - func testCategoryEquatable() { let c1 = WordPressElements.Category( termID: 1, @@ -50,5 +49,4 @@ final class WordPressElementsTests: XCTestCase { XCTAssertNotEqual(pm1, pm2) } - } diff --git a/Tests/SyndiKitTests/WordpressTests.swift b/Tests/SyndiKitTests/WordpressTests.swift index 55ce6a7..3682da0 100644 --- a/Tests/SyndiKitTests/WordpressTests.swift +++ b/Tests/SyndiKitTests/WordpressTests.swift @@ -3,18 +3,16 @@ import Foundation import XCTest final class WordpressTests: XCTestCase { - - static let baseSiteURLs : [String : URL] = [ - "articles" : URL(string: "https://brightdigit.com/")!, - "tutorials" : URL(string: "https://brightdigit.com/")! + static let baseSiteURLs: [String: URL] = [ + "articles": URL(string: "https://brightdigit.com/")!, + "tutorials": URL(string: "https://brightdigit.com/")! ] - - - static let baseBlogURLs : [String : URL] = [ - "articles" : URL(string: "https://brightdigit.com")!, - "tutorials" : URL(string: "https://learningswift.brightdigit.com")! + + static let baseBlogURLs: [String: URL] = [ + "articles": URL(string: "https://brightdigit.com")!, + "tutorials": URL(string: "https://learningswift.brightdigit.com")! ] - + func testDateDecoder() { let dateDecoder = DateFormatterDecoder.RSS.decoder let result = dateDecoder.decodeString("Fri, 06 Oct 2017 17:21:35 +0000") From 89744909105176730dfe4363bfdcd6bd005c073e Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Sun, 24 Sep 2023 11:03:58 +0300 Subject: [PATCH 13/23] fix: some lint-issues --- .../Media/Podcast/PodcastChapters+MimeType.swift | 2 +- .../Media/Podcast/PodcastTranscript+MimeType.swift | 10 +++++----- Tests/SyndiKitTests/Content.Directories.swift | 2 +- Tests/SyndiKitTests/Extensions/FileManager.swift | 9 +++------ Tests/SyndiKitTests/Extensions/Sequence.swift | 2 +- Tests/SyndiKitTests/Extensions/SiteCollection.swift | 2 +- Tests/SyndiKitTests/Extensions/String.swift | 2 +- Tests/SyndiKitTests/RSSCoded.Durations.swift | 2 +- 8 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift index b6c37ed..2031b02 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift @@ -10,7 +10,7 @@ extension PodcastChapters { init?(mimeType: MimeType) { switch mimeType { - case .json: self = .json + case .json: self = .json case .unknown: return nil } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift index e3e9ec2..55c59d8 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift @@ -15,11 +15,11 @@ extension PodcastTranscript { init?(mimeType: MimeType) { switch mimeType { - case .plain: self = .plain - case .html: self = .html - case .srt: self = .srt - case .vtt: self = .vtt - case .json: self = .json + case .plain: self = .plain + case .html: self = .html + case .srt: self = .srt + case .vtt: self = .vtt + case .json: self = .json case .subrip: self = .subrip case .unknown: return nil } diff --git a/Tests/SyndiKitTests/Content.Directories.swift b/Tests/SyndiKitTests/Content.Directories.swift index 23684de..24fd7eb 100644 --- a/Tests/SyndiKitTests/Content.Directories.swift +++ b/Tests/SyndiKitTests/Content.Directories.swift @@ -1,7 +1,7 @@ import Foundation @testable import SyndiKit -extension Content { +internal extension Content { enum Directories { static let data = URL(fileURLWithPath: #file) .deletingLastPathComponent() diff --git a/Tests/SyndiKitTests/Extensions/FileManager.swift b/Tests/SyndiKitTests/Extensions/FileManager.swift index 903ea5e..7e4951a 100644 --- a/Tests/SyndiKitTests/Extensions/FileManager.swift +++ b/Tests/SyndiKitTests/Extensions/FileManager.swift @@ -1,6 +1,6 @@ import Foundation -extension FileManager { +internal extension FileManager { func dataFromDirectory(at sourceURL: URL) throws -> [(String, Result)] { let urls = try contentsOfDirectory( at: sourceURL, @@ -8,10 +8,7 @@ extension FileManager { options: [] ) - return urls.mapPairResult { - try Data(contentsOf: $0) - }.map { - ($0.0.deletingPathExtension().lastPathComponent, $0.1) - } + return urls.mapPairResult { try Data(contentsOf: $0) } + .map { ($0.0.deletingPathExtension().lastPathComponent, $0.1) } } } diff --git a/Tests/SyndiKitTests/Extensions/Sequence.swift b/Tests/SyndiKitTests/Extensions/Sequence.swift index f81f980..85950ba 100644 --- a/Tests/SyndiKitTests/Extensions/Sequence.swift +++ b/Tests/SyndiKitTests/Extensions/Sequence.swift @@ -1,4 +1,4 @@ -extension Sequence { +internal extension Sequence { func mapPairResult( _ transform: @escaping (Element) throws -> Success ) -> [(Element, Result)] { diff --git a/Tests/SyndiKitTests/Extensions/SiteCollection.swift b/Tests/SyndiKitTests/Extensions/SiteCollection.swift index 4e1ed0a..2fde8e3 100644 --- a/Tests/SyndiKitTests/Extensions/SiteCollection.swift +++ b/Tests/SyndiKitTests/Extensions/SiteCollection.swift @@ -1,7 +1,7 @@ import Foundation @testable import SyndiKit -extension SiteCollection { +internal extension SiteCollection { init(contentsOf url: URL, using decoder: JSONDecoder = .init()) throws { let data = try Data(contentsOf: url) self = try decoder.decode(SiteCollection.self, from: data) diff --git a/Tests/SyndiKitTests/Extensions/String.swift b/Tests/SyndiKitTests/Extensions/String.swift index 7072bc6..6049af4 100644 --- a/Tests/SyndiKitTests/Extensions/String.swift +++ b/Tests/SyndiKitTests/Extensions/String.swift @@ -1,4 +1,4 @@ -extension String { +internal extension String { func trimAndNilIfEmpty() -> String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text diff --git a/Tests/SyndiKitTests/RSSCoded.Durations.swift b/Tests/SyndiKitTests/RSSCoded.Durations.swift index add5ad2..5dd1f0e 100644 --- a/Tests/SyndiKitTests/RSSCoded.Durations.swift +++ b/Tests/SyndiKitTests/RSSCoded.Durations.swift @@ -1,6 +1,6 @@ import Foundation -extension SyndiKitTests { +internal extension SyndiKitTests { static let durationSets: [String: [TimeInterval]] = [ "empowerapps-show": [ 2_746, From ed5151e4bb3e7db297eafe664cb69d967e171580 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Mon, 25 Sep 2023 09:52:16 +0300 Subject: [PATCH 14/23] test: UTF8EncodedURL, XMLStringInt --- Tests/SyndiKitTests/UTF8EncodedURLTests.swift | 22 ++++++++++++ Tests/SyndiKitTests/XMLStringIntTests.swift | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 Tests/SyndiKitTests/UTF8EncodedURLTests.swift create mode 100644 Tests/SyndiKitTests/XMLStringIntTests.swift diff --git a/Tests/SyndiKitTests/UTF8EncodedURLTests.swift b/Tests/SyndiKitTests/UTF8EncodedURLTests.swift new file mode 100644 index 0000000..e0d822c --- /dev/null +++ b/Tests/SyndiKitTests/UTF8EncodedURLTests.swift @@ -0,0 +1,22 @@ +@testable import SyndiKit +import XMLCoder +import XCTest + +internal final class UTF8EncodedURLTests: XCTestCase { + internal func testDecode() throws { + let expectedURL = URL(strict: "http://www.example.com/index.php")! + let urlStr = """ + "\(expectedURL)" + """ + + guard let data = urlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(urlStr)") + return + } + + let sut = try JSONDecoder().decode(UTF8EncodedURL.self, from: data) + + XCTAssertEqual(sut.value, expectedURL) + XCTAssertNil(sut.string) + } +} diff --git a/Tests/SyndiKitTests/XMLStringIntTests.swift b/Tests/SyndiKitTests/XMLStringIntTests.swift new file mode 100644 index 0000000..5b4b547 --- /dev/null +++ b/Tests/SyndiKitTests/XMLStringIntTests.swift @@ -0,0 +1,36 @@ +@testable import SyndiKit +import XMLCoder +import XCTest + +internal final class XMLStringIntTests: XCTestCase { + internal func testDecodeValidXMLValue() throws { + let expectedAge = 10 + let xmlStr = """ + \(expectedAge) + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(XMLStringInt.self, from: data) + + XCTAssertEqual(sut.value, expectedAge) + } + + internal func testDecodeInvalidXMLValue() throws { + let xmlStr = """ + invalid + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + XCTAssertThrowsError(try XMLDecoder().decode(XMLStringInt.self, from: data)) { error in + XCTAssertNotNil(error as? DecodingError) + } + } +} From ac187177781d136913089562df79647736d30a09 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 05:38:30 +0300 Subject: [PATCH 15/23] update: make geo,osm of PodcastLocation as custom types, update: swiftdoc for podcast elements --- Sources/SyndiKit/Character.swift | 14 +++ Sources/SyndiKit/Collection.swift | 7 ++ .../Podcast/PodcastChapters+MimeType.swift | 2 +- .../Media/Podcast/PodcastChapters.swift | 7 +- .../Media/Podcast/PodcastEpisode.swift | 2 + .../Media/Podcast/PodcastFunding.swift | 8 +- .../Media/Podcast/PodcastLocation.swift | 115 +++++++++++++++++- .../Formats/Media/Podcast/PodcastLocked.swift | 8 +- .../Media/Podcast/PodcastPerson+Role.swift | 2 +- .../Formats/Media/Podcast/PodcastPerson.swift | 13 +- .../Formats/Media/Podcast/PodcastSeason.swift | 10 +- .../Media/Podcast/PodcastSoundbite.swift | 16 ++- .../Podcast/PodcastTranscript+MimeType.swift | 2 +- .../Media/Podcast/PodcastTranscript.swift | 9 +- Sources/SyndiKit/Substring.SubSequence.swift | 17 +++ 15 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 Sources/SyndiKit/Character.swift create mode 100644 Sources/SyndiKit/Collection.swift create mode 100644 Sources/SyndiKit/Substring.SubSequence.swift diff --git a/Sources/SyndiKit/Character.swift b/Sources/SyndiKit/Character.swift new file mode 100644 index 0000000..17cc821 --- /dev/null +++ b/Sources/SyndiKit/Character.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Ahmed Shendy on 27/09/2023. +// + +import Foundation + +extension Character { + func asOsmType() -> PodcastLocation.OsmQuery.OsmType? { + .init(rawValue: String(self)) + } +} diff --git a/Sources/SyndiKit/Collection.swift b/Sources/SyndiKit/Collection.swift new file mode 100644 index 0000000..90b8c4f --- /dev/null +++ b/Sources/SyndiKit/Collection.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Collection { + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift index 2031b02..46a4d2b 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift @@ -26,7 +26,7 @@ extension PodcastChapters { } else if case let .unknown(string) = self { return string } else { - fatalError("This should never happen!") + fatalError("Type attribute of should either be `KnownMimeType`, or unknown!") } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index 8fdfcc2..c59c96a 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -1,6 +1,11 @@ import Foundation -/// +/// ```xml +/// +/// ``` public struct PodcastChapters: Codable { public let url: URL public let type: MimeType diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift index 637834d..5ae1403 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift @@ -1,4 +1,6 @@ import Foundation + + public protocol PodcastEpisode { var title: String? { get } var episode: Int? { get } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift index e221e52..b262097 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift @@ -1,6 +1,12 @@ import Foundation -/// Support the show!g +/// ```xml +/// +/// Support the show! +/// +/// ``` public struct PodcastFunding: Codable { public let url: URL public let description: String? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index fa7461c..bd43f8c 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -1,9 +1,56 @@ import Foundation -/// Austin, TX +/// ```xml +/// +/// Austin, TX +/// +/// ``` public struct PodcastLocation: Codable { - public let geo: String - public let osm: String + /// Examples: + /// - `geo:37.786971,-122.399677`, a simple latlon description. + /// - `geo:37.786971,-122.399677,250`, a latlon including a height of 250 meters above ground level. + /// - `geo:37.786971,-122.399677;u=350`, a latlon with an accuracy ('uncertainty') of 350 meters. + public struct GeoURI: Codable { + let latitude: Double + let longitude: Double + let altitude: Double? + let height: Int? + let accuracy: Double? + + public init(latitude: Double, longitude: Double, altitude: Double? = nil, height: Int? = nil, accuracy: Double? = nil) { + self.latitude = latitude + self.longitude = longitude + self.altitude = altitude + self.height = height + self.accuracy = accuracy + } + } + + /// Examples: + /// - The United States of America: `R148838` + /// - The Eiffel Tower in Paris: `W5013364` + /// - Paris, but - optionally - the revision made on 8 Jan 2021: `R7444#188` + public struct OsmQuery: Codable { + enum OsmType: String, Codable, CaseIterable { + case node = "N" + case way = "W" + case relation = "R" + + static func isValid(_ rawValue: String) -> Bool { + OsmType(rawValue: rawValue) != nil + } + } + + let id: Int + let type: OsmType + let revision: Int? + } + + public let geo: GeoURI + public let osm: OsmQuery public let name: String @@ -13,4 +60,66 @@ public struct PodcastLocation: Codable { case name = "" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + + + let geoStr = try container.decode(String.self, forKey: .geo) + + guard + let geoScheme = geoStr.split(separator: ":")[safe: 0], + geoScheme == "geo" else { + throw DecodingError.dataCorruptedError( + forKey: .geo, + in: container, + debugDescription: "Invalid prefix for geo attribute: \(geoStr)" + ) + } + guard let geoPath = geoStr.split(separator: ":")[safe: 1] else { + throw DecodingError.dataCorruptedError( + forKey: .geo, + in: container, + debugDescription: "Invalid path for geo attribute: \(geoStr)" + ) + } + guard + let geoCoords = geoPath.split(separator: ";")[safe: 0], + let latitude = geoCoords.split(separator: ",")[safe: 0]?.asDouble(), + let longitude = geoCoords.split(separator: ",")[safe: 1]?.asDouble() + else { + throw DecodingError.dataCorruptedError( + forKey: .geo, + in: container, + debugDescription: "Invalid coordinates for geo attribute: \(geoStr)" + ) + } + let altitude = geoCoords.split(separator: ",")[safe: 2]?.asDouble() + let height = geoCoords.split(separator: ",")[safe: 2]?.asExactInt() + let accuracy = geoPath.split(separator: ";")[safe: 1]? + .split(separator: "=")[safe: 1]? + .asDouble() + self.geo = .init(latitude: latitude, longitude: longitude, altitude: altitude, height: height, accuracy: accuracy) + + + var osmStr = try container.decode(String.self, forKey: .osm) + + guard let osmType = osmStr.removeFirst().asOsmType() else { + throw DecodingError.dataCorruptedError( + forKey: .osm, + in: container, + debugDescription: "Invalid type for osm attribute: \(osmStr)" + ) + } + guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asInt() else { + throw DecodingError.dataCorruptedError( + forKey: .osm, + in: container, + debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)" + ) + } + let osmRevision = osmStr.split(separator: "#")[safe: 1]?.asInt() + self.osm = .init(id: osmID, type: osmType, revision: osmRevision) + } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 90afdcd..04aeda2 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -1,6 +1,12 @@ import Foundation -/// no +/// ```xml +/// +/// no +/// +/// ``` public struct PodcastLocked: Codable { public let owner: String? public let isLocked: Bool diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift index 79efebc..d5fb6f6 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift @@ -44,7 +44,7 @@ extension PodcastPerson { } else if case let .unknown(string) = self { return string } else { - fatalError("This should never happen!") + fatalError("Role attribute of should either be a `KnownRole`, or unknown!") } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 68b6f72..70c3c5f 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -1,11 +1,14 @@ import Foundation +/// ```xml /// Alice Brown +/// group="writing" +/// role="guest" +/// href="https://www.wikipedia/alicebrown" +/// img="http://example.com/images/alicebrown.jpg" +/// > +/// Alice Brown +/// public struct PodcastPerson: Codable { public let role: Role? public let group: String? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift index c0d67f5..4f91193 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift @@ -1,7 +1,15 @@ import Foundation +/// ```xml /// 5 -/// 3 +/// ``` +/// ```xml +/// +/// 3 +/// +/// ``` public struct PodcastSeason: Codable { public let name: String? public let number: Int diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift index d699136..ab3698e 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift @@ -1,7 +1,19 @@ import Foundation -/// Why the Podcast Namespace Matters -/// +/// ```xml +/// +/// Why the Podcast Namespace Matters +/// +/// ``` +/// ```xml +/// +/// ``` public struct PodcastSoundbite: Codable { public let startTime: TimeInterval public let duration: TimeInterval diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift index 55c59d8..4f502cc 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift @@ -41,7 +41,7 @@ extension PodcastTranscript { } else if case let .unknown(string) = self { return string } else { - fatalError("This should never happen!") + fatalError("Type attribute of should either be a `KnownMimeType`, or unknown!") } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index 4885393..3838cb4 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -1,10 +1,11 @@ import Foundation +/// ```xml /// public struct PodcastTranscript: Codable { public enum Relationship: String, Codable { diff --git a/Sources/SyndiKit/Substring.SubSequence.swift b/Sources/SyndiKit/Substring.SubSequence.swift new file mode 100644 index 0000000..e770ecc --- /dev/null +++ b/Sources/SyndiKit/Substring.SubSequence.swift @@ -0,0 +1,17 @@ +import Foundation + +extension Substring.SubSequence { + func asDouble() -> Double? { + Double(self) + } + + func asInt() -> Int? { + return Int(self) + } + + func asExactInt() -> Int? { + guard let double = Double(self) else { return nil } + + return Int(exactly: double) + } +} From d17fe3275ac1c3aec1c35a82f117fee39df8097e Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 05:47:14 +0300 Subject: [PATCH 16/23] update: make all podcast elements as Equatable (except PodcastEpisode) --- .../Media/Podcast/PodcastChapters.swift | 2 +- .../Media/Podcast/PodcastFunding.swift | 2 +- .../Podcast/PodcastLocation+GeoURI.swift | 23 ++++++++++ .../Podcast/PodcastLocation+OsmQuery.swift | 23 ++++++++++ .../Media/Podcast/PodcastLocation.swift | 42 +------------------ .../Formats/Media/Podcast/PodcastLocked.swift | 2 +- .../Formats/Media/Podcast/PodcastPerson.swift | 2 +- .../Formats/Media/Podcast/PodcastSeason.swift | 2 +- .../Media/Podcast/PodcastSoundbite.swift | 2 +- .../Media/Podcast/PodcastTranscript.swift | 2 +- 10 files changed, 54 insertions(+), 48 deletions(-) create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift create mode 100644 Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index c59c96a..b25c5d4 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -6,7 +6,7 @@ import Foundation /// type="application/json+chapters" /// /> /// ``` -public struct PodcastChapters: Codable { +public struct PodcastChapters: Codable, Equatable { public let url: URL public let type: MimeType diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift index b262097..568bb12 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift @@ -7,7 +7,7 @@ import Foundation /// Support the show! /// /// ``` -public struct PodcastFunding: Codable { +public struct PodcastFunding: Codable, Equatable { public let url: URL public let description: String? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift new file mode 100644 index 0000000..23dc407 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift @@ -0,0 +1,23 @@ +import Foundation + +extension PodcastLocation { + /// Examples: + /// - `geo:37.786971,-122.399677`, a simple latlon description. + /// - `geo:37.786971,-122.399677,250`, a latlon including a height of 250 meters above ground level. + /// - `geo:37.786971,-122.399677;u=350`, a latlon with an accuracy ('uncertainty') of 350 meters. + public struct GeoURI: Codable, Equatable { + let latitude: Double + let longitude: Double + let altitude: Double? + let height: Int? + let accuracy: Double? + + public init(latitude: Double, longitude: Double, altitude: Double? = nil, height: Int? = nil, accuracy: Double? = nil) { + self.latitude = latitude + self.longitude = longitude + self.altitude = altitude + self.height = height + self.accuracy = accuracy + } + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift new file mode 100644 index 0000000..a353bc6 --- /dev/null +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift @@ -0,0 +1,23 @@ +import Foundation + +extension PodcastLocation { + /// Examples: + /// - The United States of America: `R148838` + /// - The Eiffel Tower in Paris: `W5013364` + /// - Paris, but - optionally - the revision made on 8 Jan 2021: `R7444#188` + public struct OsmQuery: Codable, Equatable { + enum OsmType: String, Codable, CaseIterable { + case node = "N" + case way = "W" + case relation = "R" + + static func isValid(_ rawValue: String) -> Bool { + OsmType(rawValue: rawValue) != nil + } + } + + let id: Int + let type: OsmType + let revision: Int? + } +} diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index bd43f8c..e377096 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -8,47 +8,7 @@ import Foundation /// Austin, TX /// /// ``` -public struct PodcastLocation: Codable { - /// Examples: - /// - `geo:37.786971,-122.399677`, a simple latlon description. - /// - `geo:37.786971,-122.399677,250`, a latlon including a height of 250 meters above ground level. - /// - `geo:37.786971,-122.399677;u=350`, a latlon with an accuracy ('uncertainty') of 350 meters. - public struct GeoURI: Codable { - let latitude: Double - let longitude: Double - let altitude: Double? - let height: Int? - let accuracy: Double? - - public init(latitude: Double, longitude: Double, altitude: Double? = nil, height: Int? = nil, accuracy: Double? = nil) { - self.latitude = latitude - self.longitude = longitude - self.altitude = altitude - self.height = height - self.accuracy = accuracy - } - } - - /// Examples: - /// - The United States of America: `R148838` - /// - The Eiffel Tower in Paris: `W5013364` - /// - Paris, but - optionally - the revision made on 8 Jan 2021: `R7444#188` - public struct OsmQuery: Codable { - enum OsmType: String, Codable, CaseIterable { - case node = "N" - case way = "W" - case relation = "R" - - static func isValid(_ rawValue: String) -> Bool { - OsmType(rawValue: rawValue) != nil - } - } - - let id: Int - let type: OsmType - let revision: Int? - } - +public struct PodcastLocation: Codable, Equatable { public let geo: GeoURI public let osm: OsmQuery diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 04aeda2..77553fe 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -7,7 +7,7 @@ import Foundation /// no /// /// ``` -public struct PodcastLocked: Codable { +public struct PodcastLocked: Codable, Equatable { public let owner: String? public let isLocked: Bool diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 70c3c5f..669ff78 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -9,7 +9,7 @@ import Foundation /// > /// Alice Brown /// -public struct PodcastPerson: Codable { +public struct PodcastPerson: Codable, Equatable { public let role: Role? public let group: String? public let href: URL? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift index 4f91193..e574c90 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift @@ -10,7 +10,7 @@ import Foundation /// 3 /// /// ``` -public struct PodcastSeason: Codable { +public struct PodcastSeason: Codable, Equatable { public let name: String? public let number: Int diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift index ab3698e..77953da 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift @@ -14,7 +14,7 @@ import Foundation /// duration="60.0" /// /> /// ``` -public struct PodcastSoundbite: Codable { +public struct PodcastSoundbite: Codable, Equatable { public let startTime: TimeInterval public let duration: TimeInterval diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index 3838cb4..66d414e 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -7,7 +7,7 @@ import Foundation /// language="es" /// rel="captions" /// /> -public struct PodcastTranscript: Codable { +public struct PodcastTranscript: Codable, Equatable { public enum Relationship: String, Codable { case captions } From 13ca305f9d9bdd4254cca03c6ac18af9d4fd31c1 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 07:03:50 +0300 Subject: [PATCH 17/23] add: tests to podcast elements to increase testcov for previous commit --- .../Podcast/PodcastLocation+GeoURI.swift | 4 +- .../Media/Podcast/PodcastLocation.swift | 5 +- Sources/SyndiKit/Substring.SubSequence.swift | 4 +- Tests/SyndiKitTests/RSSCodedTests.swift | 214 ++++++++++++++++++ 4 files changed, 220 insertions(+), 7 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift index 23dc407..8206434 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift @@ -8,14 +8,12 @@ extension PodcastLocation { public struct GeoURI: Codable, Equatable { let latitude: Double let longitude: Double - let altitude: Double? let height: Int? let accuracy: Double? - public init(latitude: Double, longitude: Double, altitude: Double? = nil, height: Int? = nil, accuracy: Double? = nil) { + public init(latitude: Double, longitude: Double, height: Int? = nil, accuracy: Double? = nil) { self.latitude = latitude self.longitude = longitude - self.altitude = altitude self.height = height self.accuracy = accuracy } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index e377096..57d5ae4 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -55,12 +55,11 @@ public struct PodcastLocation: Codable, Equatable { debugDescription: "Invalid coordinates for geo attribute: \(geoStr)" ) } - let altitude = geoCoords.split(separator: ",")[safe: 2]?.asDouble() let height = geoCoords.split(separator: ",")[safe: 2]?.asExactInt() let accuracy = geoPath.split(separator: ";")[safe: 1]? .split(separator: "=")[safe: 1]? .asDouble() - self.geo = .init(latitude: latitude, longitude: longitude, altitude: altitude, height: height, accuracy: accuracy) + self.geo = .init(latitude: latitude, longitude: longitude, height: height, accuracy: accuracy) var osmStr = try container.decode(String.self, forKey: .osm) @@ -72,7 +71,7 @@ public struct PodcastLocation: Codable, Equatable { debugDescription: "Invalid type for osm attribute: \(osmStr)" ) } - guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asInt() else { + guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else { throw DecodingError.dataCorruptedError( forKey: .osm, in: container, diff --git a/Sources/SyndiKit/Substring.SubSequence.swift b/Sources/SyndiKit/Substring.SubSequence.swift index e770ecc..38eafa7 100644 --- a/Sources/SyndiKit/Substring.SubSequence.swift +++ b/Sources/SyndiKit/Substring.SubSequence.swift @@ -6,7 +6,9 @@ extension Substring.SubSequence { } func asInt() -> Int? { - return Int(self) + guard let double = Double(self) else { return nil } + + return Int(double) } func asExactInt() -> Int? { diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index e11cdcb..c5f2320 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -245,6 +245,220 @@ public final class SyndiKitTests: XCTestCase { XCTAssertEqual(chapters?.type, .json) } + func testPodcastPeopleUnknownRole() throws { + let expectedRole = "worker" + let xmlStr = """ + Alice Brown + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastPerson.self, from: data) + + XCTAssertEqual(sut.role, .unknown(expectedRole)) + } + + func testPodcastPeopleMissingRole() throws { + let xmlStr = """ + Alice Brown + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastPerson.self, from: data) + + XCTAssertNil(sut.role) + } + + func testPodcastChaptersUnknownMimeType() throws { + let expectedType = "yaml" + let xmlStr = """ + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastChapters.self, from: data) + + XCTAssertEqual(sut.type, .unknown(expectedType)) + } + + func testPodcastLocationOfTypeRelation() throws { + let expectedLatitude = 30.2672 + let expectedLongitude = 97.7431 + let expectedOsmType = "R" + let expectedOsmID = 113314 + let xmlStr = """ + + Austin, TX + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) + + XCTAssertEqual(sut.geo.latitude, expectedLatitude) + XCTAssertEqual(sut.geo.longitude, expectedLongitude) + XCTAssertEqual(sut.osm.id, expectedOsmID) + XCTAssertEqual(sut.osm.type, .relation) + XCTAssertNil(sut.osm.revision) + } + + func testPodcastLocationWithAccuracy() throws { + let expectedLatitude = 30.2672 + let expectedLongitude = 97.7431 + let expectedAccuracy = 350.0 + let xmlStr = """ + + Austin, TX + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) + + XCTAssertEqual(sut.geo.latitude, expectedLatitude) + XCTAssertEqual(sut.geo.longitude, expectedLongitude) + XCTAssertEqual(sut.geo.accuracy, expectedAccuracy) + XCTAssertNil(sut.geo.height) + } + + func testPodcastLocationWithHeight() throws { + let expectedLatitude = 30.2672 + let expectedLongitude = 97.7431 + let expectedHeight = 250 + let xmlStr = """ + + Austin, TX + + """ + + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) + + XCTAssertEqual(sut.geo.latitude, expectedLatitude) + XCTAssertEqual(sut.geo.longitude, expectedLongitude) + XCTAssertEqual(sut.geo.height, expectedHeight) + XCTAssertNil(sut.geo.accuracy) + } + + func testPodcastLocationWithInvalidGeoData() throws { + let missingGeoScheme = """ + Austin, TX + """ + + try assertInvalidGeoData(from: missingGeoScheme) + + let missingCoords = """ + Austin, TX + """ + try assertInvalidGeoData(from: missingCoords) + + let invalidCoords = """ + Austin, TX + """ + try assertInvalidGeoData(from: invalidCoords) + } + + private func assertInvalidGeoData(from xmlStr: String) throws { + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + XCTAssertThrowsError( + try XMLDecoder().decode(PodcastLocation.self, from: data) + ) { error in + assertPodcastLocationDecodingError(error, codingKey: .geo) + } + } + + func testPodcastLocationWithInvalidOsmData() throws { + let invalidOsmType = """ + Austin, TX + """ + + try assertInvalidOsmData(from: invalidOsmType) + + let invalidOsmID = """ + Austin, TX + """ + + try assertInvalidOsmData(from: invalidOsmID) + } + + private func assertInvalidOsmData(from xmlStr: String) throws { + guard let data = xmlStr.data(using: .utf8) else { + XCTFail("Expected data out of \(xmlStr)") + return + } + + XCTAssertThrowsError( + try XMLDecoder().decode(PodcastLocation.self, from: data) + ) { error in + assertPodcastLocationDecodingError(error, codingKey: .osm) + } + } + + private func assertPodcastLocationDecodingError(_ error: Error, codingKey: PodcastLocation.CodingKeys) { + guard + let decodingError = error as? DecodingError, + case let DecodingError.dataCorrupted(context) = decodingError else { + XCTFail() + return + } + + XCTAssertTrue( + context.codingPath.contains( + where: { $0.stringValue == codingKey.rawValue } + ) + ) + } + func testPodcastEpisodes() { let missingEpisodes = ["it-guy": [76, 56, 45]] let podcasts = [ From ee0efbd9c6f7192fbb4c9a1d11ed5ecf6df25565 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 07:10:02 +0300 Subject: [PATCH 18/23] update: PodcastChapters & PodcastTranscript with custom decoding for caseInsensitive enums --- .../Formats/Media/Podcast/PodcastChapters.swift | 8 ++++++++ .../Formats/Media/Podcast/PodcastPerson.swift | 12 ++++++------ .../Formats/Media/Podcast/PodcastTranscript.swift | 10 ++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index b25c5d4..ff82246 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -14,4 +14,12 @@ public struct PodcastChapters: Codable, Equatable { case url case type } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.url = try container.decode(URL.self, forKey: .url) + + let typeStr = try container.decode(String.self, forKey: .type) + self.type = MimeType(caseInsensitive: typeStr) + } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index 669ff78..bb79dca 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -27,19 +27,19 @@ public struct PodcastPerson: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - group = try container.decodeIfPresent(String.self, forKey: .group) - fullname = try container.decode(String.self, forKey: .fullname) + self.group = try container.decodeIfPresent(String.self, forKey: .group) + self.fullname = try container.decode(String.self, forKey: .fullname) if let roleStr = try container.decodeIfPresent(String.self, forKey: .role) { - role = Role(caseInsensitive: roleStr) + self.role = Role(caseInsensitive: roleStr) } else { - role = nil + self.role = nil } let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? "" - href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) + self.href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) let imgUrl = try container.decodeIfPresent(String.self, forKey: .img) ?? "" - img = imgUrl.isEmpty ? nil : URL(string: imgUrl) + self.img = imgUrl.isEmpty ? nil : URL(string: imgUrl) } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index 66d414e..48d9366 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -23,4 +23,14 @@ public struct PodcastTranscript: Codable, Equatable { case language case rel } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.url = try container.decode(URL.self, forKey: .url) + self.language = try container.decodeIfPresent(String.self, forKey: .language) + self.rel = try container.decodeIfPresent(Relationship.self, forKey: .rel) + + let typeStr = try container.decode(String.self, forKey: .type) + self.type = MimeType(caseInsensitive: typeStr) + } } From ec1f3c0e7776190e07ee64da2df3b7e6398ffe2e Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 16:39:16 +0300 Subject: [PATCH 19/23] update: move custom decoding of geo & osm into their own types --- .../Podcast/PodcastLocation+GeoURI.swift | 45 ++++++++++++++ .../Podcast/PodcastLocation+OsmQuery.swift | 28 +++++++++ .../Media/Podcast/PodcastLocation.swift | 58 +------------------ 3 files changed, 75 insertions(+), 56 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift index 8206434..d2603d1 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift @@ -17,5 +17,50 @@ extension PodcastLocation { self.height = height self.accuracy = accuracy } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let geoStr = try container.decode(String.self) + + guard + let geoScheme = geoStr.split(separator: ":")[safe: 0], + geoScheme == "geo" else { + throw DecodingError.dataCorrupted( + .init( + codingPath: [PodcastLocation.CodingKeys.geo], + debugDescription: "Invalid prefix for geo attribute: \(geoStr)" + ) + ) + } + guard let geoPath = geoStr.split(separator: ":")[safe: 1] else { + throw DecodingError.dataCorrupted( + .init( + codingPath: [PodcastLocation.CodingKeys.geo], + debugDescription: "Invalid path for geo attribute: \(geoStr)" + ) + ) + } + guard + let geoCoords = geoPath.split(separator: ";")[safe: 0], + let latitude = geoCoords.split(separator: ",")[safe: 0]?.asDouble(), + let longitude = geoCoords.split(separator: ",")[safe: 1]?.asDouble() + else { + throw DecodingError.dataCorrupted( + .init( + codingPath: [PodcastLocation.CodingKeys.geo], + debugDescription: "Invalid coordinates for geo attribute: \(geoStr)" + ) + ) + } + let height = geoCoords.split(separator: ",")[safe: 2]?.asExactInt() + let accuracy = geoPath.split(separator: ";")[safe: 1]? + .split(separator: "=")[safe: 1]? + .asDouble() + + self.latitude = latitude + self.longitude = longitude + self.height = height + self.accuracy = accuracy + } } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift index a353bc6..ddb93d7 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift @@ -19,5 +19,33 @@ extension PodcastLocation { let id: Int let type: OsmType let revision: Int? + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + var osmStr = try container.decode(String.self) + + guard let osmType = osmStr.removeFirst().asOsmType() else { + throw DecodingError.dataCorrupted( + .init( + codingPath: [PodcastLocation.CodingKeys.osm], + debugDescription: "Invalid type for osm attribute: \(osmStr)" + ) + ) + } + guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else { + throw DecodingError.dataCorrupted( + .init( + codingPath: [PodcastLocation.CodingKeys.osm], + debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)" + ) + ) + } + let osmRevision = osmStr.split(separator: "#")[safe: 1]?.asInt() + + self.id = osmID + self.type = osmType + self.revision = osmRevision + } } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index 57d5ae4..e66cf70 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -23,62 +23,8 @@ public struct PodcastLocation: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.geo = try container.decode(GeoURI.self, forKey: .geo) + self.osm = try container.decode(OsmQuery.self, forKey: .osm) self.name = try container.decode(String.self, forKey: .name) - - - let geoStr = try container.decode(String.self, forKey: .geo) - - guard - let geoScheme = geoStr.split(separator: ":")[safe: 0], - geoScheme == "geo" else { - throw DecodingError.dataCorruptedError( - forKey: .geo, - in: container, - debugDescription: "Invalid prefix for geo attribute: \(geoStr)" - ) - } - guard let geoPath = geoStr.split(separator: ":")[safe: 1] else { - throw DecodingError.dataCorruptedError( - forKey: .geo, - in: container, - debugDescription: "Invalid path for geo attribute: \(geoStr)" - ) - } - guard - let geoCoords = geoPath.split(separator: ";")[safe: 0], - let latitude = geoCoords.split(separator: ",")[safe: 0]?.asDouble(), - let longitude = geoCoords.split(separator: ",")[safe: 1]?.asDouble() - else { - throw DecodingError.dataCorruptedError( - forKey: .geo, - in: container, - debugDescription: "Invalid coordinates for geo attribute: \(geoStr)" - ) - } - let height = geoCoords.split(separator: ",")[safe: 2]?.asExactInt() - let accuracy = geoPath.split(separator: ";")[safe: 1]? - .split(separator: "=")[safe: 1]? - .asDouble() - self.geo = .init(latitude: latitude, longitude: longitude, height: height, accuracy: accuracy) - - - var osmStr = try container.decode(String.self, forKey: .osm) - - guard let osmType = osmStr.removeFirst().asOsmType() else { - throw DecodingError.dataCorruptedError( - forKey: .osm, - in: container, - debugDescription: "Invalid type for osm attribute: \(osmStr)" - ) - } - guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else { - throw DecodingError.dataCorruptedError( - forKey: .osm, - in: container, - debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)" - ) - } - let osmRevision = osmStr.split(separator: "#")[safe: 1]?.asInt() - self.osm = .init(id: osmID, type: osmType, revision: osmRevision) } } From 77e2bd29f4d1340b51a21804f418e4c740646ea4 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 18:37:37 +0300 Subject: [PATCH 20/23] update: osm to osmQuery, and remove all xml elements --- .../Formats/Media/Podcast/PodcastChapters.swift | 6 ------ .../Formats/Media/Podcast/PodcastEpisode.swift | 1 - .../Formats/Media/Podcast/PodcastFunding.swift | 7 ------- .../Media/Podcast/PodcastLocation+GeoURI.swift | 4 ---- .../Media/Podcast/PodcastLocation+OsmQuery.swift | 8 ++------ .../Formats/Media/Podcast/PodcastLocation.swift | 14 +++----------- .../Formats/Media/Podcast/PodcastLocked.swift | 7 ------- .../Formats/Media/Podcast/PodcastPerson.swift | 9 --------- .../Formats/Media/Podcast/PodcastSeason.swift | 10 ---------- .../Formats/Media/Podcast/PodcastSoundbite.swift | 14 -------------- .../Formats/Media/Podcast/PodcastTranscript.swift | 7 ------- Tests/SyndiKitTests/RSSCodedTests.swift | 8 ++++---- 12 files changed, 9 insertions(+), 86 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index ff82246..1460ea0 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -1,11 +1,5 @@ import Foundation -/// ```xml -/// -/// ``` public struct PodcastChapters: Codable, Equatable { public let url: URL public let type: MimeType diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift index 5ae1403..d5b3488 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift @@ -1,6 +1,5 @@ import Foundation - public protocol PodcastEpisode { var title: String? { get } var episode: Int? { get } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift index 568bb12..227ccb4 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift @@ -1,12 +1,5 @@ import Foundation -/// ```xml -/// -/// Support the show! -/// -/// ``` public struct PodcastFunding: Codable, Equatable { public let url: URL public let description: String? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift index d2603d1..d9ecbd3 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift @@ -1,10 +1,6 @@ import Foundation extension PodcastLocation { - /// Examples: - /// - `geo:37.786971,-122.399677`, a simple latlon description. - /// - `geo:37.786971,-122.399677,250`, a latlon including a height of 250 meters above ground level. - /// - `geo:37.786971,-122.399677;u=350`, a latlon with an accuracy ('uncertainty') of 350 meters. public struct GeoURI: Codable, Equatable { let latitude: Double let longitude: Double diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift index ddb93d7..44a48e1 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift @@ -1,10 +1,6 @@ import Foundation extension PodcastLocation { - /// Examples: - /// - The United States of America: `R148838` - /// - The Eiffel Tower in Paris: `W5013364` - /// - Paris, but - optionally - the revision made on 8 Jan 2021: `R7444#188` public struct OsmQuery: Codable, Equatable { enum OsmType: String, Codable, CaseIterable { case node = "N" @@ -28,7 +24,7 @@ extension PodcastLocation { guard let osmType = osmStr.removeFirst().asOsmType() else { throw DecodingError.dataCorrupted( .init( - codingPath: [PodcastLocation.CodingKeys.osm], + codingPath: [PodcastLocation.CodingKeys.osmQuery], debugDescription: "Invalid type for osm attribute: \(osmStr)" ) ) @@ -36,7 +32,7 @@ extension PodcastLocation { guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else { throw DecodingError.dataCorrupted( .init( - codingPath: [PodcastLocation.CodingKeys.osm], + codingPath: [PodcastLocation.CodingKeys.osmQuery], debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)" ) ) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index e66cf70..e047d9c 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -1,22 +1,14 @@ import Foundation -/// ```xml -/// -/// Austin, TX -/// -/// ``` public struct PodcastLocation: Codable, Equatable { public let geo: GeoURI - public let osm: OsmQuery + public let osmQuery: OsmQuery public let name: String enum CodingKeys: String, CodingKey { case geo - case osm + case osmQuery = "osm" case name = "" } @@ -24,7 +16,7 @@ public struct PodcastLocation: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.geo = try container.decode(GeoURI.self, forKey: .geo) - self.osm = try container.decode(OsmQuery.self, forKey: .osm) + self.osmQuery = try container.decode(OsmQuery.self, forKey: .osmQuery) self.name = try container.decode(String.self, forKey: .name) } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift index 77553fe..b35c891 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift @@ -1,12 +1,5 @@ import Foundation -/// ```xml -/// -/// no -/// -/// ``` public struct PodcastLocked: Codable, Equatable { public let owner: String? public let isLocked: Bool diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index bb79dca..e3baaa9 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -1,14 +1,5 @@ import Foundation -/// ```xml -/// -/// Alice Brown -/// public struct PodcastPerson: Codable, Equatable { public let role: Role? public let group: String? diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift index e574c90..e18ba77 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift @@ -1,15 +1,5 @@ import Foundation -/// ```xml -/// 5 -/// ``` -/// ```xml -/// -/// 3 -/// -/// ``` public struct PodcastSeason: Codable, Equatable { public let name: String? public let number: Int diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift index 77953da..3119f6a 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift @@ -1,19 +1,5 @@ import Foundation -/// ```xml -/// -/// Why the Podcast Namespace Matters -/// -/// ``` -/// ```xml -/// -/// ``` public struct PodcastSoundbite: Codable, Equatable { public let startTime: TimeInterval public let duration: TimeInterval diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index 48d9366..f79a592 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -1,12 +1,5 @@ import Foundation -/// ```xml -/// public struct PodcastTranscript: Codable, Equatable { public enum Relationship: String, Codable { case captions diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index c5f2320..24de225 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -329,9 +329,9 @@ public final class SyndiKitTests: XCTestCase { XCTAssertEqual(sut.geo.latitude, expectedLatitude) XCTAssertEqual(sut.geo.longitude, expectedLongitude) - XCTAssertEqual(sut.osm.id, expectedOsmID) - XCTAssertEqual(sut.osm.type, .relation) - XCTAssertNil(sut.osm.revision) + XCTAssertEqual(sut.osmQuery.id, expectedOsmID) + XCTAssertEqual(sut.osmQuery.type, .relation) + XCTAssertNil(sut.osmQuery.revision) } func testPodcastLocationWithAccuracy() throws { @@ -440,7 +440,7 @@ public final class SyndiKitTests: XCTestCase { XCTAssertThrowsError( try XMLDecoder().decode(PodcastLocation.self, from: data) ) { error in - assertPodcastLocationDecodingError(error, codingKey: .osm) + assertPodcastLocationDecodingError(error, codingKey: .osmQuery) } } From 0d148a60305a3b536f808038a541b1380c968a2f Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 19:12:42 +0300 Subject: [PATCH 21/23] update: make osmQuery, geo as optional --- .../Media/Podcast/PodcastLocation.swift | 8 +++--- Tests/SyndiKitTests/RSSCodedTests.swift | 26 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index e047d9c..b1b9614 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -1,8 +1,8 @@ import Foundation public struct PodcastLocation: Codable, Equatable { - public let geo: GeoURI - public let osmQuery: OsmQuery + public let geo: GeoURI? + public let osmQuery: OsmQuery? public let name: String @@ -15,8 +15,8 @@ public struct PodcastLocation: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.geo = try container.decode(GeoURI.self, forKey: .geo) - self.osmQuery = try container.decode(OsmQuery.self, forKey: .osmQuery) + self.geo = try container.decodeIfPresent(GeoURI.self, forKey: .geo) + self.osmQuery = try container.decodeIfPresent(OsmQuery.self, forKey: .osmQuery) self.name = try container.decode(String.self, forKey: .name) } } diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 24de225..1d889fb 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -327,11 +327,11 @@ public final class SyndiKitTests: XCTestCase { let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) - XCTAssertEqual(sut.geo.latitude, expectedLatitude) - XCTAssertEqual(sut.geo.longitude, expectedLongitude) - XCTAssertEqual(sut.osmQuery.id, expectedOsmID) - XCTAssertEqual(sut.osmQuery.type, .relation) - XCTAssertNil(sut.osmQuery.revision) + XCTAssertEqual(sut.geo?.latitude, expectedLatitude) + XCTAssertEqual(sut.geo?.longitude, expectedLongitude) + XCTAssertEqual(sut.osmQuery?.id, expectedOsmID) + XCTAssertEqual(sut.osmQuery?.type, .relation) + XCTAssertNil(sut.osmQuery?.revision) } func testPodcastLocationWithAccuracy() throws { @@ -354,10 +354,10 @@ public final class SyndiKitTests: XCTestCase { let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) - XCTAssertEqual(sut.geo.latitude, expectedLatitude) - XCTAssertEqual(sut.geo.longitude, expectedLongitude) - XCTAssertEqual(sut.geo.accuracy, expectedAccuracy) - XCTAssertNil(sut.geo.height) + XCTAssertEqual(sut.geo?.latitude, expectedLatitude) + XCTAssertEqual(sut.geo?.longitude, expectedLongitude) + XCTAssertEqual(sut.geo?.accuracy, expectedAccuracy) + XCTAssertNil(sut.geo?.height) } func testPodcastLocationWithHeight() throws { @@ -380,10 +380,10 @@ public final class SyndiKitTests: XCTestCase { let sut = try XMLDecoder().decode(PodcastLocation.self, from: data) - XCTAssertEqual(sut.geo.latitude, expectedLatitude) - XCTAssertEqual(sut.geo.longitude, expectedLongitude) - XCTAssertEqual(sut.geo.height, expectedHeight) - XCTAssertNil(sut.geo.accuracy) + XCTAssertEqual(sut.geo?.latitude, expectedLatitude) + XCTAssertEqual(sut.geo?.longitude, expectedLongitude) + XCTAssertEqual(sut.geo?.height, expectedHeight) + XCTAssertNil(sut.geo?.accuracy) } func testPodcastLocationWithInvalidGeoData() throws { From 47c037bd5582ce9ff6b73b3cec24c4c3eddb03f3 Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 22:35:36 +0300 Subject: [PATCH 22/23] resolve: review comments --- .../Formats/Media/Podcast/PodcastLocation+GeoURI.swift | 10 +++++----- .../Formats/Media/Podcast/PodcastLocation.swift | 7 ------- .../Formats/Media/Podcast/PodcastTranscript.swift | 10 ---------- Tests/SyndiKitTests/RSSCodedTests.swift | 10 +++++----- 4 files changed, 10 insertions(+), 27 deletions(-) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift index d9ecbd3..6b52f2b 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift @@ -4,13 +4,13 @@ extension PodcastLocation { public struct GeoURI: Codable, Equatable { let latitude: Double let longitude: Double - let height: Int? + let altitude: Double? let accuracy: Double? - public init(latitude: Double, longitude: Double, height: Int? = nil, accuracy: Double? = nil) { + public init(latitude: Double, longitude: Double, altitude: Double? = nil, accuracy: Double? = nil) { self.latitude = latitude self.longitude = longitude - self.height = height + self.altitude = altitude self.accuracy = accuracy } @@ -48,14 +48,14 @@ extension PodcastLocation { ) ) } - let height = geoCoords.split(separator: ",")[safe: 2]?.asExactInt() + let altitude = geoCoords.split(separator: ",")[safe: 2]?.asDouble() let accuracy = geoPath.split(separator: ";")[safe: 1]? .split(separator: "=")[safe: 1]? .asDouble() self.latitude = latitude self.longitude = longitude - self.height = height + self.altitude = altitude self.accuracy = accuracy } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift index b1b9614..df8c1c1 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift @@ -12,11 +12,4 @@ public struct PodcastLocation: Codable, Equatable { case name = "" } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.geo = try container.decodeIfPresent(GeoURI.self, forKey: .geo) - self.osmQuery = try container.decodeIfPresent(OsmQuery.self, forKey: .osmQuery) - self.name = try container.decode(String.self, forKey: .name) - } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift index f79a592..bde41ac 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift @@ -16,14 +16,4 @@ public struct PodcastTranscript: Codable, Equatable { case language case rel } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.url = try container.decode(URL.self, forKey: .url) - self.language = try container.decodeIfPresent(String.self, forKey: .language) - self.rel = try container.decodeIfPresent(Relationship.self, forKey: .rel) - - let typeStr = try container.decode(String.self, forKey: .type) - self.type = MimeType(caseInsensitive: typeStr) - } } diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift index 1d889fb..8e6f70b 100644 --- a/Tests/SyndiKitTests/RSSCodedTests.swift +++ b/Tests/SyndiKitTests/RSSCodedTests.swift @@ -357,16 +357,16 @@ public final class SyndiKitTests: XCTestCase { XCTAssertEqual(sut.geo?.latitude, expectedLatitude) XCTAssertEqual(sut.geo?.longitude, expectedLongitude) XCTAssertEqual(sut.geo?.accuracy, expectedAccuracy) - XCTAssertNil(sut.geo?.height) + XCTAssertNil(sut.geo?.altitude) } - func testPodcastLocationWithHeight() throws { + func testPodcastLocationWithAltitude() throws { let expectedLatitude = 30.2672 let expectedLongitude = 97.7431 - let expectedHeight = 250 + let expectedAltitude = 250.0 let xmlStr = """ Austin, TX @@ -382,7 +382,7 @@ public final class SyndiKitTests: XCTestCase { XCTAssertEqual(sut.geo?.latitude, expectedLatitude) XCTAssertEqual(sut.geo?.longitude, expectedLongitude) - XCTAssertEqual(sut.geo?.height, expectedHeight) + XCTAssertEqual(sut.geo?.altitude, expectedAltitude) XCTAssertNil(sut.geo?.accuracy) } From b913f810a541871ee77d8898a822cc354f31d0da Mon Sep 17 00:00:00 2001 From: Ahmed Shendy Date: Wed, 27 Sep 2023 22:57:33 +0300 Subject: [PATCH 23/23] fix: use default decoding for PodcastPerson+Role, PodcastTranscript+MimeType, PodcastChapters+MimeType --- Sources/SyndiKit/Character.swift | 7 ------- .../Formats/Media/Podcast/PodcastChapters+MimeType.swift | 6 +----- .../SyndiKit/Formats/Media/Podcast/PodcastChapters.swift | 8 -------- .../Formats/Media/Podcast/PodcastPerson+Role.swift | 6 +----- .../SyndiKit/Formats/Media/Podcast/PodcastPerson.swift | 7 +------ .../Media/Podcast/PodcastTranscript+MimeType.swift | 6 +----- 6 files changed, 4 insertions(+), 36 deletions(-) diff --git a/Sources/SyndiKit/Character.swift b/Sources/SyndiKit/Character.swift index 17cc821..6bbf343 100644 --- a/Sources/SyndiKit/Character.swift +++ b/Sources/SyndiKit/Character.swift @@ -1,10 +1,3 @@ -// -// File.swift -// -// -// Created by Ahmed Shendy on 27/09/2023. -// - import Foundation extension Character { diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift index 46a4d2b..66c29d4 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift @@ -31,11 +31,7 @@ extension PodcastChapters { } public init?(rawValue: String) { - if let knownMimeType = KnownMimeType(rawValue: rawValue) { - self = .init(knownMimeType: knownMimeType) - } else { - self = .unknown(rawValue) - } + self.init(caseInsensitive: rawValue) } public init(caseInsensitive: String) { diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift index 1460ea0..561012e 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift @@ -8,12 +8,4 @@ public struct PodcastChapters: Codable, Equatable { case url case type } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.url = try container.decode(URL.self, forKey: .url) - - let typeStr = try container.decode(String.self, forKey: .type) - self.type = MimeType(caseInsensitive: typeStr) - } } diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift index d5fb6f6..cefbba6 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift @@ -49,11 +49,7 @@ extension PodcastPerson { } public init?(rawValue: String) { - if let knownRole = KnownRole(rawValue: rawValue) { - self = .init(knownRole: knownRole) - } else { - self = .unknown(rawValue) - } + self.init(caseInsensitive: rawValue) } public init(caseInsensitive: String) { diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift index e3baaa9..0ed23f3 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift @@ -18,15 +18,10 @@ public struct PodcastPerson: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.role = try container.decodeIfPresent(Role.self, forKey: .role) self.group = try container.decodeIfPresent(String.self, forKey: .group) self.fullname = try container.decode(String.self, forKey: .fullname) - if let roleStr = try container.decodeIfPresent(String.self, forKey: .role) { - self.role = Role(caseInsensitive: roleStr) - } else { - self.role = nil - } - let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? "" self.href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl) diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift index 4f502cc..3e6263d 100644 --- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift +++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift @@ -46,11 +46,7 @@ extension PodcastTranscript { } public init?(rawValue: String) { - if let knownMimeType = KnownMimeType(rawValue: rawValue) { - self = .init(knownMimeType: knownMimeType) - } else { - self = .unknown(rawValue) - } + self.init(caseInsensitive: rawValue) } public init(caseInsensitive: String) {