diff --git a/.github/workflows/syndikit.yml b/.github/workflows/syndikit.yml
index 377e675..3f5fcec 100644
--- a/.github/workflows/syndikit.yml
+++ b/.github/workflows/syndikit.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
runs-on: [ubuntu-20.04, ubuntu-22.04]
- swift-version: [5.7.3, 5.8]
+ swift-version: [5.7.3, 5.8.1, 5.9]
include:
- runs-on: ubuntu-20.04
swift-version: 5.5.3
@@ -30,14 +30,13 @@ jobs:
id: cache-spm-linux
uses: actions/cache@v3
env:
- cache-name: cache-spm
+ cache-name: SPM
with:
path: .build
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Package.resolved') }}
+ key: ${{ env.cache-name }}-${{ runner.os }}-${{ env.SWIFT_VER }}-${{ hashFiles('Package.resolved') }}-${{ env.RELEASE_DOT }}
restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
+ ${{ env.cache-name }}-${{ runner.os }}-${{ env.SWIFT_VER }}-${{ hashFiles('Package.resolved') }}
+ ${{ env.cache-name }}-${{ runner.os }}-${{ env.SWIFT_VER }}
- name: Set Ubuntu Release DOT
run: echo "RELEASE_DOT=$(lsb_release -sr)" >> $GITHUB_ENV
- name: Set Ubuntu Release NUM
@@ -48,14 +47,10 @@ jobs:
id: cache-swift-linux
uses: actions/cache@v3
env:
- cache-name: cache-swift
+ cache-name: swift
with:
path: swift-${{ env.SWIFT_VER }}-RELEASE-ubuntu${{ env.RELEASE_DOT }}
- key: ${{ runner.os }}-${{ env.RELEASE_DOT }}-${{ env.cache-name }}-${{ env.SWIFT_VER }}
- restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
+ key: ${{ env.cache-name }}-${{ runner.os }}-${{ env.SWIFT_VER }}-${{ env.RELEASE_DOT }}
- name: Download Swift
if: steps.cache-swift-linux.outputs.cache-hit != 'true'
run: curl -O https://download.swift.org/swift-${SWIFT_VER}-release/ubuntu${RELEASE_NUM}/swift-${SWIFT_VER}-RELEASE/swift-${SWIFT_VER}-RELEASE-ubuntu${RELEASE_DOT}.tar.gz
@@ -71,7 +66,7 @@ jobs:
- name: Prepare Code Coverage
run: llvm-cov export -format="lcov" .build/x86_64-unknown-linux-gnu/debug/${{ env.PACKAGE_NAME }}PackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > info.lcov
- name: Upload to CodeCov.io
- run: bash <(curl https://codecov.io/bash) -F github -F ${RELEASE_NAME} -F ${SWIFT_VER}
+ run: bash <(curl https://codecov.io/bash) -F spm -F ${RELEASE_NAME} -F ${SWIFT_VER}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-macos:
@@ -114,11 +109,17 @@ jobs:
watchName: "Apple Watch Ultra (49mm)"
iPhoneName: "iPhone 14"
- runs-on: macos-13
- xcode: "/Applications/Xcode_14.3.app"
+ xcode: "/Applications/Xcode_14.3.1.app"
iOSVersion: "16.4"
watchOSVersion: "9.4"
watchName: "Apple Watch Ultra (49mm)"
iPhoneName: "iPhone 14 Pro Max"
+ - runs-on: macos-13
+ xcode: "/Applications/Xcode_15.0.app"
+ iOSVersion: "17.0"
+ watchOSVersion: "10.0"
+ watchName: "Apple Watch Ultra (49mm)"
+ iPhoneName: "iPhone 14 Pro Max"
steps:
- uses: actions/checkout@v3
- name: Cache swift package modules
@@ -134,7 +135,7 @@ jobs:
${{ runner.os }}-build-
${{ runner.os }}-
- name: Cache mint
- if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' && matrix.xcode == '/Applications/Xcode_14.1.app' }}
+ if: ${{ github.event_name == 'pull_request' && ( github.base_ref == 'main' || endsWith( github.ref_name , 'Prep') ) && matrix.xcode == '/Applications/Xcode_14.3.1.app' }}
id: cache-mint
uses: actions/cache@v3
env:
@@ -151,33 +152,32 @@ jobs:
- name: Setup Xcode
run: sudo xcode-select -s ${{ matrix.xcode }}/Contents/Developer
- name: Install mint
- if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' && matrix.xcode == '/Applications/Xcode_14.1.app' }}
+ if: ${{ github.event_name == 'pull_request' && ( github.base_ref == 'main' || endsWith( github.ref_name , 'Prep') ) && matrix.xcode == '/Applications/Xcode_14.3.1.app' }}
run: |
brew update
brew install mint
- name: Initialize CodeQL
+ if: startsWith(matrix.xcode,'/Applications/Xcode_14.3.1')
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Build
run: swift build
- name: Perform CodeQL Analysis
+ if: startsWith(matrix.xcode,'/Applications/Xcode_14.3.1')
uses: github/codeql-action/analyze@v2
- name: Run Swift Package tests
run: swift test -v --enable-code-coverage
- - uses: sersoft-gmbh/swift-coverage-action@v2
- - name: Upload SPM coverage to Codecov
- uses: codecov/codecov-action@v2
- with:
- fail_ci_if_error: true
- flags: spm
- verbose: true
- token: ${{ secrets.CODECOV_TOKEN }}
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ - name: Upload SPM to CodeCov.io
+ run: bash <(curl https://codecov.io/bash) -F spm -F macOS -F ${XCODE_NAME}
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Clean up spm build directory
run: rm -rf .build
- name: Lint
run: ./scripts/lint.sh
- if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' && matrix.xcode == '/Applications/Xcode_14.3.app' }}
+ if: ${{ github.event_name == 'pull_request' && ( github.base_ref == 'main' || endsWith( github.ref_name , 'Prep') ) && matrix.xcode == '/Applications/Xcode_14.3.1.app' }}
- name: Dump PIF
if: startsWith(matrix.xcode,'/Applications/Xcode_14')
run: |
@@ -190,38 +190,16 @@ jobs:
done
- name: Run iOS target tests
run: xcodebuild test -scheme SyndiKit -sdk iphonesimulator -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test
- - uses: sersoft-gmbh/swift-coverage-action@v2
- - name: Upload iOS coverage to Codecov
- uses: codecov/codecov-action@v2
- with:
- fail_ci_if_error: true
- flags: iOS,iOS-${{ matrix.iOSVersion }}
- verbose: true
- token: ${{ secrets.CODECOV_TOKEN }}
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ - name: Upload iOS Coverage to CodeCov.io
+ run: bash <(curl https://codecov.io/bash) -F iOS -F iOS${{ matrix.iOSVersion }} -F macOS -F ${XCODE_NAME}
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Run watchOS target tests
run: xcodebuild test -scheme SyndiKit -sdk watchsimulator -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test
- - uses: sersoft-gmbh/swift-coverage-action@v2
- - name: Upload watchOS coverage to Codecov
- uses: codecov/codecov-action@v2
- with:
- fail_ci_if_error: true
- flags: watchOS,watchOS${{ matrix.watchOSVersion }}
- verbose: true
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ - name: Upload watchOS Coverage to CodeCov.io
+ run: bash <(curl https://codecov.io/bash) -F watchOS -F watchOS${{ matrix.watchOSVersion }} -F macOS -F ${XCODE_NAME}
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }}
- deploy:
- name: Deploy to Netlify
- needs: [build-macos, build-ubuntu]
- env:
- PACKAGE_NAME: SyndiKit
- runs-on: macos-12
- if: ${{ github.ref == 'refs/heads/main' }}
- steps:
- - uses: actions/checkout@v2
- - name: Setup Netlify
- run: brew install netlify-cli
- - name: Resolve Package Dependencies
- run: xcodebuild -resolvePackageDependencies -scheme ${{ env.PACKAGE_NAME }} -derivedDataPath DerivedData
- - name: Build DocC Documentation
- run: xcodebuild docbuild -scheme ${{ env.PACKAGE_NAME }} -destination 'platform=macOS' -derivedDataPath DerivedData
- - name: Deploy Files
- run: netlify deploy --site ${{ secrets.NETLIFY_SITE_ID }} --auth ${{ secrets.NETLIFY_AUTH_TOKEN }} --prod
diff --git a/.swiftlint.yml b/.swiftlint.yml
index 9b8e635..db96f2b 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -114,9 +114,8 @@ identifier_name:
excluded:
- id
excluded:
- - Tests/*/XCTestManifests.swift
+ - Tests
- DerivedData
- .build
- - Tests/LinuxMain.swift
indentation_width:
indentation_width: 2
diff --git a/Data/OPML/category.opml b/Data/OPML/category.opml
new file mode 100644
index 0000000..5121bdc
--- /dev/null
+++ b/Data/OPML/category.opml
@@ -0,0 +1,10 @@
+
+
+
+ Illustrating the category attribute
+ Mon, 31 Oct 2005 19:23:00 GMT
+
+
+
+
+
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/Data/OPML/development.opml b/Data/OPML/development.opml
new file mode 100644
index 0000000..a5c7f00
--- /dev/null
+++ b/Data/OPML/development.opml
@@ -0,0 +1,673 @@
+
+
+
+ Development Blogs (English Language) from the iOS Dev Directory (http://iosdevdirectory.com)
+ Mon, 11 Sep 2023 08:09:22 +0000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Data/OPML/directory.opml b/Data/OPML/directory.opml
new file mode 100644
index 0000000..230831b
--- /dev/null
+++ b/Data/OPML/directory.opml
@@ -0,0 +1,26 @@
+
+
+
+ scriptingNewsDirectory.opml
+ Thu, 13 Oct 2005 15:34:07 GMT
+ Tue, 25 Oct 2005 21:33:57 GMT
+ Dave Winer
+ dwiner@yahoo.com
+
+ 1
+ 105
+ 466
+ 386
+ 964
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Data/OPML/placesLived.opml b/Data/OPML/placesLived.opml
new file mode 100644
index 0000000..430231b
--- /dev/null
+++ b/Data/OPML/placesLived.opml
@@ -0,0 +1,43 @@
+
+
+
+ placesLived.opml
+ Mon, 27 Feb 2006 12:09:48 GMT
+ Mon, 27 Feb 2006 12:11:44 GMT
+ Dave Winer
+ http://www.opml.org/profiles/sendMail?usernum=1
+ 1, 2, 5, 10, 13, 15
+ 1
+ 242
+ 329
+ 665
+ 547
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Data/OPML/simpleScript.opml b/Data/OPML/simpleScript.opml
new file mode 100644
index 0000000..b0c5612
--- /dev/null
+++ b/Data/OPML/simpleScript.opml
@@ -0,0 +1,34 @@
+
+
+
+ workspace.userlandsamples.doSomeUpstreaming
+ Mon, 11 Feb 2002 22:48:02 GMT
+ Sun, 30 Oct 2005 03:30:17 GMT
+ Dave Winer
+ dwiner@yahoo.com
+ 1, 2, 4
+ 1
+ 74
+ 41
+ 314
+ 475
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Data/OPML/states.opml b/Data/OPML/states.opml
new file mode 100644
index 0000000..360a171
--- /dev/null
+++ b/Data/OPML/states.opml
@@ -0,0 +1,91 @@
+
+
+
+ states.opml
+ Tue, 15 Mar 2005 16:35:45 GMT
+ Thu, 14 Jul 2005 23:41:05 GMT
+ Dave Winer
+ dave@scripting.com
+ 1, 6, 13, 16, 18, 20
+ 1
+ 106
+ 106
+ 558
+ 479
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Data/OPML/subscriptionList.opml b/Data/OPML/subscriptionList.opml
new file mode 100644
index 0000000..f8bba8e
--- /dev/null
+++ b/Data/OPML/subscriptionList.opml
@@ -0,0 +1,40 @@
+
+
+
+ mySubscriptions.opml
+ Sat, 18 Jun 2005 12:11:52 GMT
+ Tue, 02 Aug 2005 21:42:48 GMT
+ Dave Winer
+ dave@scripting.com
+
+ 1
+ 61
+ 304
+ 562
+ 842
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/SyndiKit/Character.swift b/Sources/SyndiKit/Character.swift
new file mode 100644
index 0000000..6bbf343
--- /dev/null
+++ b/Sources/SyndiKit/Character.swift
@@ -0,0 +1,7 @@
+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..a0beab0
--- /dev/null
+++ b/Sources/SyndiKit/Collection.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension Collection {
+ subscript(safe index: Index) -> Element? {
+ indices.contains(index) ? self[index] : nil
+ }
+}
diff --git a/Sources/SyndiKit/Common/Feedable.swift b/Sources/SyndiKit/Common/Feedable.swift
index 2ed0cc0..21ddcba 100644
--- a/Sources/SyndiKit/Common/Feedable.swift
+++ b/Sources/SyndiKit/Common/Feedable.swift
@@ -9,7 +9,7 @@ import Foundation
/// - ``siteURL``
/// - ``summary``
/// - ``updated``
-/// - ``author``
+/// - ``authors``
/// - ``copyright``
/// - ``image``
/// - ``children``
diff --git a/Sources/SyndiKit/Common/Primitives/ListString.swift b/Sources/SyndiKit/Common/Primitives/ListString.swift
new file mode 100644
index 0000000..58b66db
--- /dev/null
+++ b/Sources/SyndiKit/Common/Primitives/ListString.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+public struct ListString<
+ Value: LosslessStringConvertible & Equatable
+>: Codable, Equatable {
+ public let values: [Value]
+
+ internal init(values: [Value]) {
+ self.values = values
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let listString = try container.decode(String.self)
+ let strings = listString.components(separatedBy: ",")
+ let values = try strings
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+ .map(Self.createValueFrom)
+ self.init(values: values)
+ }
+
+ private static func createValueFrom(_ string: String) throws -> Value {
+ guard let value: Value = .init(string) else {
+ throw DecodingError.typeMismatch(
+ Value.self,
+ .init(codingPath: [], debugDescription: "Invalid value: \(string)")
+ )
+ }
+ return value
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ let strings = values.map(String.init)
+ let listString = strings.joined(separator: ",")
+ try container.encode(listString)
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift
index 331ba2b..26a4327 100644
--- a/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift
+++ b/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift
@@ -45,6 +45,10 @@ public struct RSSChannel: Codable {
public let wpBaseSiteURL: URL?
public let wpBaseBlogURL: URL?
+ public let podcastLocked: PodcastLocked?
+ public let podcastFundings: [PodcastFunding]
+ public let podcastPeople: [PodcastPerson]
+
enum CodingKeys: String, CodingKey {
case title
case link
@@ -65,6 +69,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 podcastPeople = "podcast:person"
}
}
diff --git a/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift b/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift
index 18a6052..647bebb 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 podcastPeople: [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?
@@ -35,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?
@@ -58,7 +62,11 @@ public struct RSSItem: Codable {
itunesExplicit: String? = nil,
itunesDuration: TimeInterval? = nil,
itunesImage: iTunesImage? = nil,
- podcastPerson: [PodcastPerson]? = nil,
+ podcastPeople: [PodcastPerson] = [],
+ podcastTranscripts: [PodcastTranscript] = [],
+ podcastChapters: PodcastChapters? = nil,
+ podcastSoundbites: [PodcastSoundbite] = [],
+ podcastSeason: PodcastSeason? = nil,
enclosure: Enclosure? = nil,
creators: [String] = [],
wpCommentStatus: String? = nil,
@@ -75,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
@@ -96,7 +104,11 @@ public struct RSSItem: Codable {
self.itunesExplicit = itunesExplicit
self.itunesDuration = itunesDuration.map(iTunesDuration.init)
self.itunesImage = itunesImage
- self.podcastPerson = podcastPerson
+ self.podcastPeople = podcastPeople
+ 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(
+ podcastPeople = try container.decodeIfPresent(
[PodcastPerson].self,
- forKey: .podcastPerson
+ forKey: .podcastPeople
+ ) ?? []
+ 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)
@@ -203,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)
@@ -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 podcastPeople = "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+MimeType.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift
new file mode 100644
index 0000000..4b57a7e
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift
@@ -0,0 +1,54 @@
+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(
+ // swiftlint:disable:next line_length
+ "Type attribute of with value: \(self) should either be `KnownMimeType`, or unknown!"
+ )
+ }
+ }
+
+ public init?(rawValue: String) {
+ self.init(caseInsensitive: 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
new file mode 100644
index 0000000..561012e
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct PodcastChapters: Codable, Equatable {
+ public let url: URL
+ public let type: MimeType
+
+ 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 96%
rename from Sources/SyndiKit/Formats/Media/PodcastEpisode.swift
rename to Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift
index 97bca65..d5b3488 100644
--- a/Sources/SyndiKit/Formats/Media/PodcastEpisode.swift
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift
@@ -1,4 +1,5 @@
import Foundation
+
public protocol PodcastEpisode {
var title: String? { get }
var episode: Int? { get }
@@ -37,6 +38,6 @@ struct PodcastEpisodeProperties: PodcastEpisode {
duration = rssItem.itunesDuration?.value
image = rssItem.itunesImage
self.enclosure = enclosure
- people = rssItem.podcastPerson ?? []
+ people = rssItem.podcastPeople
}
}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift
new file mode 100644
index 0000000..227ccb4
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct PodcastFunding: Codable, Equatable {
+ public let url: URL
+ public let description: String?
+
+ enum CodingKeys: String, CodingKey {
+ case url
+ case description = ""
+ }
+}
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..a5bbe92
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift
@@ -0,0 +1,106 @@
+import Foundation
+
+public extension PodcastLocation {
+ struct GeoURI: Codable, Equatable, LosslessStringConvertible {
+ public let latitude: Double
+ public let longitude: Double
+ public let altitude: Double?
+ public let accuracy: Double?
+
+ public var description: String {
+ var description = "geo:\(latitude),\(longitude)"
+
+ if let altitude = altitude {
+ description += ",\(altitude)"
+ }
+
+ if let accuracy = accuracy {
+ description += ";u=\(accuracy)"
+ }
+
+ return description
+ }
+
+ public init(
+ latitude: Double,
+ longitude: Double,
+ altitude: Double? = nil,
+ accuracy: Double? = nil
+ ) {
+ self.latitude = latitude
+ self.longitude = longitude
+ self.altitude = altitude
+ self.accuracy = accuracy
+ }
+
+ public init?(_ description: String) {
+ try? self.init(singleValue: description)
+ }
+
+ public init(singleValue: String) throws {
+ let pathComponents = try Self.pathComponents(from: singleValue)
+
+ guard
+ let geoCoords = pathComponents[safe: 0]?.split(separator: ","),
+ let latitude = geoCoords[safe: 0]?.asDouble(),
+ let longitude = geoCoords[safe: 1]?.asDouble()
+ else {
+ throw DecodingError.dataCorrupted(
+ .init(
+ codingPath: [PodcastLocation.CodingKeys.geo],
+ debugDescription: "Invalid coordinates for geo attribute: \(singleValue)"
+ )
+ )
+ }
+
+ let altitude = geoCoords[safe: 2]?.asDouble()
+
+ let accuracy = pathComponents[safe: 1]?
+ .split(separator: "=")[safe: 1]?
+ .asDouble()
+
+ self.init(
+ latitude: latitude,
+ longitude: longitude,
+ altitude: altitude,
+ accuracy: accuracy
+ )
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let singleValue = try container.decode(String.self)
+
+ try self.init(singleValue: singleValue)
+ }
+
+ private static func pathComponents(from string: String) throws -> [Substring] {
+ let components = string.split(separator: ":")
+
+ guard
+ components[safe: 0] == "geo" else {
+ throw DecodingError.dataCorrupted(
+ .init(
+ codingPath: [PodcastLocation.CodingKeys.geo],
+ debugDescription: "Invalid prefix for geo attribute: \(string)"
+ )
+ )
+ }
+ guard let geoPath = components[safe: 1] else {
+ throw DecodingError.dataCorrupted(
+ .init(
+ codingPath: [PodcastLocation.CodingKeys.geo],
+ debugDescription: "Invalid path for geo attribute: \(string)"
+ )
+ )
+ }
+
+ return geoPath.split(separator: ";")
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(description)
+ }
+ }
+}
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..6eaa0de
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift
@@ -0,0 +1,47 @@
+import Foundation
+
+public extension PodcastLocation {
+ 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?
+
+ 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.osmQuery],
+ debugDescription: "Invalid type for osm attribute: \(osmStr)"
+ )
+ )
+ }
+ guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else {
+ throw DecodingError.dataCorrupted(
+ .init(
+ codingPath: [PodcastLocation.CodingKeys.osmQuery],
+ debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)"
+ )
+ )
+ }
+ let osmRevision = osmStr.split(separator: "#")[safe: 1]?.asInt()
+
+ id = osmID
+ type = osmType
+ revision = osmRevision
+ }
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift
new file mode 100644
index 0000000..df8c1c1
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+public struct PodcastLocation: Codable, Equatable {
+ public let geo: GeoURI?
+ public let osmQuery: OsmQuery?
+
+ public let name: String
+
+ enum CodingKeys: String, CodingKey {
+ case geo
+ case osmQuery = "osm"
+
+ case name = ""
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift
new file mode 100644
index 0000000..b35c891
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+public struct PodcastLocked: Codable, Equatable {
+ public let owner: String?
+ public let isLocked: Bool
+
+ enum CodingKeys: String, CodingKey {
+ case owner
+ case isLocked = ""
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ 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
new file mode 100644
index 0000000..d3359d1
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift
@@ -0,0 +1,78 @@
+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 {
+ 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(
+ // swiftlint:disable:next line_length
+ "Role attribute of with value: \(self) should either be a `KnownRole`, or unknown!"
+ )
+ }
+ }
+
+ public init?(rawValue: String) {
+ self.init(caseInsensitive: rawValue)
+ }
+
+ public init(caseInsensitive: String) {
+ if let knownRole = KnownRole(caseInsensitive: caseInsensitive) {
+ self = .init(knownRole: knownRole)
+ } else {
+ 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
+ }
+ }
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift
index 45494ab..8ee3322 100644
--- a/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift
@@ -1,18 +1,31 @@
import Foundation
-public struct PodcastPerson: Codable {
- public let email: String?
- public let role: String
- public let href: String
+public struct PodcastPerson: Codable, Equatable {
+ public let role: Role?
+ public let group: String?
+ public let href: URL?
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 = ""
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ role = try container.decodeIfPresent(Role.self, forKey: .role)
+ group = try container.decodeIfPresent(String.self, forKey: .group)
+ fullname = try container.decode(String.self, forKey: .fullname)
+
+ let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? ""
+ href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl)
+
+ let imgUrl = try container.decodeIfPresent(String.self, forKey: .img) ?? ""
+ img = imgUrl.isEmpty ? nil : URL(string: imgUrl)
}
}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift
new file mode 100644
index 0000000..e18ba77
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct PodcastSeason: Codable, Equatable {
+ public let name: String?
+ public let number: Int
+
+ enum CodingKeys: String, CodingKey {
+ case name
+ case number = ""
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift
new file mode 100644
index 0000000..3119f6a
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+public struct PodcastSoundbite: Codable, Equatable {
+ public let startTime: TimeInterval
+ public let duration: TimeInterval
+
+ public let title: String?
+
+ enum CodingKeys: String, CodingKey {
+ case startTime
+ case duration
+
+ case title = ""
+ }
+}
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..654c9d6
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift
@@ -0,0 +1,74 @@
+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(
+ // swiftlint:disable:next line_length
+ "Type attribute of with value: \(self) should either be a `KnownMimeType`, or unknown!"
+ )
+ }
+ }
+
+ public init?(rawValue: String) {
+ self.init(caseInsensitive: 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
new file mode 100644
index 0000000..bde41ac
--- /dev/null
+++ b/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+public struct PodcastTranscript: Codable, Equatable {
+ public enum Relationship: String, Codable {
+ case captions
+ }
+
+ public let url: URL
+ public let type: MimeType
+ public let language: String?
+ public let rel: Relationship?
+
+ enum CodingKeys: String, CodingKey {
+ case url
+ case type
+ case language
+ case rel
+ }
+}
diff --git a/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift b/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift
index e36a0bd..3c395a8 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
@@ -24,10 +24,13 @@ public extension WordPressElements {
}
extension WordPressElements.Category: Equatable {
- public static func == (lhs: WordPressElements.Category, rhs: WordPressElements.Category) -> Bool {
+ 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..fd56724 100644
--- a/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift
+++ b/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift
@@ -18,8 +18,11 @@ public extension WordPressElements {
}
extension WordPressElements.PostMeta: Equatable {
- public static func == (lhs: WordPressElements.PostMeta, rhs: WordPressElements.PostMeta) -> Bool {
+ 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/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/Sources/SyndiKit/Formats/OPML/OPML.swift b/Sources/SyndiKit/Formats/OPML/OPML.swift
new file mode 100644
index 0000000..debc827
--- /dev/null
+++ b/Sources/SyndiKit/Formats/OPML/OPML.swift
@@ -0,0 +1,14 @@
+import Foundation
+
+public struct OPML: Codable, Equatable {
+ public let version: String
+
+ public let head: Head
+ public let body: Body
+
+ enum CodingKeys: String, CodingKey {
+ case version
+ case head
+ case body
+ }
+}
diff --git a/Sources/SyndiKit/Formats/OPML/OPMLBody.swift b/Sources/SyndiKit/Formats/OPML/OPMLBody.swift
new file mode 100644
index 0000000..1e405e0
--- /dev/null
+++ b/Sources/SyndiKit/Formats/OPML/OPMLBody.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public extension OPML {
+ struct Body: Codable, Equatable {
+ public let outlines: [Outline]
+
+ enum CodingKeys: String, CodingKey {
+ case outlines = "outline"
+ }
+ }
+}
diff --git a/Sources/SyndiKit/Formats/OPML/OPMLHead.swift b/Sources/SyndiKit/Formats/OPML/OPMLHead.swift
new file mode 100644
index 0000000..7d62c8f
--- /dev/null
+++ b/Sources/SyndiKit/Formats/OPML/OPMLHead.swift
@@ -0,0 +1,35 @@
+import Foundation
+
+public extension OPML {
+ struct Head: Codable, Equatable {
+ public let title: String?
+ public let dateCreated: String?
+ public let dateModified: String?
+ public let ownerName: String?
+ public let ownerEmail: String?
+ public let ownerId: String?
+ public let docs: String?
+ public let expansionStates: ListString?
+ public let vertScrollState: Int?
+ public let windowTop: Int?
+ public let windowLeft: Int?
+ public let windowBottom: Int?
+ public let windowRight: Int?
+
+ enum CodingKeys: String, CodingKey {
+ case title
+ case dateCreated
+ case dateModified
+ case ownerName
+ case ownerEmail
+ case ownerId
+ case docs
+ case expansionStates = "expansionState"
+ case vertScrollState
+ case windowTop
+ case windowLeft
+ case windowBottom
+ case windowRight
+ }
+ }
+}
diff --git a/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift b/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift
new file mode 100644
index 0000000..ec098d5
--- /dev/null
+++ b/Sources/SyndiKit/Formats/OPML/OPMLOutline.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+public extension OPML {
+ struct Outline: Codable, Equatable {
+ public let text: String
+ public let title: String?
+ public let description: String?
+ public let type: OutlineType?
+ public let url: URL?
+ public let htmlUrl: URL?
+ public let xmlUrl: URL?
+ public let language: String?
+ public let created: String?
+ public let categories: ListString?
+ public let isComment: Bool?
+ public let isBreakpoint: Bool?
+ public let version: String?
+
+ public let outlines: [Outline]?
+
+ enum CodingKeys: String, CodingKey {
+ case text
+ case title
+ case description
+ case type
+ case url
+ case htmlUrl
+ case xmlUrl
+ case language
+ case created
+ case categories = "category"
+ case isComment
+ case isBreakpoint
+ case version
+
+ case outlines = "outline"
+ }
+ }
+}
diff --git a/Sources/SyndiKit/Formats/OPML/OPMLOutlineType.swift b/Sources/SyndiKit/Formats/OPML/OPMLOutlineType.swift
new file mode 100644
index 0000000..3400edb
--- /dev/null
+++ b/Sources/SyndiKit/Formats/OPML/OPMLOutlineType.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+public enum OutlineType: String, Codable {
+ case rss
+ case link
+ case include
+}
diff --git a/Sources/SyndiKit/Substring.SubSequence.swift b/Sources/SyndiKit/Substring.SubSequence.swift
new file mode 100644
index 0000000..38eafa7
--- /dev/null
+++ b/Sources/SyndiKit/Substring.SubSequence.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+extension Substring.SubSequence {
+ func asDouble() -> Double? {
+ Double(self)
+ }
+
+ func asInt() -> Int? {
+ guard let double = Double(self) else { return nil }
+
+ return Int(double)
+ }
+
+ func asExactInt() -> Int? {
+ guard let double = Double(self) else { return nil }
+
+ return Int(exactly: double)
+ }
+}
diff --git a/Sources/SyndiKit/SyndiKit.docc/SyndiKit.md b/Sources/SyndiKit/SyndiKit.docc/SyndiKit.md
index c0f4d3b..e2c3281 100644
--- a/Sources/SyndiKit/SyndiKit.docc/SyndiKit.md
+++ b/Sources/SyndiKit/SyndiKit.docc/SyndiKit.md
@@ -182,13 +182,13 @@ Abstract media types which can be pulled for the various ``Entryable`` objects.
- ``MediaContent``
- ``Video``
-
### XML Primitive Types
In many cases, types are encoded in non-matching types but are intended to strong-typed for various formats. These primitives are setup to make XML decoding easier while retaining their intended strong-type.
- ``CData``
- ``XMLStringInt``
+- ``ListString``
### Syndication Updates
@@ -206,6 +206,7 @@ Specific properties related to the Atom format.
- ``AtomEntry``
- ``AtomCategory``
- ``AtomMedia``
+- ``AtomMediaGroup``
- ``Link``
### JSON Feed Format
@@ -215,6 +216,11 @@ Specific properties related to the JSON Feed format.
- ``JSONFeed``
- ``JSONItem``
+### OPML Feed Formate
+
+- ``OPML``
+- ``OutlineType``
+
### RSS Feed Format
Specific properties related to the RSS Feed format.
@@ -226,6 +232,19 @@ Specific properties related to the RSS Feed format.
- ``RSSItemCategory``
- ``Enclosure``
+### Podcast Extensions
+
+Specific properties related to [podcasts](https://github.com/Podcastindex-org/podcast-namespace).
+
+- ``PodcastPerson``
+- ``PodcastSeason``
+- ``PodcastChapters``
+- ``PodcastLocation``
+- ``PodcastSoundbite``
+- ``PodcastTranscript``
+- ``PodcastFunding``
+- ``PodcastLocked``
+
### WordPress Extensions
Specific extension properties provided by WordPress.
diff --git a/Tests/SyndiKitTests/Content.Directories.swift b/Tests/SyndiKitTests/Content.Directories.swift
index 124760e..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()
@@ -10,6 +10,7 @@ extension Content {
.appendingPathComponent("Data")
static let XML = data.appendingPathComponent("XML")
static let JSON = data.appendingPathComponent("JSON")
+ static let OPML = data.appendingPathComponent("OPML")
static let WordPress = data.appendingPathComponent("WordPress")
}
}
diff --git a/Tests/SyndiKitTests/Content.ResultDictionary.swift b/Tests/SyndiKitTests/Content.ResultDictionary.swift
index 9e465d9..8232287 100644
--- a/Tests/SyndiKitTests/Content.ResultDictionary.swift
+++ b/Tests/SyndiKitTests/Content.ResultDictionary.swift
@@ -1,5 +1,6 @@
import Foundation
@testable import SyndiKit
+import XMLCoder
enum Content {
typealias ResultDictionary = [String: Result]
@@ -17,12 +18,30 @@ enum Content {
}.map(Dictionary.init(uniqueKeysWithValues:)).get()
}
- static let decoder = SynDecoder()
+ static let synDecoder = SynDecoder()
+ static let xmlDecoder = XMLDecoder()
- // swiftlint:disable force_try line_length
- static let xmlFeeds = try! Content.resultDictionaryFrom(directoryURL: Directories.XML, by: Self.decoder.decode(_:))
- static let jsonFeeds = try! Content.resultDictionaryFrom(directoryURL: Directories.JSON, by: Self.decoder.decode(_:))
- static let wordpressDataSet = try! FileManager.default.dataFromDirectory(at: Directories.WordPress)
+ static let xmlFeeds = try! Content.resultDictionaryFrom(
+ directoryURL: Directories.XML,
+ by: Self.synDecoder.decode(_:)
+ )
+ static let jsonFeeds = try! Content.resultDictionaryFrom(
+ directoryURL: Directories.JSON,
+ by: Self.synDecoder.decode(_:)
+ )
+ static let opml = try! Content.resultDictionaryFrom(
+ directoryURL: Directories.OPML,
+ by: Self.xmlDecoder.decodeOPML(_:)
+ )
+ static let wordpressDataSet = try! FileManager.default.dataFromDirectory(
+ at: Directories.WordPress
+ )
static let blogs: SiteCollection = try! .init(contentsOf: Directories.data.appendingPathComponent("blogs.json"))
// swiftlint:enable force_try line_length
}
+
+extension XMLDecoder {
+ func decodeOPML(_ data: Data) throws -> OPML {
+ try decode(OPML.self, from: data)
+ }
+}
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/OPMLTests.swift b/Tests/SyndiKitTests/OPMLTests.swift
new file mode 100644
index 0000000..22da4ab
--- /dev/null
+++ b/Tests/SyndiKitTests/OPMLTests.swift
@@ -0,0 +1,125 @@
+@testable import SyndiKit
+import XCTest
+
+internal final class OPMLTests: XCTestCase {
+ internal func testSubscriptionList() throws {
+ let opml = try Content.opml["subscriptionList"]?.get()
+
+ XCTAssertEqual(opml?.head.title, "mySubscriptions.opml")
+ XCTAssertEqual(opml?.head.ownerEmail, "dave@scripting.com")
+ XCTAssertEqual(opml?.body.outlines.count, 13)
+
+ let firstOutline = opml?.body.outlines.first
+
+ XCTAssertEqual(firstOutline?.text, "CNET News.com")
+ XCTAssertEqual(firstOutline?.description, "Tech news and business reports by CNET News.com. Focused on information technology, core topics include computers, hardware, software, networking, and Internet media.")
+ XCTAssertEqual(firstOutline?.htmlUrl, URL(string: "http://news.com.com/")!)
+ XCTAssertEqual(firstOutline?.language, "unknown")
+ XCTAssertEqual(firstOutline?.title, "CNET News.com")
+ XCTAssertEqual(firstOutline?.type, .rss)
+ XCTAssertEqual(firstOutline?.version, "RSS2")
+ XCTAssertEqual(firstOutline?.xmlUrl, URL(string: "http://news.com.com/2547-1_3-0-5.xml")!)
+ }
+
+ internal func testStates() throws {
+ let opml = try Content.opml["states"]?.get()
+
+ XCTAssertEqual(opml?.head.title, "states.opml")
+ XCTAssertEqual(opml?.head.ownerEmail, "dave@scripting.com")
+ XCTAssertEqual(opml?.body.outlines.count, 1)
+
+ 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)
+ }
+
+ internal func testCategory() throws {
+ let opml = try Content.opml["category"]?.get()
+
+ XCTAssertEqual(opml?.head.title, "Illustrating the category attribute")
+ XCTAssertEqual(opml?.body.outlines.count, 1)
+
+ let outline = opml?.body.outlines.first
+
+ XCTAssertEqual(outline?.text, "The Mets are the best team in baseball.")
+ XCTAssertEqual(outline?.categories?.values.count, 2)
+ XCTAssertEqual(outline?.categories?.values[0], "/Philosophy/Baseball/Mets")
+ XCTAssertEqual(outline?.categories?.values[1], "/Tourism/New York")
+ }
+
+ internal func testPlacesLived() throws {
+ let opml = try Content.opml["placesLived"]?.get()
+
+ XCTAssertEqual(opml?.head.title, "placesLived.opml")
+ XCTAssertEqual(opml?.head.ownerId, "http://www.opml.org/profiles/sendMail?usernum=1")
+ XCTAssertEqual(opml?.head.expansionStates?.values.count, 6)
+ XCTAssertEqual(opml?.head.expansionStates?.values[0], 1)
+ XCTAssertEqual(opml?.head.expansionStates?.values[3], 10)
+ }
+
+ internal func testSimpleScript() throws {
+ let opml = try Content.opml["simpleScript"]?.get()
+
+ XCTAssertEqual(opml?.head.title, "workspace.userlandsamples.doSomeUpstreaming")
+ XCTAssertEqual(opml?.head.expansionStates?.values.count, 3)
+ XCTAssertEqual(opml?.head.expansionStates?.values[0], 1)
+ XCTAssertEqual(opml?.head.expansionStates?.values[2], 4)
+
+ XCTAssertEqual(opml?.body.outlines.count, 4)
+
+ let isCommentOutline = opml?.body.outlines.first
+ XCTAssertEqual(isCommentOutline?.text, "Changes")
+ XCTAssertEqual(isCommentOutline?.isComment, true)
+
+ let isBreakpointOutline = opml?.body.outlines[1].outlines?.first
+ XCTAssertEqual(isBreakpointOutline?.text, "file.surefilepath (f)")
+ XCTAssertEqual(isBreakpointOutline?.isBreakpoint, true)
+ }
+
+ internal func testInvalidExpansionStateType() throws {
+ XCTAssertThrowsError(try Content.opml["category_invalidExpansionState"]?.get()) { error in
+ guard case let .typeMismatch(type, context) = error as? DecodingError else {
+ XCTFail("Expected typeMismatch error.")
+ return
+ }
+
+ XCTAssertTrue(type is Int.Type)
+ XCTAssertEqual(
+ context.debugDescription,
+ "Invalid value: one"
+ )
+ }
+ }
+
+ internal func testType() throws {
+ var opml = try Content.opml["subscriptionList"]?.get()
+
+ XCTAssertEqual(opml?.body.outlines.first?.type, .rss)
+ XCTAssertNotNil(opml?.body.outlines.first?.text)
+ XCTAssertNotNil(opml?.body.outlines.first?.xmlUrl)
+
+ opml = try Content.opml["directory"]?.get()
+
+ XCTAssertEqual(opml?.body.outlines.first?.type, .link)
+ XCTAssertNotNil(opml?.body.outlines.first?.url)
+
+ opml = try Content.opml["placesLived"]?.get()
+ let floridaOutline = opml?.body.outlines.first?
+ .outlines?.first(
+ where: { $0.text == "Florida" }
+ )
+
+ XCTAssertEqual(floridaOutline?.type, .include)
+ XCTAssertNotNil(floridaOutline?.url)
+ }
+}
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,
diff --git a/Tests/SyndiKitTests/RSSCodedTests.swift b/Tests/SyndiKitTests/RSSCodedTests.swift
index 0faafe5..56f9dcc 100644
--- a/Tests/SyndiKitTests/RSSCodedTests.swift
+++ b/Tests/SyndiKitTests/RSSCodedTests.swift
@@ -176,6 +176,291 @@ 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?.isLocked, 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.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 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 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.267_2
+ let expectedLongitude = 97.743_1
+ let expectedOsmType = "R"
+ let expectedOsmID = 113_314
+ 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.osmQuery?.id, expectedOsmID)
+ XCTAssertEqual(sut.osmQuery?.type, .relation)
+ XCTAssertNil(sut.osmQuery?.revision)
+ }
+
+ func testPodcastLocationWithAccuracy() throws {
+ let expectedLatitude = 30.267_2
+ let expectedLongitude = 97.743_1
+ 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)
+ XCTAssertEqual(sut.geo?.description, "geo:\(expectedLatitude),\(expectedLongitude);u=\(expectedAccuracy)")
+ XCTAssertNil(sut.geo?.altitude)
+ }
+
+ func testPodcastLocationWithAltitude() throws {
+ let expectedLatitude = 30.267_2
+ let expectedLongitude = 97.743_1
+ let expectedAltitude = 250.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?.altitude, expectedAltitude)
+ XCTAssertEqual(sut.geo?.description, "geo:\(expectedLatitude),\(expectedLongitude),\(expectedAltitude)")
+ 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: .osmQuery)
+ }
+ }
+
+ 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 = [
@@ -255,12 +540,12 @@ 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
}
- XCTAssertNil(item.podcastPerson)
+ XCTAssertTrue(item.podcastPeople.isEmpty)
}
func testEpisodesWithHostAndGuestPersons() {
@@ -282,22 +567,22 @@ 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.podcastPeople.first(where: { $0.role == .host })
XCTAssertNotNil(host)
- XCTAssertEqual(host?.name, "Leo Dion")
- XCTAssertEqual(host?.href, "https://brightdigit.com")
+ XCTAssertEqual(host?.fullname, "Leo Dion")
+ 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.podcastPerson?.first(where: { $0.role.lowercased() == "guest" })
+ let guest = item.podcastPeople.first(where: { $0.role == .guest })
XCTAssertNotNil(guest)
- XCTAssertEqual(guest?.name, "CompileSwift")
- XCTAssertEqual(guest?.href, "https://compileswift.com")
+ XCTAssertEqual(guest?.fullname, "CompileSwift")
+ 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")
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/UTF8EncodedURLTests.swift b/Tests/SyndiKitTests/UTF8EncodedURLTests.swift
new file mode 100644
index 0000000..567b887
--- /dev/null
+++ b/Tests/SyndiKitTests/UTF8EncodedURLTests.swift
@@ -0,0 +1,22 @@
+@testable import SyndiKit
+import XCTest
+import XMLCoder
+
+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/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")
diff --git a/Tests/SyndiKitTests/XMLStringIntTests.swift b/Tests/SyndiKitTests/XMLStringIntTests.swift
new file mode 100644
index 0000000..64a4a8a
--- /dev/null
+++ b/Tests/SyndiKitTests/XMLStringIntTests.swift
@@ -0,0 +1,36 @@
+@testable import SyndiKit
+import XCTest
+import XMLCoder
+
+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)
+ }
+ }
+}
diff --git a/project.yml b/project.yml
new file mode 100644
index 0000000..39c657e
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,13 @@
+name: SyndiKit
+settings:
+ LINT_MODE: ${LINT_MODE}
+packages:
+ SyndiKit:
+ path: .
+aggregateTargets:
+ Lint:
+ buildScripts:
+ - path: Scripts/lint.sh
+ name: Lint
+ basedOnDependencyAnalysis: false
+ schemes: {}
\ No newline at end of file