├── .dockerignore
├── .github
├── dependabot.yml
└── workflows
│ ├── codeql.yml
│ └── syndikit.yml
├── .gitignore
├── .hound.yml
├── .htaccess
├── .periphery.yml
├── .spi.yml
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── Assets
└── logo.svg
├── Data
├── JSON
│ ├── advancedswift.json
│ ├── andyibanez.json
│ ├── appfigures.youtube.json
│ ├── apple.developer.json
│ ├── apple.releases.json
│ ├── atomicbird.json
│ ├── avanderlee.json
│ ├── cnn_latest.json
│ ├── cocoacasts.json
│ ├── donnywals.json
│ ├── empowerapps-show.json
│ ├── enekoalonso.json
│ ├── fivestars.json
│ ├── ideveloper.json
│ ├── ios-goodies.json
│ ├── iosdevweekly.json
│ ├── it-guy.json
│ ├── kilo.youtube.json
│ ├── mecid.json
│ ├── mjtsai.json
│ ├── mokacoding.json
│ ├── radar.json
│ ├── raywenderlich.json
│ ├── revenuecat.json
│ ├── rhonabwy.json
│ ├── senestenyt.json
│ ├── stewart.youtube.json
│ ├── swiftbysundell.json
│ ├── swiftpackageindex.json
│ ├── swiftweeklybrief.json
│ ├── timac.json
│ ├── tundsdev.youtube.json
│ ├── vincent.youtube.json
│ └── wwdcnotes.json
├── OPML
│ ├── category.opml
│ ├── category_invalidExpansionState.opml
│ ├── development.opml
│ ├── directory.opml
│ ├── placesLived.opml
│ ├── simpleScript.opml
│ ├── states.opml
│ └── subscriptionList.opml
├── WordPress
│ ├── articles.xml
│ └── tutorials.xml
├── XML
│ ├── advancedswift.xml
│ ├── andyibanez.xml
│ ├── appfigures.youtube.xml
│ ├── apple.developer.xml
│ ├── apple.releases.xml
│ ├── atomicbird.xml
│ ├── avanderlee.xml
│ ├── cnn_latest.xml
│ ├── cocoacasts.xml
│ ├── donnywals.xml
│ ├── empowerapps-show-cdata_summary.xml
│ ├── empowerapps-show.xml
│ ├── enekoalonso.xml
│ ├── fivestars.xml
│ ├── ideveloper.xml
│ ├── ios-goodies.xml
│ ├── iosdevweekly.xml
│ ├── it-guy.xml
│ ├── kilo.youtube.xml
│ ├── mecid.xml
│ ├── mjtsai.xml
│ ├── mokacoding.xml
│ ├── news.rss
│ ├── radar.xml
│ ├── raywenderlich.xml
│ ├── revenuecat.xml
│ ├── rhonabwy.xml
│ ├── senestenyt.xml
│ ├── stewart.youtube.xml
│ ├── swiftbysundell.xml
│ ├── swiftpackageindex.xml
│ ├── swiftweeklybrief.xml
│ ├── timac.xml
│ ├── tundsdev.youtube.xml
│ ├── vincent.youtube.xml
│ ├── wait-wait-dont-tell-me.xml
│ └── wwdcnotes.xml
├── blogs.json
└── urls.tsv
├── Dockerfile
├── LICENSE
├── Mintfile
├── Package.resolved
├── Package.swift
├── README.md
├── Scripts
├── build_test_data.sh
├── docc.sh
├── gh-md-toc
├── httpd.conf
└── lint.sh
├── Sources
└── SyndiKit
│ ├── Character.swift
│ ├── Collection.swift
│ ├── Common
│ ├── Author.swift
│ ├── EntryCategory.swift
│ ├── Entryable.swift
│ ├── Feedable.swift
│ ├── Link.swift
│ └── Primitives
│ │ ├── CData.swift
│ │ ├── ListString.swift
│ │ ├── UTF8EncodedURL.swift
│ │ └── XMLStringInt.swift
│ ├── Decoding
│ ├── AnyDecoding.swift
│ ├── CustomDecoderSetup.swift
│ ├── DateFormatterDecoder.swift
│ ├── DecodableFeed.swift
│ ├── DecoderSetup.swift
│ ├── DecoderSource.swift
│ ├── Decoding.swift
│ ├── DecodingError.swift
│ ├── SynDecoder.swift
│ └── TypeDecoder.swift
│ ├── Dictionary.swift
│ ├── Formats
│ ├── Blogs
│ │ ├── CategoryDescriptor.swift
│ │ ├── CategoryLanguage.swift
│ │ ├── Site.swift
│ │ ├── SiteCategory.swift
│ │ ├── SiteCategoryType.swift
│ │ ├── SiteCollection.swift
│ │ ├── SiteDirectory.swift
│ │ ├── SiteDirectoryBuilder.swift
│ │ ├── SiteLanguage.swift
│ │ ├── SiteLanguageCategory+Site.swift
│ │ ├── SiteLanguageCategory.swift
│ │ ├── SiteLanguageContent.swift
│ │ └── SiteLanguageType.swift
│ ├── Feeds
│ │ ├── Atom
│ │ │ ├── AtomCategory.swift
│ │ │ ├── AtomEntry.swift
│ │ │ ├── AtomFeed.swift
│ │ │ ├── AtomMedia.swift
│ │ │ └── AtomMediaGroup.swift
│ │ ├── JSONFeed
│ │ │ ├── JSONFeed.swift
│ │ │ └── JSONItem.swift
│ │ └── RSS
│ │ │ ├── Enclosure.swift
│ │ │ ├── EntryID.swift
│ │ │ ├── RSSChannel.swift
│ │ │ ├── RSSFeed.swift
│ │ │ ├── RSSImage.swift
│ │ │ ├── RSSItem+Decodings.swift
│ │ │ ├── RSSItem+Init.swift
│ │ │ ├── RSSItem.swift
│ │ │ └── RSSItemCategory.swift
│ ├── Media
│ │ ├── MediaContent.swift
│ │ ├── Podcast
│ │ │ ├── PodcastChapters+MimeType.swift
│ │ │ ├── PodcastChapters.swift
│ │ │ ├── PodcastEpisode.swift
│ │ │ ├── PodcastFunding.swift
│ │ │ ├── PodcastLocation+GeoURI.swift
│ │ │ ├── PodcastLocation+OsmQuery.swift
│ │ │ ├── PodcastLocation.swift
│ │ │ ├── PodcastLocked.swift
│ │ │ ├── PodcastPerson+Role.swift
│ │ │ ├── PodcastPerson.swift
│ │ │ ├── PodcastSeason.swift
│ │ │ ├── PodcastSoundbite.swift
│ │ │ ├── PodcastTranscript+MimeType.swift
│ │ │ └── PodcastTranscript.swift
│ │ ├── Video.swift
│ │ ├── Wordpress
│ │ │ ├── WPCategory.swift
│ │ │ ├── WPPostMeta.swift
│ │ │ ├── WPTag.swift
│ │ │ ├── WordPressPost+RSSItem.swift
│ │ │ └── WordPressPost.swift
│ │ ├── YouTube
│ │ │ └── YouTubeID.swift
│ │ └── iTunes
│ │ │ ├── iTunesDuration.swift
│ │ │ ├── iTunesEpisode.swift
│ │ │ ├── iTunesImage.swift
│ │ │ └── iTunesOwner.swift
│ ├── OPML
│ │ ├── OPML+Body.swift
│ │ ├── OPML+Head.swift
│ │ ├── OPML+Outline.swift
│ │ ├── OPML.swift
│ │ └── OutlineType.swift
│ └── SyndicationUpdate
│ │ ├── SyndicationUpdate.swift
│ │ ├── SyndicationUpdateFrequency.swift
│ │ └── SyndicationUpdatePeriod.swift
│ ├── KeyedDecodingContainerProtocol.swift
│ ├── Substring.SubSequence.swift
│ ├── SyndiKit.docc
│ ├── Resources
│ │ ├── favicon.svg
│ │ └── logo.png
│ └── SyndiKit.md
│ └── URL.swift
├── Tests
└── SyndiKitTests
│ ├── BlogTests.swift
│ ├── Content.Directories.swift
│ ├── Content.ResultDictionary.swift
│ ├── DecodingErrorTests.swift
│ ├── Extensions
│ ├── FileManager.swift
│ ├── JSONFeed.swift
│ ├── Sequence.swift
│ ├── SiteCollection.swift
│ ├── String.swift
│ └── URL.swift
│ ├── OPMLTests.swift
│ ├── RSSCoded.Durations.swift
│ ├── RSSCodedTests.swift
│ ├── RSSGUIDTests.swift
│ ├── RSSItemCategoryTests.swift
│ ├── UTF8EncodedURLTests.swift
│ ├── WordPressElementsTests.swift
│ ├── WordpressTests.swift
│ └── XMLStringIntTests.swift
├── codecov.yml
├── netlify.toml
└── project.yml
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build/
2 | .swiftpm/
3 | DerivedData
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "swift" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | branches: [ "main" ]
19 | schedule:
20 | - cron: '16 9 * * 1'
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze
25 | # Runner size impacts CodeQL analysis time. To learn more, please see:
26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
27 | # - https://gh.io/supported-runners-and-hardware-resources
28 | # - https://gh.io/using-larger-runners
29 | # Consider using larger runners for possible analysis time improvements.
30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
32 | permissions:
33 | actions: read
34 | contents: read
35 | security-events: write
36 |
37 | strategy:
38 | fail-fast: false
39 | matrix:
40 | language: [ 'swift' ]
41 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
42 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
43 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
44 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
45 |
46 | steps:
47 | - name: Checkout repository
48 | uses: actions/checkout@v4
49 |
50 | # Initializes the CodeQL tools for scanning.
51 | - name: Initialize CodeQL
52 | uses: github/codeql-action/init@v3
53 | with:
54 | languages: ${{ matrix.language }}
55 | # If you wish to specify custom queries, you can do so here or in a config file.
56 | # By default, queries listed here will override any specified in a config file.
57 | # Prefix the list here with "+" to use these queries and those in the config file.
58 |
59 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
60 | # queries: security-extended,security-and-quality
61 |
62 |
63 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
64 | # If this step fails, then you should remove it and run the build manually (see below)
65 | - name: Autobuild
66 | uses: github/codeql-action/autobuild@v3
67 |
68 | # ℹ️ Command-line programs to run using the OS shell.
69 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
70 |
71 | # If the Autobuild fails above, remove it and uncomment the following three lines.
72 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
73 |
74 | # - run: |
75 | # echo "Run, Build Application using script"
76 | # ./location_of_script_within_repo/buildscript.sh
77 |
78 | - name: Perform CodeQL Analysis
79 | uses: github/codeql-action/analyze@v3
80 | with:
81 | category: "/language:${{matrix.language}}"
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,swiftpackagemanager,xcode,macos
3 |
4 | ### macOS ###
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Icon must end with two \r
11 | Icon
12 |
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### Swift ###
34 | # Xcode
35 | #
36 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
37 |
38 | ## User settings
39 | xcuserdata/
40 |
41 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
42 | *.xcscmblueprint
43 | *.xccheckout
44 |
45 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
46 | build/
47 | DerivedData/
48 | *.moved-aside
49 | *.pbxuser
50 | !default.pbxuser
51 | *.mode1v3
52 | !default.mode1v3
53 | *.mode2v3
54 | !default.mode2v3
55 | *.perspectivev3
56 | !default.perspectivev3
57 |
58 | ## Obj-C/Swift specific
59 | *.hmap
60 |
61 | ## App packaging
62 | *.ipa
63 | *.dSYM.zip
64 | *.dSYM
65 |
66 | ## Playgrounds
67 | timeline.xctimeline
68 | playground.xcworkspace
69 |
70 | # Swift Package Manager
71 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
72 | Packages/
73 | Package.pins
74 | #Package.resolved
75 | *.xcodeproj
76 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
77 | # hence it is not needed unless you have added a package configuration file to your project
78 | .swiftpm
79 |
80 | .build/
81 |
82 | # CocoaPods
83 | # We recommend against adding the Pods directory to your .gitignore. However
84 | # you should judge for yourself, the pros and cons are mentioned at:
85 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
86 | # Pods/
87 | # Add this line if you want to avoid checking in source code from the Xcode workspace
88 | # *.xcworkspace
89 |
90 | # Carthage
91 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
92 | # Carthage/Checkouts
93 |
94 | Carthage/Build/
95 |
96 | # Accio dependency management
97 | Dependencies/
98 | .accio/
99 |
100 | # fastlane
101 | # It is recommended to not store the screenshots in the git repo.
102 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
103 | # For more information about the recommended setup visit:
104 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
105 |
106 | fastlane/report.xml
107 | fastlane/Preview.html
108 | fastlane/screenshots/**/*.png
109 | fastlane/test_output
110 |
111 | # Code Injection
112 | # After new code Injection tools there's a generated folder /iOSInjectionProject
113 | # https://github.com/johnno1962/injectionforxcode
114 |
115 | iOSInjectionProject/
116 |
117 | ### SwiftPackageManager ###
118 | Packages
119 | xcuserdata
120 | *.xcodeproj
121 |
122 |
123 | ### SwiftPM ###
124 |
125 |
126 | ### Xcode ###
127 | # Xcode
128 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
129 |
130 |
131 |
132 |
133 | ## Gcc Patch
134 | /*.gcno
135 |
136 | ### Xcode Patch ###
137 | *.xcodeproj/*
138 | !*.xcodeproj/project.pbxproj
139 | !*.xcodeproj/xcshareddata/
140 | !*.xcworkspace/contents.xcworkspacedata
141 | **/xcshareddata/WorkspaceSettings.xcsettings
142 |
143 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos
144 | Output
145 | .mint
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | swiftlint:
2 | config_file: .swiftlint.yml
3 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 | # Enable custom routing.
2 | RewriteEngine On
3 |
4 | RedirectMatch ^/$ /documentation/
5 |
6 | # Route documentation and tutorial pages.
7 | RewriteRule ^(documentation|tutorials)\/.*$ SyndiKit.doccarchive/index.html [L]
8 |
9 | RewriteRule /data/documentation.json SyndiKit.doccarchive/data/documentation/syndikit.json [L]
10 |
11 | # Route files and data for the documentation archive.
12 | #
13 | # If the file path doesn't exist in the website's root ...
14 | RewriteCond %{REQUEST_FILENAME} !-f
15 | RewriteCond %{REQUEST_FILENAME} !-d
16 |
17 | # ... route the request to that file path with the documentation archive.
18 | RewriteRule .* SyndiKit.doccarchive/$0 [L]
--------------------------------------------------------------------------------
/.periphery.yml:
--------------------------------------------------------------------------------
1 | retain_public: true
2 | targets:
3 | - SyndiKit
4 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [SyndiKit]
5 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5
2 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --indent 2
2 | --header strip
3 | --commas inline
4 | --disable wrapMultilineStatementBraces
5 | --extensionacl on-declarations
6 | --decimalgrouping 3,4
7 | --exclude .build, DerivedData
8 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - anyobject_protocol
3 | - array_init
4 | - attributes
5 | - closure_body_length
6 | - closure_end_indentation
7 | - closure_spacing
8 | - collection_alignment
9 | - conditional_returns_on_newline
10 | - contains_over_filter_count
11 | - contains_over_filter_is_empty
12 | - contains_over_first_not_nil
13 | - contains_over_range_nil_comparison
14 | - convenience_type
15 | - discouraged_object_literal
16 | - discouraged_optional_boolean
17 | - empty_collection_literal
18 | - empty_count
19 | - empty_string
20 | - empty_xctest_method
21 | - enum_case_associated_values_count
22 | - expiring_todo
23 | - explicit_acl
24 | - explicit_init
25 | - explicit_self
26 | - explicit_top_level_acl
27 | - fallthrough
28 | - fatal_error_message
29 | - file_header
30 | - file_name
31 | - file_name_no_space
32 | - file_types_order
33 | - first_where
34 | - flatmap_over_map_reduce
35 | - force_unwrapping
36 | - function_default_parameter_at_end
37 | - ibinspectable_in_extension
38 | - identical_operands
39 | - implicit_return
40 | - implicitly_unwrapped_optional
41 | - indentation_width
42 | - joined_default_parameter
43 | - last_where
44 | - legacy_multiple
45 | - legacy_random
46 | - literal_expression_end_indentation
47 | - lower_acl_than_parent
48 | - missing_docs
49 | - modifier_order
50 | - multiline_arguments
51 | - multiline_arguments_brackets
52 | - multiline_function_chains
53 | - multiline_literal_brackets
54 | - multiline_parameters
55 | - nimble_operator
56 | - nslocalizedstring_key
57 | - nslocalizedstring_require_bundle
58 | - number_separator
59 | - object_literal
60 | - operator_usage_whitespace
61 | - optional_enum_case_matching
62 | - overridden_super_call
63 | - override_in_extension
64 | - pattern_matching_keywords
65 | - prefer_self_type_over_type_of_self
66 | - prefer_zero_over_explicit_init
67 | - private_action
68 | - private_outlet
69 | - prohibited_interface_builder
70 | - prohibited_super_call
71 | - quick_discouraged_call
72 | - quick_discouraged_focused_test
73 | - quick_discouraged_pending_test
74 | - reduce_into
75 | - redundant_nil_coalescing
76 | - redundant_type_annotation
77 | - required_enum_case
78 | - single_test_class
79 | - sorted_first_last
80 | - sorted_imports
81 | - static_operator
82 | - strict_fileprivate
83 | - strong_iboutlet
84 | - switch_case_on_newline
85 | - toggle_bool
86 | - trailing_closure
87 | - type_contents_order
88 | - unavailable_function
89 | - unneeded_parentheses_in_closure_argument
90 | - unowned_variable_capture
91 | - untyped_error_in_catch
92 | - unused_declaration
93 | - unused_import
94 | - vertical_parameter_alignment_on_call
95 | - vertical_whitespace_between_cases
96 | - vertical_whitespace_closing_braces
97 | - vertical_whitespace_opening_braces
98 | - xct_specific_matcher
99 | - yoda_condition
100 | cyclomatic_complexity:
101 | - 6
102 | - 9
103 | file_length:
104 | - 200
105 | - 550
106 | function_body_length:
107 | - 15
108 | - 25
109 | function_parameter_count: 8
110 | line_length:
111 | - 90
112 | - 90
113 | type_name:
114 | excluded:
115 | - iTunesDuration
116 | - iTunesEpisode
117 | - iTunesOwner
118 | - iTunesImage
119 | identifier_name:
120 | excluded:
121 | - id
122 | excluded:
123 | - Tests
124 | - DerivedData
125 | - .build
126 | indentation_width:
127 | indentation_width: 2
128 |
--------------------------------------------------------------------------------
/Assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/Data/JSON/appfigures.youtube.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "Appfigures",
4 | "home_page_url": "https://www.youtube.com/channel/UCfvk5lomVsbHBYWciRVNHlg",
5 | "author": {
6 | "name": "Appfigures"
7 | },
8 | "items": [
9 | {
10 | "guid": "yt:video:Eb6EeOqdTw8",
11 | "url": "https://www.youtube.com/watch?v=Eb6EeOqdTw8",
12 | "title": "What are Custom Product Pages in iOS 15 - The Basics",
13 | "date_published": "2021-06-19T02:40:31.000Z",
14 | "author": {
15 | "name": "Appfigures"
16 | }
17 | },
18 | {
19 | "guid": "yt:video:nqoxX09Tnmo",
20 | "url": "https://www.youtube.com/watch?v=nqoxX09Tnmo",
21 | "title": "What's new for Subscription Apps in iOS 15?",
22 | "date_published": "2021-06-19T02:36:34.000Z",
23 | "author": {
24 | "name": "Appfigures"
25 | }
26 | },
27 | {
28 | "guid": "yt:video:1WvLV3LuGHQ",
29 | "url": "https://www.youtube.com/watch?v=1WvLV3LuGHQ",
30 | "title": "These New App Store Guidelines are Interesting...",
31 | "date_published": "2021-06-19T02:33:33.000Z",
32 | "author": {
33 | "name": "Appfigures"
34 | }
35 | },
36 | {
37 | "guid": "yt:video:BpwokwdeKQw",
38 | "url": "https://www.youtube.com/watch?v=BpwokwdeKQw",
39 | "title": "A/B Testing in the App Store is a BIG DEAL for App Makers!!!",
40 | "date_published": "2021-06-19T02:31:33.000Z",
41 | "author": {
42 | "name": "Appfigures"
43 | }
44 | },
45 | {
46 | "guid": "yt:video:Er_wPp7LVGo",
47 | "url": "https://www.youtube.com/watch?v=Er_wPp7LVGo",
48 | "title": "What Can You Do with Custom Product Pages in the New App Store?",
49 | "date_published": "2021-06-19T02:26:39.000Z",
50 | "author": {
51 | "name": "Appfigures"
52 | }
53 | },
54 | {
55 | "guid": "yt:video:nHUmqDxVTEc",
56 | "url": "https://www.youtube.com/watch?v=nHUmqDxVTEc",
57 | "title": "StoreKit 2 Will Forever Change How Apps Monetize",
58 | "date_published": "2021-06-19T02:16:25.000Z",
59 | "author": {
60 | "name": "Appfigures"
61 | }
62 | },
63 | {
64 | "guid": "yt:video:CtoD1ZZkRxY",
65 | "url": "https://www.youtube.com/watch?v=CtoD1ZZkRxY",
66 | "title": "AF Chats - WWDC 2021 Recap with Joe Cieplinski",
67 | "date_published": "2021-06-17T15:54:19.000Z",
68 | "author": {
69 | "name": "Appfigures"
70 | }
71 | },
72 | {
73 | "guid": "yt:video:M4SB2Ei94cI",
74 | "url": "https://www.youtube.com/watch?v=M4SB2Ei94cI",
75 | "title": "AF Chats - Anatomy of an App Store Scam with Kosta Eleftheriou",
76 | "date_published": "2021-06-07T10:45:01.000Z",
77 | "author": {
78 | "name": "Appfigures"
79 | }
80 | },
81 | {
82 | "guid": "yt:video:HHXn70jc-qU",
83 | "url": "https://www.youtube.com/watch?v=HHXn70jc-qU",
84 | "title": "This Week in Apps #64 - TikTok the King Maker",
85 | "date_published": "2021-06-05T14:05:31.000Z",
86 | "author": {
87 | "name": "Appfigures"
88 | }
89 | },
90 | {
91 | "guid": "yt:video:sG5Hm-h3NgE",
92 | "url": "https://www.youtube.com/watch?v=sG5Hm-h3NgE",
93 | "title": "This Week in Apps #63 - The Streaming Wars Continue",
94 | "date_published": "2021-06-05T14:05:15.000Z",
95 | "author": {
96 | "name": "Appfigures"
97 | }
98 | },
99 | {
100 | "guid": "yt:video:x2ZdYZ5HGnI",
101 | "url": "https://www.youtube.com/watch?v=x2ZdYZ5HGnI",
102 | "title": "This Week in Apps #62 - The Money's Still Good",
103 | "date_published": "2021-05-22T01:06:34.000Z",
104 | "author": {
105 | "name": "Appfigures"
106 | }
107 | },
108 | {
109 | "guid": "yt:video:LAZWoCvsN-w",
110 | "url": "https://www.youtube.com/watch?v=LAZWoCvsN-w",
111 | "title": "This Week in Apps #61 - ATT Continues to Wreak Havoc, Peacock Grows Up, and more.",
112 | "date_published": "2021-05-22T01:05:45.000Z",
113 | "author": {
114 | "name": "Appfigures"
115 | }
116 | },
117 | {
118 | "guid": "yt:video:tPKTYwitA5Q",
119 | "url": "https://www.youtube.com/watch?v=tPKTYwitA5Q",
120 | "title": "Live App Teardown (May. 2021) - Sunbasket and Color Defense",
121 | "date_published": "2021-05-13T14:13:11.000Z",
122 | "author": {
123 | "name": "Appfigures"
124 | }
125 | },
126 | {
127 | "guid": "yt:video:Hhz9iZrI1oQ",
128 | "url": "https://www.youtube.com/watch?v=Hhz9iZrI1oQ",
129 | "title": "This Week in Apps #60 - They're At It Again",
130 | "date_published": "2021-05-10T17:33:13.000Z",
131 | "author": {
132 | "name": "Appfigures"
133 | }
134 | },
135 | {
136 | "guid": "yt:video:H2dh8KoYG8A",
137 | "url": "https://www.youtube.com/watch?v=H2dh8KoYG8A",
138 | "title": "This Week in Apps #59 - Do They Even Care?",
139 | "date_published": "2021-05-01T14:00:03.000Z",
140 | "author": {
141 | "name": "Appfigures"
142 | }
143 | }
144 | ]
145 | }
--------------------------------------------------------------------------------
/Data/JSON/kilo.youtube.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "Kilo Loco",
4 | "home_page_url": "https://www.youtube.com/channel/UCv75sKQFFIenWHrprnrR9aA",
5 | "author": {
6 | "name": "Kilo Loco"
7 | },
8 | "items": [
9 | {
10 | "guid": "yt:video:3hccNoPE59U",
11 | "url": "https://www.youtube.com/watch?v=3hccNoPE59U",
12 | "title": "What Developers Thought of WWDC 2021",
13 | "date_published": "2021-06-10T15:28:17.000Z",
14 | "author": {
15 | "name": "Kilo Loco"
16 | }
17 | },
18 | {
19 | "guid": "yt:video:QW-oEfH_lgg",
20 | "url": "https://www.youtube.com/watch?v=QW-oEfH_lgg",
21 | "title": "Cache Images and URLs with Flutter | Day 27 - #30DaysOfFlutter",
22 | "date_published": "2021-05-11T20:01:53.000Z",
23 | "author": {
24 | "name": "Kilo Loco"
25 | }
26 | },
27 | {
28 | "guid": "yt:video:0Tp70eB5WrM",
29 | "url": "https://www.youtube.com/watch?v=0Tp70eB5WrM",
30 | "title": "Flutter: Social Media App",
31 | "date_published": "2021-04-20T13:55:02.000Z",
32 | "author": {
33 | "name": "Kilo Loco"
34 | }
35 | },
36 | {
37 | "guid": "yt:video:5E4T-Qb3l18",
38 | "url": "https://www.youtube.com/watch?v=5E4T-Qb3l18",
39 | "title": "Upload Images to Amplify Using Flutter | Day 26 - #30DaysOfFlutter",
40 | "date_published": "2021-04-20T03:27:01.000Z",
41 | "author": {
42 | "name": "Kilo Loco"
43 | }
44 | },
45 | {
46 | "guid": "yt:video:8XyRqtZCnrU",
47 | "url": "https://www.youtube.com/watch?v=8XyRqtZCnrU",
48 | "title": "Flutter: Social Media Post Feed",
49 | "date_published": "2021-04-14T15:05:50.000Z",
50 | "author": {
51 | "name": "Kilo Loco"
52 | }
53 | },
54 | {
55 | "guid": "yt:video:2Hoq6BafoEA",
56 | "url": "https://www.youtube.com/watch?v=2Hoq6BafoEA",
57 | "title": "Flutter: Remote Image Loading",
58 | "date_published": "2021-04-12T15:15:17.000Z",
59 | "author": {
60 | "name": "Kilo Loco"
61 | }
62 | },
63 | {
64 | "guid": "yt:video:WPhfsCSSM7k",
65 | "url": "https://www.youtube.com/watch?v=WPhfsCSSM7k",
66 | "title": "Flutter: Remote Image Loading",
67 | "date_published": "2021-04-07T15:06:12.000Z",
68 | "author": {
69 | "name": "Kilo Loco"
70 | }
71 | },
72 | {
73 | "guid": "yt:video:a4ix-CpsLLA",
74 | "url": "https://www.youtube.com/watch?v=a4ix-CpsLLA",
75 | "title": "Flutter: New Post Functionality for Social Media App",
76 | "date_published": "2021-04-06T15:49:50.000Z",
77 | "author": {
78 | "name": "Kilo Loco"
79 | }
80 | },
81 | {
82 | "guid": "yt:video:Rgw8k6MuEuY",
83 | "url": "https://www.youtube.com/watch?v=Rgw8k6MuEuY",
84 | "title": "Flutter: New Post UI for Social Media App",
85 | "date_published": "2021-04-05T14:52:51.000Z",
86 | "author": {
87 | "name": "Kilo Loco"
88 | }
89 | },
90 | {
91 | "guid": "yt:video:gho0MzVkS4c",
92 | "url": "https://www.youtube.com/watch?v=gho0MzVkS4c",
93 | "title": "Flutter with Swift and Xcode",
94 | "date_published": "2021-04-01T16:47:18.000Z",
95 | "author": {
96 | "name": "Kilo Loco"
97 | }
98 | },
99 | {
100 | "guid": "yt:video:BMBZBjfnqy8",
101 | "url": "https://www.youtube.com/watch?v=BMBZBjfnqy8",
102 | "title": "Flutter: Create New Post on Social Media App",
103 | "date_published": "2021-03-31T15:06:08.000Z",
104 | "author": {
105 | "name": "Kilo Loco"
106 | }
107 | },
108 | {
109 | "guid": "yt:video:jf6sU7fAeKE",
110 | "url": "https://www.youtube.com/watch?v=jf6sU7fAeKE",
111 | "title": "Flutter: Save Post Data",
112 | "date_published": "2021-03-30T16:02:41.000Z",
113 | "author": {
114 | "name": "Kilo Loco"
115 | }
116 | },
117 | {
118 | "guid": "yt:video:7iZAZ0CwFKI",
119 | "url": "https://www.youtube.com/watch?v=7iZAZ0CwFKI",
120 | "title": "Camera and Photo Gallery Using Image Picker with Flutter | Day 25 - #30DaysOfFlutter",
121 | "date_published": "2021-03-30T04:44:18.000Z",
122 | "author": {
123 | "name": "Kilo Loco"
124 | }
125 | },
126 | {
127 | "guid": "yt:video:gnxhNIle7VE",
128 | "url": "https://www.youtube.com/watch?v=gnxhNIle7VE",
129 | "title": "Flutter: Social Media Feed",
130 | "date_published": "2021-03-29T16:06:48.000Z",
131 | "author": {
132 | "name": "Kilo Loco"
133 | }
134 | },
135 | {
136 | "guid": "yt:video:SBJFl-3wqx8",
137 | "url": "https://www.youtube.com/watch?v=SBJFl-3wqx8",
138 | "title": "User Profile UI with BLoC and Flutter | Day 24 - #30DaysOfFlutter",
139 | "date_published": "2021-03-25T17:29:43.000Z",
140 | "author": {
141 | "name": "Kilo Loco"
142 | }
143 | }
144 | ]
145 | }
--------------------------------------------------------------------------------
/Data/JSON/stewart.youtube.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "Stewart Lynch",
4 | "home_page_url": "https://www.youtube.com/channel/UCOWdR4sFkmolWkU2fg669Gg",
5 | "author": {
6 | "name": "Stewart Lynch"
7 | },
8 | "items": [
9 | {
10 | "guid": "yt:video:QrTChgzseVk",
11 | "url": "https://www.youtube.com/watch?v=QrTChgzseVk",
12 | "title": "SwiftUI Login Screen Workflow",
13 | "date_published": "2021-06-20T13:44:57.000Z",
14 | "author": {
15 | "name": "Stewart Lynch"
16 | }
17 | },
18 | {
19 | "guid": "yt:video:4ochXtdrd70",
20 | "url": "https://www.youtube.com/watch?v=4ochXtdrd70",
21 | "title": "Sorting Objects in Swift with Multiple Criteria",
22 | "date_published": "2021-06-13T13:45:37.000Z",
23 | "author": {
24 | "name": "Stewart Lynch"
25 | }
26 | },
27 | {
28 | "guid": "yt:video:lF9fOeUwWF8",
29 | "url": "https://www.youtube.com/watch?v=lF9fOeUwWF8",
30 | "title": "Switching Themes in SwiftUI",
31 | "date_published": "2021-06-06T13:54:22.000Z",
32 | "author": {
33 | "name": "Stewart Lynch"
34 | }
35 | },
36 | {
37 | "guid": "yt:video:r29-06lbLmQ",
38 | "url": "https://www.youtube.com/watch?v=r29-06lbLmQ",
39 | "title": "CollectionView Paging Layout for SwiftUI and Layout Designer",
40 | "date_published": "2021-05-30T13:48:03.000Z",
41 | "author": {
42 | "name": "Stewart Lynch"
43 | }
44 | },
45 | {
46 | "guid": "yt:video:u1kGK9RTEH4",
47 | "url": "https://www.youtube.com/watch?v=u1kGK9RTEH4",
48 | "title": "Channel Watcher - An Open Source, SwiftUI, Combine, iPhone, iPad, Mac app",
49 | "date_published": "2021-05-23T13:48:08.000Z",
50 | "author": {
51 | "name": "Stewart Lynch"
52 | }
53 | },
54 | {
55 | "guid": "yt:video:tuHcwRe61KE",
56 | "url": "https://www.youtube.com/watch?v=tuHcwRe61KE",
57 | "title": "Demystifying Completion Handlers and Asynchronous Functions",
58 | "date_published": "2021-05-16T13:51:55.000Z",
59 | "author": {
60 | "name": "Stewart Lynch"
61 | }
62 | },
63 | {
64 | "guid": "yt:video:kCJyhG8zjvY",
65 | "url": "https://www.youtube.com/watch?v=kCJyhG8zjvY",
66 | "title": "Navigation Bar Styling in SwiftUI",
67 | "date_published": "2021-05-09T13:28:24.000Z",
68 | "author": {
69 | "name": "Stewart Lynch"
70 | }
71 | },
72 | {
73 | "guid": "yt:video:NY0LFoHQUbk",
74 | "url": "https://www.youtube.com/watch?v=NY0LFoHQUbk",
75 | "title": "Introduction to Generics in Swift",
76 | "date_published": "2021-05-02T13:35:13.000Z",
77 | "author": {
78 | "name": "Stewart Lynch"
79 | }
80 | },
81 | {
82 | "guid": "yt:video:JmLGI3hdh6c",
83 | "url": "https://www.youtube.com/watch?v=JmLGI3hdh6c",
84 | "title": "Combine CurrentValueSubject",
85 | "date_published": "2021-04-29T13:50:35.000Z",
86 | "author": {
87 | "name": "Stewart Lynch"
88 | }
89 | },
90 | {
91 | "guid": "yt:video:Cy7zokKsB4M",
92 | "url": "https://www.youtube.com/watch?v=Cy7zokKsB4M",
93 | "title": "Combine @Published properties and Just Publisher",
94 | "date_published": "2021-04-27T13:44:04.000Z",
95 | "author": {
96 | "name": "Stewart Lynch"
97 | }
98 | },
99 | {
100 | "guid": "yt:video:1YtC5wVtiZ0",
101 | "url": "https://www.youtube.com/watch?v=1YtC5wVtiZ0",
102 | "title": "Combine PassthroughSubject and SwiftUI",
103 | "date_published": "2021-04-25T13:38:24.000Z",
104 | "author": {
105 | "name": "Stewart Lynch"
106 | }
107 | },
108 | {
109 | "guid": "yt:video:dpmy-msRlCA",
110 | "url": "https://www.youtube.com/watch?v=dpmy-msRlCA",
111 | "title": "Responsible Error Handling In SwiftUI",
112 | "date_published": "2021-04-22T13:38:47.000Z",
113 | "author": {
114 | "name": "Stewart Lynch"
115 | }
116 | },
117 | {
118 | "guid": "yt:video:PC0OXxAH-O0",
119 | "url": "https://www.youtube.com/watch?v=PC0OXxAH-O0",
120 | "title": "File DataPersistence in SwiftUI",
121 | "date_published": "2021-04-20T13:33:06.000Z",
122 | "author": {
123 | "name": "Stewart Lynch"
124 | }
125 | },
126 | {
127 | "guid": "yt:video:EeSzURDoGCQ",
128 | "url": "https://www.youtube.com/watch?v=EeSzURDoGCQ",
129 | "title": "Building the ToDo app in SwiftUI",
130 | "date_published": "2021-04-18T13:43:42.000Z",
131 | "author": {
132 | "name": "Stewart Lynch"
133 | }
134 | },
135 | {
136 | "guid": "yt:video:Uf_pmEwmpJo",
137 | "url": "https://www.youtube.com/watch?v=Uf_pmEwmpJo",
138 | "title": "Not another SwiftUI ToDo App!!!",
139 | "date_published": "2021-04-14T13:47:46.000Z",
140 | "author": {
141 | "name": "Stewart Lynch"
142 | }
143 | }
144 | ]
145 | }
--------------------------------------------------------------------------------
/Data/JSON/tundsdev.youtube.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "tundsdev",
4 | "home_page_url": "https://www.youtube.com/channel/UC7AuV86ZjR3YaEdb5USNvWQ",
5 | "author": {
6 | "name": "tundsdev"
7 | },
8 | "items": [
9 | {
10 | "guid": "yt:video:l-iN0kY_bmg",
11 | "url": "https://www.youtube.com/watch?v=l-iN0kY_bmg",
12 | "title": "Setup SwiftUI App with Firebase SPM (Swift Package Manager, SwiftUI Tutorial, Firebase Crashlytics)",
13 | "date_published": "2021-06-13T15:00:31.000Z",
14 | "author": {
15 | "name": "tundsdev"
16 | }
17 | },
18 | {
19 | "guid": "yt:video:Dhfdg_6ksy8",
20 | "url": "https://www.youtube.com/watch?v=Dhfdg_6ksy8",
21 | "title": "Refactoring code in SwiftUI (SwiftUI Tutorial, SwiftUI Onboarding, SwiftUI)",
22 | "date_published": "2021-06-04T15:17:40.000Z",
23 | "author": {
24 | "name": "tundsdev"
25 | }
26 | },
27 | {
28 | "guid": "yt:video:v-6qU-DqaZo",
29 | "url": "https://www.youtube.com/watch?v=v-6qU-DqaZo",
30 | "title": "SwiftUI Form Validation using Combine (SwiftUI Tutorial, Regular Expressions, Combine Framework)",
31 | "date_published": "2021-05-30T15:00:33.000Z",
32 | "author": {
33 | "name": "tundsdev"
34 | }
35 | },
36 | {
37 | "guid": "yt:video:EWjLHRbQK4I",
38 | "url": "https://www.youtube.com/watch?v=EWjLHRbQK4I",
39 | "title": "Form Validation in UIKit using Combine Framework (Combine Framework, UIKit Tutorial, UIKit Form)",
40 | "date_published": "2021-05-23T15:00:04.000Z",
41 | "author": {
42 | "name": "tundsdev"
43 | }
44 | },
45 | {
46 | "guid": "yt:video:4YIckjckiWI",
47 | "url": "https://www.youtube.com/watch?v=4YIckjckiWI",
48 | "title": "Create a Dynamic Form in UIKit (Compositional Layout, Diffable Data Source, UICollectionView)",
49 | "date_published": "2021-05-16T15:00:02.000Z",
50 | "author": {
51 | "name": "tundsdev"
52 | }
53 | },
54 | {
55 | "guid": "yt:video:F5aDfGNdsac",
56 | "url": "https://www.youtube.com/watch?v=F5aDfGNdsac",
57 | "title": "Getting Started With Unit Testing in Swift (XCTest, Test Cases, Code Coverage)",
58 | "date_published": "2021-05-09T15:00:03.000Z",
59 | "author": {
60 | "name": "tundsdev"
61 | }
62 | },
63 | {
64 | "guid": "yt:video:HXYqU5ClIk4",
65 | "url": "https://www.youtube.com/watch?v=HXYqU5ClIk4",
66 | "title": "Build Onboarding Screens in SwiftUI (TabView, PageTabViewStyle, SwiftUI Tutorial, @AppStorage)",
67 | "date_published": "2021-05-02T15:00:15.000Z",
68 | "author": {
69 | "name": "tundsdev"
70 | }
71 | },
72 | {
73 | "guid": "yt:video:X2m0f2NoB10",
74 | "url": "https://www.youtube.com/watch?v=X2m0f2NoB10",
75 | "title": "Getting Started with Combine (Practical Combine Framework Examples in UIKit & SwiftUI)",
76 | "date_published": "2021-04-25T15:00:13.000Z",
77 | "author": {
78 | "name": "tundsdev"
79 | }
80 | },
81 | {
82 | "guid": "yt:video:sYVXpRFmv54",
83 | "url": "https://www.youtube.com/watch?v=sYVXpRFmv54",
84 | "title": "How I Became A Lead App Developer Without A Computer Science Degree (Self Taught Programmer)",
85 | "date_published": "2021-04-18T14:10:35.000Z",
86 | "author": {
87 | "name": "tundsdev"
88 | }
89 | },
90 | {
91 | "guid": "yt:video:j7a4jvHz4MM",
92 | "url": "https://www.youtube.com/watch?v=j7a4jvHz4MM",
93 | "title": "Dark Mode in SwiftUI using @Binding & @AppStorage (SwiftUI Tutorial, @Binding, @AppStorage)",
94 | "date_published": "2021-04-11T15:00:16.000Z",
95 | "author": {
96 | "name": "tundsdev"
97 | }
98 | },
99 | {
100 | "guid": "yt:video:cmaZUw7GSGs",
101 | "url": "https://www.youtube.com/watch?v=cmaZUw7GSGs",
102 | "title": "SwiftUI Link, Open links in Safari in SwiftUI (SwiftUI Tutorial, SwiftUI Link View)",
103 | "date_published": "2021-04-07T15:06:53.000Z",
104 | "author": {
105 | "name": "tundsdev"
106 | }
107 | },
108 | {
109 | "guid": "yt:video:fktqZRRQgyU",
110 | "url": "https://www.youtube.com/watch?v=fktqZRRQgyU",
111 | "title": "Build A Settings Screen In SwiftUI (SwiftUI Tutorial, SwiftUI Form, FormView)",
112 | "date_published": "2021-04-06T15:00:13.000Z",
113 | "author": {
114 | "name": "tundsdev"
115 | }
116 | },
117 | {
118 | "guid": "yt:video:-kxqoTCgxkI",
119 | "url": "https://www.youtube.com/watch?v=-kxqoTCgxkI",
120 | "title": "How to land your first iOS developer job (Self Taught iOS Developer, iOS developer, SwiftUI)",
121 | "date_published": "2021-04-05T14:00:00.000Z",
122 | "author": {
123 | "name": "tundsdev"
124 | }
125 | },
126 | {
127 | "guid": "yt:video:TZ3-iQ462Q8",
128 | "url": "https://www.youtube.com/watch?v=TZ3-iQ462Q8",
129 | "title": "Using UIKit in SwiftUI (SwiftUI Tutorial, SFSafariViewController, Xcode 12)",
130 | "date_published": "2021-03-28T15:00:09.000Z",
131 | "author": {
132 | "name": "tundsdev"
133 | }
134 | },
135 | {
136 | "guid": "yt:video:cfwEt__pnvA",
137 | "url": "https://www.youtube.com/watch?v=cfwEt__pnvA",
138 | "title": "SwiftUI Redacted Tutorial - Loading Skeleton View - (SwiftUI Tutorial, Xcode 12, SwiftUI 2.0)",
139 | "date_published": "2021-03-21T15:09:18.000Z",
140 | "author": {
141 | "name": "tundsdev"
142 | }
143 | }
144 | ]
145 | }
--------------------------------------------------------------------------------
/Data/JSON/vincent.youtube.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "https://jsonfeed.org/version/1",
3 | "title": "Vincent Pradeilles",
4 | "home_page_url": "https://www.youtube.com/channel/UCjkoQk5fOk6lH-shlm53vlw",
5 | "author": {
6 | "name": "Vincent Pradeilles"
7 | },
8 | "items": [
9 | {
10 | "guid": "yt:video:wUoDhf4tzvY",
11 | "url": "https://www.youtube.com/watch?v=wUoDhf4tzvY",
12 | "title": "Swift Tips #35 - reduce",
13 | "date_published": "2021-06-17T15:16:45.000Z",
14 | "author": {
15 | "name": "Vincent Pradeilles"
16 | }
17 | },
18 | {
19 | "guid": "yt:video:p3qdi6KyzDk",
20 | "url": "https://www.youtube.com/watch?v=p3qdi6KyzDk",
21 | "title": "Five weird but valid Swift syntaxes 😮",
22 | "date_published": "2021-06-15T14:58:05.000Z",
23 | "author": {
24 | "name": "Vincent Pradeilles"
25 | }
26 | },
27 | {
28 | "guid": "yt:video:rjw7Fesc1tk",
29 | "url": "https://www.youtube.com/watch?v=rjw7Fesc1tk",
30 | "title": "So what got the iOS community excited at WWDC 2021?",
31 | "date_published": "2021-06-10T15:08:51.000Z",
32 | "author": {
33 | "name": "Vincent Pradeilles"
34 | }
35 | },
36 | {
37 | "guid": "yt:video:3OZ0FBOV0LQ",
38 | "url": "https://www.youtube.com/watch?v=3OZ0FBOV0LQ",
39 | "title": "My highlights of WWDC 2021 so far!",
40 | "date_published": "2021-06-08T14:52:46.000Z",
41 | "author": {
42 | "name": "Vincent Pradeilles"
43 | }
44 | },
45 | {
46 | "guid": "yt:video:Oi_C7dD6ats",
47 | "url": "https://www.youtube.com/watch?v=Oi_C7dD6ats",
48 | "title": "How to SwiftUI #01 - Displaying images on top of each other",
49 | "date_published": "2021-06-03T15:03:30.000Z",
50 | "author": {
51 | "name": "Vincent Pradeilles"
52 | }
53 | },
54 | {
55 | "guid": "yt:video:mzsz_Tit1HA",
56 | "url": "https://www.youtube.com/watch?v=mzsz_Tit1HA",
57 | "title": "Don't use this syntax! (Or you'll get a retain cycle 😱)",
58 | "date_published": "2021-06-01T14:55:12.000Z",
59 | "author": {
60 | "name": "Vincent Pradeilles"
61 | }
62 | },
63 | {
64 | "guid": "yt:video:2vZz1v-2v1E",
65 | "url": "https://www.youtube.com/watch?v=2vZz1v-2v1E",
66 | "title": "Looking at some Shit Swift Code 💩, Live! 🎙 with Jérôme Alves",
67 | "date_published": "2021-05-31T15:19:17.000Z",
68 | "author": {
69 | "name": "Vincent Pradeilles"
70 | }
71 | },
72 | {
73 | "guid": "yt:video:_VswqQqVlHo",
74 | "url": "https://www.youtube.com/watch?v=_VswqQqVlHo",
75 | "title": "Swift Tips #34 - defer",
76 | "date_published": "2021-05-27T15:01:18.000Z",
77 | "author": {
78 | "name": "Vincent Pradeilles"
79 | }
80 | },
81 | {
82 | "guid": "yt:video:58IChtjCPGM",
83 | "url": "https://www.youtube.com/watch?v=58IChtjCPGM",
84 | "title": "Experimenting with Result Builders: it's harder than I thought 😰",
85 | "date_published": "2021-05-25T14:58:45.000Z",
86 | "author": {
87 | "name": "Vincent Pradeilles"
88 | }
89 | },
90 | {
91 | "guid": "yt:video:6Abf8LDRRkI",
92 | "url": "https://www.youtube.com/watch?v=6Abf8LDRRkI",
93 | "title": "An introduction to Result Builders, Live! 🎙 with Antoine v.d. Lee",
94 | "date_published": "2021-05-23T16:19:58.000Z",
95 | "author": {
96 | "name": "Vincent Pradeilles"
97 | }
98 | },
99 | {
100 | "guid": "yt:video:MZPeg8dqqzI",
101 | "url": "https://www.youtube.com/watch?v=MZPeg8dqqzI",
102 | "title": "Swift Tips #33 - CaseIterable",
103 | "date_published": "2021-05-20T15:05:51.000Z",
104 | "author": {
105 | "name": "Vincent Pradeilles"
106 | }
107 | },
108 | {
109 | "guid": "yt:video:zhnqQCc032c",
110 | "url": "https://www.youtube.com/watch?v=zhnqQCc032c",
111 | "title": "Why should you use enums and associated values? 🤔",
112 | "date_published": "2021-05-18T14:59:43.000Z",
113 | "author": {
114 | "name": "Vincent Pradeilles"
115 | }
116 | },
117 | {
118 | "guid": "yt:video:F4PJBEBVDYM",
119 | "url": "https://www.youtube.com/watch?v=F4PJBEBVDYM",
120 | "title": "Let's answer iOS Interview questions! (Using @Paul Hudson website)",
121 | "date_published": "2021-05-16T15:58:01.000Z",
122 | "author": {
123 | "name": "Vincent Pradeilles"
124 | }
125 | },
126 | {
127 | "guid": "yt:video:FWjM8ESYgCA",
128 | "url": "https://www.youtube.com/watch?v=FWjM8ESYgCA",
129 | "title": "Swift Community #6 GraphQL (with Ellen Shapiro)",
130 | "date_published": "2021-05-13T14:56:01.000Z",
131 | "author": {
132 | "name": "Vincent Pradeilles"
133 | }
134 | },
135 | {
136 | "guid": "yt:video:4TTSZZkdOs4",
137 | "url": "https://www.youtube.com/watch?v=4TTSZZkdOs4",
138 | "title": "Five tips to improve your Swift code 💪",
139 | "date_published": "2021-05-11T15:01:06.000Z",
140 | "author": {
141 | "name": "Vincent Pradeilles"
142 | }
143 | }
144 | ]
145 | }
--------------------------------------------------------------------------------
/Data/OPML/category.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Illustrating the category attribute
5 | Mon, 31 Oct 2005 19:23:00 GMT
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Data/OPML/category_invalidExpansionState.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Illustrating the category attribute
5 | Mon, 31 Oct 2005 19:23:00 GMT
6 | one, two, three
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Data/OPML/directory.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | scriptingNewsDirectory.opml
5 | Thu, 13 Oct 2005 15:34:07 GMT
6 | Tue, 25 Oct 2005 21:33:57 GMT
7 | Dave Winer
8 | dwiner@yahoo.com
9 |
10 | 1
11 | 105
12 | 466
13 | 386
14 | 964
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Data/OPML/placesLived.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | placesLived.opml
5 | Mon, 27 Feb 2006 12:09:48 GMT
6 | Mon, 27 Feb 2006 12:11:44 GMT
7 | Dave Winer
8 | http://www.opml.org/profiles/sendMail?usernum=1
9 | 1, 2, 5, 10, 13, 15
10 | 1
11 | 242
12 | 329
13 | 665
14 | 547
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Data/OPML/simpleScript.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | workspace.userlandsamples.doSomeUpstreaming
5 | Mon, 11 Feb 2002 22:48:02 GMT
6 | Sun, 30 Oct 2005 03:30:17 GMT
7 | Dave Winer
8 | dwiner@yahoo.com
9 | 1, 2, 4
10 | 1
11 | 74
12 | 41
13 | 314
14 | 475
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Data/OPML/states.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | states.opml
5 | Tue, 15 Mar 2005 16:35:45 GMT
6 | Thu, 14 Jul 2005 23:41:05 GMT
7 | Dave Winer
8 | dave@scripting.com
9 | 1, 6, 13, 16, 18, 20
10 | 1
11 | 106
12 | 106
13 | 558
14 | 479
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Data/OPML/subscriptionList.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mySubscriptions.opml
5 | Sat, 18 Jun 2005 12:11:52 GMT
6 | Tue, 02 Aug 2005 21:42:48 GMT
7 | Dave Winer
8 | dave@scripting.com
9 |
10 | 1
11 | 61
12 | 304
13 | 562
14 | 842
15 |
16 |
17 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Data/urls.tsv:
--------------------------------------------------------------------------------
1 | https://swiftpackageindex.com/releases.rss swiftpackageindex
2 | https://mjtsai.com/blog/feed/ mjtsai
3 | http://feeds.ideveloper.co/ideveloperpodcast.xml ideveloper
4 | http://www.mokacoding.com/feed.xml mokacoding
5 | https://www.it-guy.com/podcasts/ajtm_feed.xml.rss it-guy
6 | https://developer.apple.com/news/rss/news.rss apple.developer
7 | https://wwdcnotes.com/feed.rss wwdcnotes
8 | https://developer.apple.com/news/releases/rss/releases.rss apple.releases
9 | https://www.relay.fm/radar/feed radar
10 | https://www.swiftbysundell.com/posts?format=RSS swiftbysundell
11 | https://swiftweeklybrief.com/feed.xml swiftweeklybrief
12 | https://cocoacasts.com/feed cocoacasts
13 | https://www.fivestars.blog/feed.xml fivestars
14 | https://www.raywenderlich.com/feed/podcast raywenderlich
15 | https://feeds.transistor.fm/empowerapps-show empowerapps-show
16 | https://www.andyibanez.com/tags/apple/index.xml andyibanez
17 | http://ios-goodies.com/rss ios-goodies
18 | https://blog.timac.org/index.xml timac
19 | https://www.revenuecat.com/blog/rss.xml revenuecat
20 | https://atomicbird.com/index.xml atomicbird
21 | http://www.enekoalonso.com/feed.xml enekoalonso
22 | https://iosdevweekly.com/issues.rss iosdevweekly
23 | https://www.avanderlee.com/feed/ avanderlee
24 | https://mecid.github.io/feed.xml mecid
25 | https://rhonabwy.com/feed/ rhonabwy
26 | https://www.hackingwithswift.com/articles/rss hackingwithswift
27 | https://www.advancedswift.com/rss advancedswift
28 | https://www.donnywals.com/feed/ donnywals
29 | https://www.youtube.com/feeds/videos.xml?channel_id=UC7AuV86ZjR3YaEdb5USNvWQ tundsdev.youtube
30 | https://www.youtube.com/feeds/videos.xml?channel_id=UCfvk5lomVsbHBYWciRVNHlg appfigures.youtube
31 | https://www.youtube.com/feeds/videos.xml?channel_id=UCjkoQk5fOk6lH-shlm53vlw vincent.youtube
32 | https://www.youtube.com/feeds/videos.xml?channel_id=UCOWdR4sFkmolWkU2fg669Gg stewart.youtube
33 | https://www.youtube.com/feeds/videos.xml?channel_id=UCv75sKQFFIenWHrprnrR9aA kilo.youtube
34 | https://us12.campaign-archive.com/feed?u=cb3bba007ed171091f55c47f0&id=584d0d5c40 brightdigit.newsletter
35 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM httpd:alpine
2 |
3 | RUN rm /usr/local/apache2/htdocs/index.html
4 | COPY Scripts/httpd.conf /usr/local/apache2/conf/httpd.conf
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 BrightDigit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Mintfile:
--------------------------------------------------------------------------------
1 | nicklockwood/SwiftFormat@0.47.0
2 | realm/SwiftLint@0.41.0
3 | peripheryapp/periphery@2.18.0
4 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "XMLCoder",
6 | "repositoryURL": "https://github.com/CoreOffice/XMLCoder",
7 | "state": {
8 | "branch": null,
9 | "revision": "b1e944cbd0ef33787b13f639a5418d55b3bed501",
10 | "version": "0.17.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // swiftlint:disable explicit_top_level_acl explicit_acl
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SyndiKit",
7 | products: [
8 | .library(
9 | name: "SyndiKit",
10 | targets: ["SyndiKit"]
11 | )
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1")
15 | ],
16 | targets: [
17 | .target(
18 | name: "SyndiKit",
19 | dependencies: ["XMLCoder"]
20 | ),
21 | .testTarget(
22 | name: "SyndiKitTests",
23 | dependencies: ["SyndiKit"]
24 | )
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Scripts/build_test_data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mkdir -p Data/XML
3 | mkdir -p Data/JSON
4 | {
5 | IFS=" "
6 | while read -r url file; do
7 | #echo "$url > $file"
8 | curl $url -Lo Data/XML/$file.xml
9 | curl https://feed2json.org/convert?url=$url -o Data/JSON/$file.json
10 | done
11 | } < ./Tests/SyndiKitTests/urls.tsv
--------------------------------------------------------------------------------
/Scripts/docc.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | xcodebuild docbuild -scheme SyndiKit -destination 'platform=macOS' -derivedDataPath DerivedData
3 | docker run -d -p 8080:80 -v "$(pwd)/DerivedData/Build/Products/Debug:/usr/local/apache2/htdocs/" --rm -it $(docker build -q .)
4 |
--------------------------------------------------------------------------------
/Scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -z "$SRCROOT" ]; then
4 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
5 | PACKAGE_DIR="${SCRIPT_DIR}/.."
6 | else
7 | PACKAGE_DIR="${SRCROOT}"
8 | fi
9 |
10 | if [ -z "$GITHUB_ACTION" ]; then
11 | MINT_CMD="/opt/homebrew/bin/mint"
12 | else
13 | MINT_CMD="mint"
14 | fi
15 |
16 | export MINT_PATH="$PACKAGE_DIR/.mint"
17 | MINT_ARGS="-n -m $PACKAGE_DIR/Mintfile --silent"
18 | MINT_RUN="$MINT_CMD run $MINT_ARGS"
19 |
20 | pushd $PACKAGE_DIR
21 |
22 | $MINT_CMD bootstrap -m Mintfile
23 |
24 | if [ "$LINT_MODE" == "NONE" ]; then
25 | exit
26 | elif [ "$LINT_MODE" == "STRICT" ]; then
27 | SWIFTFORMAT_OPTIONS=""
28 | SWIFTLINT_OPTIONS="--strict"
29 | else
30 | SWIFTFORMAT_OPTIONS=""
31 | SWIFTLINT_OPTIONS=""
32 | fi
33 |
34 | pushd $PACKAGE_DIR
35 |
36 | if [ -z "$CI" ]; then
37 | $MINT_RUN swiftformat .
38 | $MINT_RUN swiftlint autocorrect
39 | fi
40 |
41 | $MINT_RUN periphery scan
42 | $MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS .
43 | $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS
44 |
45 | popd
46 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Character.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Character {
4 | internal func asOsmType() -> PodcastLocation.OsmQuery.OsmType? {
5 | .init(rawValue: String(self))
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Collection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Collection {
4 | internal subscript(safe index: Index) -> Element? {
5 | indices.contains(index) ? self[index] : nil
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Author.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// a person, corporation, or similar entity.
4 | public struct Author: Codable, Equatable {
5 | /// Conveys a human-readable name for the person.
6 | public let name: String
7 |
8 | /// Contains an email address for the person.
9 | public let email: String?
10 |
11 | /// Contains a home page for the person.
12 | public let uri: URL?
13 |
14 | public init(name: String) {
15 | self.name = name
16 | email = nil
17 | uri = nil
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/EntryCategory.swift:
--------------------------------------------------------------------------------
1 | /// Abstract category type.
2 | public protocol EntryCategory {
3 | /// Term used for the category
4 | var term: String { get }
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Entryable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Basic Feed type with abstract properties.
4 | public protocol Entryable {
5 | /// Unique Identifier of the Item.
6 | var id: EntryID { get }
7 | /// The URL of the item.
8 | var url: URL? { get }
9 | /// The title of the item.
10 | var title: String { get }
11 | /// HTML content of the item.
12 | var contentHtml: String? { get }
13 | /// The item synopsis.
14 | var summary: String? { get }
15 | /// Indicates when the item was published.
16 | var published: Date? { get }
17 | /// The author of the item.
18 | var authors: [Author] { get }
19 | /// Includes the item in one or more categories.
20 | var categories: [EntryCategory] { get }
21 | /// Creator of the item.
22 | var creators: [String] { get }
23 | /// Abstraction of Podcast episode or Youtube video info.
24 | var media: MediaContent? { get }
25 | /// Image URL of the Item.
26 | var imageURL: URL? { get }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Feedable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Basic abstract Feed
4 | /// ## Topics
5 | ///
6 | /// ### Basic Properties
7 | ///
8 | /// - ``title``
9 | /// - ``siteURL``
10 | /// - ``summary``
11 | /// - ``updated``
12 | /// - ``authors``
13 | /// - ``copyright``
14 | /// - ``image``
15 | /// - ``children``
16 | ///
17 | /// ### Special Properties
18 | ///
19 | /// - ``youtubeChannelID``
20 | /// - ``syndication``
21 | public protocol Feedable {
22 | /// The name of the channel.
23 | var title: String { get }
24 | /// The URL to the website corresponding to the channel.
25 | var siteURL: URL? { get }
26 | /// Phrase or sentence describing the channel.
27 | var summary: String? { get }
28 |
29 | /// The last time the content of the channel changed.
30 | var updated: Date? { get }
31 |
32 | /// The author of the channel.
33 | var authors: [Author] { get }
34 |
35 | /// Copyright notice for content in the channel.
36 | var copyright: String? { get }
37 |
38 | /// Specifies a GIF, JPEG or PNG image that can be displayed with the channel.
39 | var image: URL? { get }
40 |
41 | /// Items or stories attached to the feed.
42 | var children: [Entryable] { get }
43 |
44 | /// For YouTube channels, this will be the youtube channel ID.
45 | var youtubeChannelID: String? { get }
46 |
47 | /// Provides syndication hints to aggregators and others
48 | var syndication: SyndicationUpdate? { get }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Link.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | public struct Link: Codable {
3 | public let href: URL
4 | public let rel: String?
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Primitives/CData.swift:
--------------------------------------------------------------------------------
1 | /// #CDATA XML element.
2 | public struct CData: Codable, ExpressibleByStringLiteral, Equatable {
3 | public enum CodingKeys: String, CodingKey {
4 | case value = "#CDATA"
5 | }
6 |
7 | public var description: String {
8 | value
9 | }
10 |
11 | /// String value of the #CDATA element.
12 | public let value: String
13 |
14 | public init(stringLiteral value: String) {
15 | self.value = value
16 | }
17 |
18 | public init(from decoder: Decoder) throws {
19 | let value: String
20 | do {
21 | let container = try decoder.container(keyedBy: CodingKeys.self)
22 | value = try container.decode(String.self, forKey: .value)
23 | } catch {
24 | let container = try decoder.singleValueContainer()
25 | value = try container.decode(String.self)
26 | }
27 | self.value = value
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Primitives/ListString.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct ListString<
4 | Value: LosslessStringConvertible & Equatable
5 | >: Codable, Equatable {
6 | public let values: [Value]
7 |
8 | internal init(values: [Value]) {
9 | self.values = values
10 | }
11 |
12 | public init(from decoder: Decoder) throws {
13 | let container = try decoder.singleValueContainer()
14 | let listString = try container.decode(String.self)
15 | let strings = listString.components(separatedBy: ",")
16 | let values = try strings
17 | .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
18 | .filter { !$0.isEmpty }
19 | .map(Self.createValueFrom)
20 | self.init(values: values)
21 | }
22 |
23 | private static func createValueFrom(_ string: String) throws -> Value {
24 | guard let value: Value = .init(string) else {
25 | throw DecodingError.typeMismatch(
26 | Value.self,
27 | .init(codingPath: [], debugDescription: "Invalid value: \(string)")
28 | )
29 | }
30 | return value
31 | }
32 |
33 | public func encode(to encoder: Encoder) throws {
34 | var container = encoder.singleValueContainer()
35 | let strings = values.map(String.init)
36 | let listString = strings.joined(separator: ",")
37 | try container.encode(listString)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Primitives/UTF8EncodedURL.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal struct UTF8EncodedURL: Codable {
4 | internal let value: URL
5 | internal let string: String?
6 |
7 | internal init(from decoder: Decoder) throws {
8 | let container = try decoder.singleValueContainer()
9 | do {
10 | value = try container.decode(URL.self)
11 | string = nil
12 | } catch let error as DecodingError {
13 | let string = try container.decode(String.self)
14 |
15 | let encodedURLString = string.addingPercentEncoding(
16 | withAllowedCharacters: .urlQueryAllowed
17 | )
18 | let encodedURL = encodedURLString.flatMap(URL.init(string:))
19 | guard let encodedURL = encodedURL else {
20 | throw error
21 | }
22 | value = encodedURL
23 | self.string = string
24 | }
25 | }
26 |
27 | internal func encode(to encoder: Encoder) throws {
28 | var container = encoder.singleValueContainer()
29 | if let string = string {
30 | try container.encode(string)
31 | } else {
32 | try container.encode(value)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Common/Primitives/XMLStringInt.swift:
--------------------------------------------------------------------------------
1 | /// XML Element which contains a ``String`` parsable into a ``Integer``.
2 | public struct XMLStringInt: Codable, ExpressibleByIntegerLiteral {
3 | public typealias IntegerLiteralType = Int
4 |
5 | /// The underlying ``Int`` value.
6 | public let value: Int
7 |
8 | public init(integerLiteral value: Int) {
9 | self.value = value
10 | }
11 |
12 | public init(from decoder: Decoder) throws {
13 | let container = try decoder.singleValueContainer()
14 | let stringValue = try container.decode(String.self)
15 | .trimmingCharacters(in: .whitespacesAndNewlines)
16 | guard let value = Int(stringValue) else {
17 | let context = DecodingError.Context(
18 | codingPath: decoder.codingPath,
19 | debugDescription: "Not Able to Parse String Into Integer",
20 | underlyingError: nil
21 | )
22 | throw DecodingError.typeMismatch(Int.self, context)
23 | }
24 | self.value = value
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/AnyDecoding.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal protocol AnyDecoding {
4 | static var label: String { get }
5 | func decodeFeed(data: Data) throws -> Feedable
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/CustomDecoderSetup.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal protocol CustomDecoderSetup {
4 | func setup(decoder: TypeDecoder)
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/DateFormatterDecoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal struct DateFormatterDecoder {
4 | internal enum RSS {
5 | private static let dateFormatStrings = [
6 | "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX",
7 | "yyyy-MM-dd'T'HH:mm:ssXXXXX",
8 | "E, d MMM yyyy HH:mm:ss zzz",
9 | "yyyy-MM-dd HH:mm:ss"
10 | ]
11 |
12 | internal static let decoder = DateFormatterDecoder(
13 | basedOnFormats: Self.dateFormatStrings
14 | )
15 | }
16 |
17 | private let formatters: [DateFormatter]
18 |
19 | internal init(basedOnFormats formats: [String]) {
20 | formatters = formats.map(Self.isoPOSIX(withFormat:))
21 | }
22 |
23 | private static func isoPOSIX(withFormat dateFormat: String) -> DateFormatter {
24 | let formatter = DateFormatter()
25 | formatter.calendar = Calendar(identifier: .iso8601)
26 | formatter.locale = Locale(identifier: "en_US_POSIX")
27 | formatter.timeZone = TimeZone(secondsFromGMT: 0)
28 | formatter.dateFormat = dateFormat
29 | return formatter
30 | }
31 |
32 | internal func decodeString(_ dateStr: String) -> Date? {
33 | for formatter in formatters {
34 | if let date = formatter.date(from: dateStr) {
35 | return date
36 | }
37 | }
38 | return nil
39 | }
40 |
41 | internal func decode(from decoder: Decoder) throws -> Date {
42 | let container = try decoder.singleValueContainer()
43 | let dateStr = try container.decode(String.self)
44 |
45 | if let date = decodeString(dateStr) {
46 | return date
47 | }
48 |
49 | let context = DecodingError.Context(
50 | codingPath: decoder.codingPath,
51 | debugDescription: "Invalid Date from '\(dateStr)'"
52 | )
53 | throw DecodingError.dataCorrupted(context)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/DecodableFeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal protocol DecodableFeed: Decodable, Feedable {
4 | static var source: DecoderSetup { get }
5 | static var label: String { get }
6 | }
7 |
8 | extension DecodableFeed {
9 | internal static func decoding(using decoder: TypeDecoder) -> Decoding {
10 | Decoding(for: Self.self, using: decoder)
11 | }
12 |
13 | internal static func anyDecoding(using decoder: TypeDecoder) -> AnyDecoding {
14 | Self.decoding(using: decoder)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/DecoderSetup.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal protocol DecoderSetup {
4 | var source: DecoderSource { get }
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/DecoderSource.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal enum DecoderSource: UInt8, DecoderSetup {
4 | case json = 0x007B
5 | case xml = 0x003C
6 |
7 | internal var source: DecoderSource {
8 | self
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/Decoding.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal struct Decoding: AnyDecoding {
4 | internal static var label: String {
5 | DecodingType.label
6 | }
7 |
8 | internal let decoder: TypeDecoder
9 |
10 | internal init(for _: DecodingType.Type, using decoder: TypeDecoder) {
11 | self.decoder = decoder
12 | }
13 |
14 | internal func decodeFeed(data: Data) throws -> Feedable {
15 | try decode(data: data)
16 | }
17 |
18 | internal func decode(data: Data) throws -> DecodingType {
19 | try decoder.decode(DecodingType.self, from: data)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/DecodingError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension DecodingError {
4 | internal struct Dictionary: Error {
5 | internal init?(errors: [String: DecodingError]) {
6 | guard errors.count > 1 else {
7 | return nil
8 | }
9 | self.errors = errors
10 | }
11 |
12 | internal let errors: [String: DecodingError]
13 | }
14 |
15 | internal static func failedAttempts(_ errors: [String: DecodingError]) -> Self {
16 | let context = DecodingError.Context(
17 | codingPath: [],
18 | debugDescription: "Failed to decode data with several decoders.",
19 | underlyingError: Dictionary(errors: errors) ?? errors.first?.value
20 | )
21 | return DecodingError.dataCorrupted(context)
22 | }
23 |
24 | internal static func dataCorrupted(
25 | codingKey: CodingKey,
26 | debugDescription: String
27 | ) -> Self {
28 | DecodingError.dataCorrupted(
29 | .init(
30 | codingPath: [codingKey],
31 | debugDescription: debugDescription
32 | )
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/SynDecoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XMLCoder
3 |
4 | /// An object that decodes instances of Feedable from JSON or XML objects.
5 | /// ## Topics
6 | ///
7 | /// ### Creating a Decoder
8 | ///
9 | /// - ``init()``
10 | ///
11 | /// ### Decoding
12 | ///
13 | /// - ``decode(_:)``
14 | public class SynDecoder {
15 | private static let defaultTypes: [DecodableFeed.Type] = [
16 | RSSFeed.self,
17 | AtomFeed.self,
18 | JSONFeed.self
19 | ]
20 |
21 | private let defaultJSONDecoderSetup: (JSONDecoder) -> Void
22 | private let defaultXMLDecoderSetup: (XMLDecoder) -> Void
23 | private let types: [DecodableFeed.Type]
24 |
25 | private lazy var defaultXMLDecoder: XMLDecoder = {
26 | let decoder = XMLDecoder()
27 | self.defaultXMLDecoderSetup(decoder)
28 | return decoder
29 | }()
30 |
31 | private lazy var defaultJSONDecoder: JSONDecoder = {
32 | let decoder = JSONDecoder()
33 | self.defaultJSONDecoderSetup(decoder)
34 | return decoder
35 | }()
36 |
37 | // swiftlint:disable:next closure_body_length
38 | private lazy var decodings: [DecoderSource: [String: AnyDecoding]] = {
39 | let decodings = types.map { type -> (DecoderSource, AnyDecoding) in
40 | let source = type.source
41 | let setup = type.source as? CustomDecoderSetup
42 | let decoder: TypeDecoder
43 |
44 | switch (source.source, setup?.setup(decoder:)) {
45 | case let (.xml, .some(setup)):
46 | decoder = XMLDecoder()
47 | setup(decoder)
48 |
49 | case let (.json, .some(setup)):
50 | decoder = JSONDecoder()
51 | setup(decoder)
52 |
53 | case (.xml, .none):
54 | decoder = self.defaultXMLDecoder
55 |
56 | case (.json, .none):
57 | decoder = self.defaultJSONDecoder
58 | }
59 |
60 | return (source.source, type.anyDecoding(using: decoder))
61 | }
62 | return Dictionary(grouping: decodings) { $0.0 }
63 | .mapValues { $0
64 | .map { $0.1 }
65 | .map { (type(of: $0).label, $0) }
66 | }
67 | .mapValues(Dictionary.init(uniqueKeysWithValues:))
68 | }()
69 |
70 | internal init(
71 | types: [DecodableFeed.Type]? = nil,
72 | defaultJSONDecoderSetup: ((JSONDecoder) -> Void)? = nil,
73 | defaultXMLDecoderSetup: ((XMLDecoder) -> Void)? = nil
74 | ) {
75 | self.types = types ?? Self.defaultTypes
76 | self.defaultJSONDecoderSetup = defaultJSONDecoderSetup ?? Self.setupJSONDecoder(_:)
77 | self.defaultXMLDecoderSetup = defaultXMLDecoderSetup ?? Self.setupXMLDecoder(_:)
78 | }
79 |
80 | /// Creates an instance of ``RSSDecoder``
81 | public convenience init() {
82 | self.init(types: nil, defaultJSONDecoderSetup: nil, defaultXMLDecoderSetup: nil)
83 | }
84 |
85 | internal static func setupJSONDecoder(_ decoder: JSONDecoder) {
86 | decoder.keyDecodingStrategy = .convertFromSnakeCase
87 | decoder.dateDecodingStrategy = .custom(DateFormatterDecoder.RSS.decoder.decode(from:))
88 | }
89 |
90 | internal static func setupXMLDecoder(_ decoder: XMLDecoder) {
91 | decoder.keyDecodingStrategy = .convertFromSnakeCase
92 | decoder.dateDecodingStrategy = .custom(DateFormatterDecoder.RSS.decoder.decode(from:))
93 | decoder.trimValueWhitespaces = false
94 | }
95 |
96 | /// Returns a ``Feedable`` object of the type you specify, decoded from a JSON object.
97 | /// - Parameter data: The JSON or XML object to decode.
98 | /// - Returns: A ``Feedable`` object
99 | ///
100 | /// If the data is not valid RSS, this method throws the
101 | /// `DecodingError.dataCorrupted(_:)` error.
102 | /// If a value within the RSS fails to decode,
103 | /// this method throws the corresponding error.
104 | ///
105 | /// ```swift
106 | /// let data = Data(contentsOf: "empowerapps-show.xml")!
107 | /// let decoder = SynDecoder()
108 | /// let feed = try decoder.decode(data)
109 | ///
110 | /// print(feed.title) // Prints "Empower Apps"
111 | /// ```
112 | public func decode(_ data: Data) throws -> Feedable {
113 | var errors = [String: DecodingError]()
114 |
115 | guard let firstByte = data.first else {
116 | throw DecodingError.dataCorrupted(
117 | .init(codingPath: [], debugDescription: "Empty Data.")
118 | )
119 | }
120 | guard let source = DecoderSource(rawValue: firstByte) else {
121 | throw DecodingError.dataCorrupted(
122 | .init(codingPath: [], debugDescription: "Unmatched First Byte: \(firstByte)")
123 | )
124 | }
125 | guard let decodings = decodings[source] else {
126 | throw DecodingError.failedAttempts(errors)
127 | }
128 | for (label, decoding) in decodings {
129 | do {
130 | return try decoding.decodeFeed(data: data)
131 | } catch let decodingError as DecodingError {
132 | errors[label] = decodingError
133 | }
134 | }
135 |
136 | throw DecodingError.failedAttempts(errors)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Decoding/TypeDecoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XMLCoder
3 |
4 | internal protocol TypeDecoder {
5 | func decode(_ type: T.Type, from data: Data) throws -> T where T: DecodableFeed
6 | }
7 |
8 | extension JSONDecoder: TypeDecoder {}
9 |
10 | extension XMLDecoder: TypeDecoder {}
11 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Dictionary.swift:
--------------------------------------------------------------------------------
1 | extension Dictionary {
2 | internal mutating func formUnion(
3 | _ collection: SequenceType,
4 | key: Key
5 | ) where Value == Set, SequenceType.Element == ElementType {
6 | if let set = self[key] {
7 | self[key] = set.union(collection)
8 | } else {
9 | self[key] = Set(collection)
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/CategoryDescriptor.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A descriptor for a category.
3 | ///
4 | /// - Note: This struct is publicly accessible.
5 | ///
6 | /// - Important: The ``title`` and ``description`` properties are read-only.
7 | ///
8 | /// - SeeAlso: ``Category``
9 | /// - SeeAlso: ``EntryCategory``
10 | public struct CategoryDescriptor {
11 | /// The title of the category.
12 | public let title: String
13 |
14 | /// The description of the category.
15 | public let description: String
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/CategoryLanguage.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing a category in a specific language.
3 | ///
4 | /// - Parameters:
5 | /// - languageCategory: The category in a specific language.
6 | /// - language: The language of the category.
7 | ///
8 | /// - Note: This struct is used internally.
9 | ///
10 | /// - SeeAlso: ``SiteCategoryType``
11 | /// - SeeAlso: ``CategoryDescriptor``
12 | /// - SeeAlso: ``SiteLanguageType``
13 | /// - SeeAlso: ``EntryCategory``
14 | public struct CategoryLanguage {
15 | /// The type of the category.
16 | public let type: SiteCategoryType
17 |
18 | /// The descriptor of the category.
19 | public let descriptor: CategoryDescriptor
20 |
21 | /// The language of the category.
22 | public let language: SiteLanguageType
23 |
24 | /// A struct representing an Atom category.
25 | /// Initializes a ``CategoryLanguage`` instance.
26 | ///
27 | /// - Parameters:
28 | /// - languageCategory: The category in a specific language.
29 | /// - language: The language of the category.
30 | /// - SeeAlso: ``EntryCategory``
31 | internal init(languageCategory: SiteLanguageCategory, language: SiteLanguageType) {
32 | type = languageCategory.slug
33 | descriptor = CategoryDescriptor(
34 | title: languageCategory.title,
35 | description: languageCategory.description
36 | )
37 | self.language = language
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/Site.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a website.
4 | public struct Site {
5 | /// The title of the website.
6 | public let title: String
7 |
8 | /// The author of the website.
9 | public let author: String
10 |
11 | /// The URL of the website.
12 | public let siteURL: URL
13 |
14 | /// The URL of the website's feed.
15 | public let feedURL: URL
16 |
17 | /// The URL of the website's Twitter page, if available.
18 | public let twitterURL: URL?
19 |
20 | /// The category of the website.
21 | public let category: SiteCategoryType
22 |
23 | /// The language of the website.
24 | public let language: SiteLanguageType
25 |
26 | /// Initializes a new ``Site`` instance.
27 | ///
28 | /// - Parameters:
29 | /// - site: The `SiteLanguageCategory.Site` instance to use as a base.
30 | /// - categoryType: The category type of the website.
31 | /// - languageType: The language type of the website.
32 | internal init(
33 | site: SiteLanguageCategory.Site,
34 | categoryType: SiteCategoryType,
35 | languageType: SiteLanguageType
36 | ) {
37 | title = site.title
38 | author = site.author
39 | siteURL = site.siteURL
40 | feedURL = site.feedURL
41 | twitterURL = site.twitterURL
42 | category = categoryType
43 | language = languageType
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteCategory.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing a site category.
3 | ///
4 | /// - Note: This struct is used to categorize sites based on their type and descriptors.
5 | ///
6 | /// - Parameters:
7 | /// - type: The type of the site category.
8 | /// - descriptors: A dictionary mapping site language types to category descriptors.
9 | ///
10 | /// - Important: This struct should not be used directly.
11 | /// Instead, use the ``SiteCategoryBuilder`` to create instances of ``SiteCategory``.
12 | ///
13 | /// - SeeAlso: ``SiteCategoryType``
14 | /// - SeeAlso: ``CategoryDescriptor``
15 | /// - SeeAlso: ``CategoryLanguage``
16 | /// - SeeAlso: ``SiteCategoryBuilder``
17 | /// - SeeAlso: ``EntryCategory``
18 | public struct SiteCategory {
19 | /// The type of the site category.
20 | public let type: SiteCategoryType
21 |
22 | /// A dictionary mapping site language types to category descriptors.
23 | public let descriptors: [SiteLanguageType: CategoryDescriptor]
24 |
25 | /// A struct representing an Atom category.
26 | /// Initializes a ``SiteCategory`` instance with the given languages.
27 | ///
28 | /// - Parameter languages: An array of ``CategoryLanguage`` instances.
29 | ///
30 | /// - Returns: A new ``SiteCategory`` instance
31 | /// if at least one language is provided, ``nil`` otherwise.
32 | /// - SeeAlso: ``EntryCategory``
33 | internal init?(languages: [CategoryLanguage]) {
34 | guard let type = languages.first?.type else {
35 | return nil
36 | }
37 | self.type = type
38 | descriptors = Dictionary(grouping: languages) { $0.language }
39 | .compactMapValues { $0.first?.descriptor }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteCategoryType.swift:
--------------------------------------------------------------------------------
1 | /// A type alias representing a site category.
2 | public typealias SiteCategoryType = String
3 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteCollection.swift:
--------------------------------------------------------------------------------
1 | /// A collection of site language content.
2 | public typealias SiteCollection = [SiteLanguageContent]
3 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteDirectoryBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A builder for creating a site collection directory.
4 | public struct SiteCollectionDirectoryBuilder: SiteDirectoryBuilder {
5 | /// Initializes a new instance of ``SiteCollectionDirectoryBuilder``.
6 | public init() {}
7 |
8 | /// A struct representing an Atom category.
9 | /// Creates a site collection directory from a site collection.
10 | ///
11 | /// - Parameter blogs: The site collection to build the directory from.
12 | ///
13 | /// - Returns: A new instance of ``SiteCollectionDirectory``.
14 | /// - SeeAlso: ``EntryCategory``
15 | public func directory(fromCollection blogs: SiteCollection) -> SiteCollectionDirectory {
16 | SiteCollectionDirectory(blogs: blogs)
17 | }
18 | }
19 |
20 | /// A protocol for building site directories.
21 | public protocol SiteDirectoryBuilder {
22 | /// The type of site directory to build.
23 | associatedtype SiteDirectoryType: SiteDirectory
24 |
25 | /// A struct representing an Atom category.
26 | /// Creates a site directory from a site collection.
27 | ///
28 | /// - Parameter blogs: The site collection to build the directory from.
29 | ///
30 | /// - Returns: A new instance of ``SiteDirectoryType``.
31 | /// - SeeAlso: ``EntryCategory``
32 | func directory(fromCollection blogs: SiteCollection) -> SiteDirectoryType
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteLanguage.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing a site language.
3 | ///
4 | /// Use this struct to define the type and title of a site language.
5 | ///
6 | /// - Note: This struct is used internally and should not be directly instantiated.
7 | ///
8 | /// - Parameters:
9 | /// - content: The content of the site language.
10 | ///
11 | /// - SeeAlso: ``SiteLanguageType``
12 | ///
13 | /// - Author: Your Name
14 | /// - SeeAlso: ``EntryCategory``
15 | public struct SiteLanguage {
16 | /// The type of the site language.
17 | public let type: SiteLanguageType
18 |
19 | /// The title of the site language.
20 | public let title: String
21 |
22 | /// A struct representing an Atom category.
23 | /// Initializes a new ``SiteLanguage`` instance.
24 | ///
25 | /// - Parameters:
26 | /// - content: The content of the site language.
27 | ///
28 | /// - Returns: A new ``SiteLanguage`` instance.
29 | /// - SeeAlso: ``EntryCategory``
30 | internal init(content: SiteLanguageContent) {
31 | type = content.language
32 | title = content.title
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteLanguageCategory+Site.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // swiftlint:disable nesting
4 | extension SiteLanguageCategory {
5 | /// A ``struct`` representing a site.
6 | public struct Site: Codable {
7 | /// The title of the site.
8 | public let title: String
9 |
10 | /// The author of the site.
11 | public let author: String
12 |
13 | /// The URL of the site.
14 | public let siteURL: URL
15 |
16 | /// The URL of the site's feed.
17 | public let feedURL: URL
18 |
19 | /// The URL of the site's Twitter page.
20 | public let twitterURL: URL?
21 |
22 | /// Coding keys to map properties to JSON keys.
23 | internal enum CodingKeys: String, CodingKey {
24 | case title
25 | case author
26 | case siteURL = "site_url"
27 | case feedURL = "feed_url"
28 | case twitterURL = "twitter_url"
29 | }
30 | }
31 | }
32 |
33 | /// A type alias for `SiteLanguageCategory.Site`.
34 | public typealias SiteStub = SiteLanguageCategory.Site
35 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteLanguageCategory.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing a category of site languages.
3 | ///
4 | /// - Note: This struct conforms to the ``Codable`` protocol.
5 | ///
6 | /// - Important: All properties of this struct are read-only.
7 | ///
8 | /// - SeeAlso: ``Site``
9 | /// - SeeAlso: ``EntryCategory``
10 | public struct SiteLanguageCategory: Codable {
11 | /// The title of the category.
12 | public let title: String
13 |
14 | /// The slug of the category.
15 | public let slug: String
16 |
17 | /// A description of the category.
18 | public let description: String
19 |
20 | /// An array of sites belonging to this category.
21 | public let sites: [Site]
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteLanguageContent.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing the content of a site in a specific language.
3 | ///
4 | /// - Note: This struct conforms to the ``Codable`` protocol.
5 | ///
6 | /// - Important: All properties of this struct are read-only.
7 | ///
8 | /// - SeeAlso: ``SiteLanguageCategory``
9 | ///
10 | /// - Author: Your Name
11 | ///
12 | /// - Version: 1.0
13 | /// - SeeAlso: ``EntryCategory``
14 | public struct SiteLanguageContent: Codable {
15 | /// The language of the site content.
16 | public let language: String
17 |
18 | /// The title of the site.
19 | public let title: String
20 |
21 | /// The categories of the site.
22 | public let categories: [SiteLanguageCategory]
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Blogs/SiteLanguageType.swift:
--------------------------------------------------------------------------------
1 | /// A type representing the language of a website.
2 | public typealias SiteLanguageType = String
3 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/Atom/AtomCategory.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing an Atom category.
3 | ///
4 | /// - Note: This struct conforms to the ``Codable`` and ``EntryCategory`` protocols.
5 | ///
6 | /// - SeeAlso: ``EntryCategory``
7 | /// - SeeAlso: ``EntryCategory``
8 | public struct AtomCategory: Codable, EntryCategory {
9 | /// The term of the category.
10 | public let term: String
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/Atom/AtomEntry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an entry in an Atom feed.
4 | public struct AtomEntry: Codable {
5 | /// The coding keys used for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case id
8 | case title
9 | case published
10 | case content
11 | case updated
12 | case links = "link"
13 | case authors = "author"
14 | case atomCategories = "category"
15 | case youtubeVideoID = "yt:videoId"
16 | case youtubeChannelID = "yt:channelId"
17 | case creators = "dc:creator"
18 | case mediaGroup = "media:group"
19 | }
20 |
21 | /// A permanent, universally unique identifier for an entry.
22 | public let id: EntryID
23 |
24 | /// A human-readable title for the entry.
25 | public let title: String
26 |
27 | /// The most recent instant in time when the entry was published.
28 | public let published: Date?
29 |
30 | /// The content of the entry.
31 | public let content: String?
32 |
33 | /// The most recent instant in time when the entry was modified.
34 | public let updated: Date
35 |
36 | /// The categories associated with the entry.
37 | public let atomCategories: [AtomCategory]
38 |
39 | /// The links associated with the entry.
40 | public let links: [Link]
41 |
42 | /// The authors of the entry.
43 | public let authors: [Author]
44 |
45 | /// The YouTube video ID, if the entry is from a YouTube channel.
46 | public let youtubeVideoID: String?
47 |
48 | /// The YouTube channel ID, if the entry is from a YouTube channel.
49 | public let youtubeChannelID: String?
50 |
51 | /// The creators of the entry.
52 | public let creators: [String]
53 |
54 | /// The media group associated with the entry.
55 | public let mediaGroup: AtomMediaGroup?
56 | }
57 |
58 | extension AtomEntry: Entryable {
59 | /// The categories associated with the entry.
60 | public var categories: [EntryCategory] {
61 | atomCategories
62 | }
63 |
64 | /// The URL of the entry.
65 | public var url: URL? {
66 | links.first?.href
67 | }
68 |
69 | /// The HTML content of the entry.
70 | public var contentHtml: String? {
71 | content?.trimmingCharacters(in: .whitespacesAndNewlines)
72 | }
73 |
74 | /// The summary of the entry.
75 | public var summary: String? {
76 | nil
77 | }
78 |
79 | /// The media content of the entry.
80 | public var media: MediaContent? {
81 | YouTubeIDProperties(entry: self).map(Video.youtube).map(MediaContent.video)
82 | }
83 |
84 | /// The URL of the entry's image.
85 | public var imageURL: URL? {
86 | mediaGroup?.thumbnails.first?.url
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/Atom/AtomFeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// An XML-based Web content and metadata syndication format.
5 | ///
6 | /// Based on the
7 | /// [specifications here](https://datatracker.ietf.org/doc/html/rfc4287#section-4.1.2).
8 | /// - SeeAlso: ``EntryCategory``
9 | public struct AtomFeed {
10 | public enum CodingKeys: String, CodingKey {
11 | case id
12 | case title
13 | case description
14 | case subtitle
15 | case published
16 | case pubDate
17 | case links = "link"
18 | case entries = "entry"
19 | case authors = "author"
20 | case youtubeChannelID = "yt:channelId"
21 | }
22 |
23 | /// Identifies the feed using a universally unique and permanent URI.
24 | /// If you have a long-term, renewable lease on your Internet domain name,
25 | /// then you can feel free to use your website's address.
26 | public let id: String
27 |
28 | /// Contains a human readable title for the feed.
29 | /// Often the same as the title of the associated website.
30 | public let title: String
31 |
32 | /// Contains a human-readable description or subtitle for the feed.
33 | public let description: String?
34 |
35 | /// Contains a human-readable description or subtitle for the feed.
36 | public let subtitle: String?
37 |
38 | /// The publication date for the content in the channel.
39 | public let published: Date?
40 |
41 | /// The publication date for the content in the channel.
42 | public let pubDate: Date?
43 |
44 | /// A reference from an entry or feed to a Web resource.
45 | public let links: [Link]
46 |
47 | /// Individual entries of the feed.
48 | public let entries: [AtomEntry]
49 |
50 | /// The author of the feed.
51 | public let authors: [Author]
52 |
53 | /// YouTube channel ID, if from a YouTube channel.
54 | public let youtubeChannelID: String?
55 | }
56 |
57 | extension AtomFeed: DecodableFeed {
58 | /// The source of the decoder for AtomFeed.
59 | internal static let source: DecoderSetup = DecoderSource.xml
60 |
61 | /// The label for AtomFeed.
62 | internal static let label: String = "Atom"
63 |
64 | /// The summary of the AtomFeed.
65 | public var summary: String? {
66 | description ?? subtitle
67 | }
68 |
69 | /// The children of the AtomFeed.
70 | public var children: [Entryable] {
71 | entries
72 | }
73 |
74 | /// The site URL of the AtomFeed.
75 | public var siteURL: URL? {
76 | links.first { $0.rel != "self" }?.href
77 | }
78 |
79 | /// The updated date of the AtomFeed.
80 | public var updated: Date? {
81 | pubDate ?? published
82 | }
83 |
84 | /// The copyright of the AtomFeed.
85 | public var copyright: String? {
86 | nil
87 | }
88 |
89 | /// The image URL of the AtomFeed.
90 | public var image: URL? {
91 | nil
92 | }
93 |
94 | /// The syndication update of the AtomFeed.
95 | public var syndication: SyndicationUpdate? {
96 | nil
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/Atom/AtomMedia.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// Media structure which enables content publishers and bloggers
5 | /// to syndicate multimedia content such as TV and video clips, movies, images and audio.
6 | ///
7 | /// For more details, check out
8 | /// [the Media RSS Specification](https://www.rssboard.org/media-rss).
9 | /// - SeeAlso: ``EntryCategory``
10 | public struct AtomMedia: Codable {
11 | /// A struct representing an Atom category.
12 | /// The type of object.
13 | ///
14 | /// While this attribute can at times seem redundant if type is supplied,
15 | /// it is included because it simplifies decision making on the reader side,
16 | /// as well as flushes out any ambiguities between MIME type and object type.
17 | /// It is an optional attribute.
18 | /// - SeeAlso: ``EntryCategory``
19 | public let url: URL
20 |
21 | /// The direct URL to the media object.
22 | public let medium: String?
23 |
24 | public init(from decoder: Decoder) throws {
25 | let container = try decoder.container(keyedBy: CodingKeys.self)
26 | url = try container.decode(UTF8EncodedURL.self, forKey: .url).value
27 | medium = try container.decodeIfPresent(String.self, forKey: .medium)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/Atom/AtomMediaGroup.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A group of media elements in an Atom feed.
4 | public struct AtomMediaGroup: Codable {
5 | /// Coding keys for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case title = "media:title"
8 | case descriptions = "media:description"
9 | case contents = "media:content"
10 | case thumbnails = "media:thumbnail"
11 | }
12 |
13 | /// The title of the media group.
14 | public let title: String?
15 |
16 | /// The media elements within the group.
17 | public let contents: [AtomMedia]
18 |
19 | /// The thumbnail images associated with the media group.
20 | public let thumbnails: [AtomMedia]
21 |
22 | /// The descriptions of the media group.
23 | public let descriptions: [String]
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/JSONFeed/JSONFeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// A struct representing a JSON feed.
5 | ///
6 | /// - Note: This struct conforms to the ``DecodableFeed`` protocol.
7 | ///
8 | /// - SeeAlso: ``DecodableFeed``
9 | /// - SeeAlso: ``EntryCategory``
10 | public struct JSONFeed {
11 | /// The version of the JSON feed.
12 | public let version: URL
13 |
14 | /// The title of the JSON feed.
15 | public let title: String
16 |
17 | /// The URL of the home page associated with the feed.
18 | public let homePageUrl: URL
19 |
20 | /// A description of the JSON feed.
21 | public let description: String?
22 |
23 | /// The author of the feed.
24 | public let author: Author?
25 |
26 | /// The items in the JSON feed.
27 | public let items: [JSONItem]
28 | }
29 |
30 | extension JSONFeed: DecodableFeed {
31 | /// The source of the decoder for JSON feed.
32 | internal static let source: DecoderSetup = DecoderSource.json
33 |
34 | /// The label for the JSON feed.
35 | internal static let label: String = "JSON"
36 |
37 | /// The YouTube channel ID associated with the feed.
38 | public var youtubeChannelID: String? {
39 | nil
40 | }
41 |
42 | /// The children of the JSON feed.
43 | public var children: [Entryable] {
44 | items
45 | }
46 |
47 | /// The summary of the JSON feed.
48 | public var summary: String? {
49 | description
50 | }
51 |
52 | /// The site URL associated with the feed.
53 | public var siteURL: URL? {
54 | homePageUrl
55 | }
56 |
57 | /// The last updated date of the JSON feed.
58 | public var updated: Date? {
59 | nil
60 | }
61 |
62 | /// The copyright information of the JSON feed.
63 | public var copyright: String? {
64 | nil
65 | }
66 |
67 | /// The image URL associated with the feed.
68 | public var image: URL? {
69 | nil
70 | }
71 |
72 | /// The syndication update information of the JSON feed.
73 | public var syndication: SyndicationUpdate? {
74 | nil
75 | }
76 |
77 | /// The authors of the JSON feed.
78 | public var authors: [Author] {
79 | guard let author = author else {
80 | return []
81 | }
82 | return [author]
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/JSONFeed/JSONItem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// A struct representing an item in JSON format.
5 | /// - SeeAlso: ``EntryCategory``
6 | public struct JSONItem: Codable {
7 | /// The unique identifier of the item.
8 | public let guid: EntryID
9 |
10 | /// The URL associated with the item.
11 | public let url: URL?
12 |
13 | /// The title of the item.
14 | public let title: String
15 |
16 | /// The HTML content of the item.
17 | public let contentHtml: String?
18 |
19 | /// A summary of the item.
20 | public let summary: String?
21 |
22 | /// The date the item was published.
23 | public let datePublished: Date?
24 |
25 | /// The author of the item.
26 | public let author: Author?
27 | }
28 |
29 | extension JSONItem: Entryable {
30 | /// A struct representing an Atom category.
31 | /// Returns an array of authors for the item.
32 | /// - SeeAlso: ``EntryCategory``
33 | public var authors: [Author] {
34 | guard let author = author else {
35 | return []
36 | }
37 | return [author]
38 | }
39 |
40 | /// The URL of the item's image.
41 | public var imageURL: URL? {
42 | nil
43 | }
44 |
45 | /// A struct representing an Atom category.
46 | /// An array of creators associated with the item.
47 | /// - SeeAlso: ``EntryCategory``
48 | public var creators: [String] {
49 | []
50 | }
51 |
52 | /// The date the item was published.
53 | public var published: Date? {
54 | datePublished
55 | }
56 |
57 | /// The unique identifier of the item.
58 | public var id: EntryID {
59 | guid
60 | }
61 |
62 | /// A struct representing an Atom category.
63 | /// An array of categories associated with the item.
64 | /// - SeeAlso: ``EntryCategory``
65 | public var categories: [EntryCategory] {
66 | []
67 | }
68 |
69 | /// The media content associated with the item.
70 | public var media: MediaContent? {
71 | nil
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/Enclosure.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an enclosure for a resource.
4 | public struct Enclosure: Codable {
5 | internal enum CodingKeys: String, CodingKey {
6 | case url
7 | case type
8 | case length
9 | }
10 |
11 | /// The URL of the enclosure.
12 | public let url: URL
13 |
14 | /// The type of the enclosure.
15 | public let type: String
16 |
17 | /// The length of the enclosure, if available.
18 | public let length: Int?
19 |
20 | /// Initializes a new ``Enclosure`` instance from a decoder.
21 | ///
22 | /// - Parameter decoder: The decoder to read data from.
23 | /// - Throws: An error if the decoding process fails.
24 | public init(from decoder: Decoder) throws {
25 | let container = try decoder.container(keyedBy: CodingKeys.self)
26 | url = try container.decode(UTF8EncodedURL.self, forKey: .url).value
27 | type = try container.decode(String.self, forKey: .type)
28 | length = try Self.decodeLength(from: container)
29 | }
30 |
31 | /// Decodes the length of the enclosure from the given container.
32 | ///
33 | /// - Parameter container: The container to decode from.
34 | /// - Returns: The length of the enclosure, or ``nil`` if not available.
35 | /// - Throws: An error if the decoding process fails.
36 | private static func decodeLength(
37 | from container: KeyedDecodingContainer
38 | ) throws -> Int? {
39 | if container.contains(.length) {
40 | do {
41 | return try container.decode(Int.self, forKey: .length)
42 | } catch {
43 | let lengthString = try container.decode(String.self, forKey: .length)
44 | if lengthString.isEmpty {
45 | return nil
46 | } else if let length = Int(lengthString) {
47 | return length
48 | } else {
49 | throw error
50 | }
51 | }
52 | } else {
53 | return nil
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/EntryID.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// An identifier for an entry based on the RSS guid.
4 | ///
5 | /// - Note: This enum conforms to
6 | /// ``Codable``, ``Equatable``, and ``LosslessStringConvertible``.
7 | public enum EntryID: Codable, Equatable, LosslessStringConvertible {
8 | /// An identifier in URL format.
9 | case url(URL)
10 |
11 | /// An identifier in UUID format.
12 | case uuid(UUID)
13 |
14 | /// An identifier in string path format.
15 | ///
16 | /// This format is commonly used by YouTube's RSS feed, in the format of:
17 | /// ```
18 | /// yt:video:(YouTube Video ID)
19 | /// ```
20 | case path([String], separatedBy: String)
21 |
22 | /// An identifier in plain un-parsable string format.
23 | case string(String)
24 |
25 | /// A string representation of the entry identifier.
26 | public var description: String {
27 | let string: String
28 | switch self {
29 | case let .url(url):
30 | string = url.absoluteString
31 |
32 | case let .uuid(uuid):
33 | string = uuid.uuidString.lowercased()
34 |
35 | case let .path(components, separatedBy: separator):
36 | string = components.joined(separator: separator)
37 |
38 | case let .string(value):
39 | string = value
40 | }
41 | return string
42 | }
43 |
44 | /// Initializes an ``EntryID`` from a string.
45 | ///
46 | /// - Parameter description: The string representation of the entry identifier.
47 | /// - Note: This initializer will never return a ``nil`` instance.
48 | /// To avoid the ``Optional`` result, use `init(string:)` instead.
49 | public init?(_ description: String) {
50 | self.init(string: description)
51 | }
52 |
53 | /// Initializes an ``EntryID`` from a string.
54 | ///
55 | /// - Parameter string: The string representation of the entry identifier.
56 | /// - Note: Use this initializer instead of `init(_:)` to avoid the ``Optional`` result.
57 | public init(string: String) {
58 | if let url = URL(strict: string) {
59 | self = .url(url)
60 | } else if let uuid = UUID(uuidString: string) {
61 | self = .uuid(uuid)
62 | } else {
63 | let components = string.components(separatedBy: ":")
64 | if components.count > 1 {
65 | self = .path(components, separatedBy: ":")
66 | } else {
67 | let components = string.components(separatedBy: "/")
68 | if components.count > 1 {
69 | self = .path(components, separatedBy: "/")
70 | } else {
71 | self = .string(string)
72 | }
73 | }
74 | }
75 | }
76 |
77 | /// Initializes an ``EntryID`` from a decoder.
78 | ///
79 | /// - Parameter decoder: The decoder to read data from.
80 | /// - Throws: An error if the decoding process fails.
81 | public init(from decoder: Decoder) throws {
82 | let container = try decoder.singleValueContainer()
83 | let string = try container.decode(String.self)
84 | self.init(string: string)
85 | }
86 |
87 | /// Encodes the ``EntryID`` into the given encoder.
88 | ///
89 | /// - Parameter encoder: The encoder to write data to.
90 | /// - Throws: An error if the encoding process fails.
91 | public func encode(to encoder: Encoder) throws {
92 | var container = encoder.singleValueContainer()
93 | try container.encode(description)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSChannel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// A struct representing information about the channel (metadata) and its contents.
5 | ///
6 | /// - Note: This struct conforms to the ``Codable`` protocol.
7 | ///
8 | /// - Remark: The ``CodingKeys`` enum is used to specify the coding keys for the struct.
9 | ///
10 | /// - SeeAlso: ``RSSItem``
11 | /// - SeeAlso: ``RSSImage``
12 | /// - SeeAlso: ``Author``
13 | /// - SeeAlso: ``WordPressElements.Category``
14 | /// - SeeAlso: ``WordPressElements.Tag``
15 | /// - SeeAlso: ``iTunesOwner``
16 | /// - SeeAlso: ``PodcastLocked``
17 | /// - SeeAlso: ``PodcastFunding``
18 | /// - SeeAlso: ``PodcastPerson``
19 | /// - SeeAlso: ``EntryCategory``
20 | public struct RSSChannel: Codable {
21 | internal enum CodingKeys: String, CodingKey {
22 | case title
23 | case link
24 | case description
25 | case lastBuildDate
26 | case pubDate
27 | case ttl
28 | case syUpdatePeriod = "sy:updatePeriod"
29 | case syUpdateFrequency = "sy:updateFrequency"
30 | case items = "item"
31 | case itunesAuthor = "itunes:author"
32 | case itunesImage = "itunes:image"
33 | case itunesOwner = "itunes:owner"
34 | case copyright
35 | case image
36 | case author
37 | case wpCategories = "wp:category"
38 | case wpTags = "wp:tag"
39 | case wpBaseSiteURL = "wp:baseSiteUrl"
40 | case wpBaseBlogURL = "wp:baseBlogUrl"
41 | case podcastLocked = "podcast:locked"
42 | case podcastFundings = "podcast:funding"
43 | case podcastPeople = "podcast:person"
44 | }
45 |
46 | /// The name of the channel.
47 | public let title: String
48 |
49 | /// The URL to the HTML website corresponding to the channel.
50 | public let link: URL
51 |
52 | /// Phrase or sentence describing the channel.
53 | public let description: String?
54 |
55 | /// The last time the content of the channel changed.
56 | public let lastBuildDate: Date?
57 |
58 | /// Indicates the publication date and time of the feed's content.
59 | public let pubDate: Date?
60 |
61 | /// TTL stands for time to live.
62 | /// It's a number of minutes that indicates
63 | /// how long a channel can be cached before refreshing from the source.
64 | public let ttl: Int?
65 |
66 | /// Describes the period over which the channel format is updated.
67 | public let syUpdatePeriod: SyndicationUpdatePeriod?
68 |
69 | /// Used to describe the frequency of updates in relation to the update period.
70 | /// A positive integer indicates how many times in that period the channel is updated.
71 | public let syUpdateFrequency: SyndicationUpdateFrequency?
72 |
73 | /// The items contained in the channel.
74 | public let items: [RSSItem]
75 |
76 | /// The author of the channel.
77 | public let itunesAuthor: String?
78 |
79 | /// The image associated with the channel.
80 | public let itunesImage: String?
81 |
82 | /// The owner of the channel.
83 | public let itunesOwner: iTunesOwner?
84 |
85 | /// Copyright notice for content in the channel.
86 | public let copyright: String?
87 |
88 | /// Specifies a GIF, JPEG or PNG image that can be displayed with the channel.
89 | public let image: RSSImage?
90 |
91 | /// The author of the channel.
92 | public let author: Author?
93 |
94 | /// The categories associated with the channel.
95 | public let wpCategories: [WordPressElements.Category]
96 |
97 | /// The tags associated with the channel.
98 | public let wpTags: [WordPressElements.Tag]
99 |
100 | /// The base site URL of the channel.
101 | public let wpBaseSiteURL: URL?
102 |
103 | /// The base blog URL of the channel.
104 | public let wpBaseBlogURL: URL?
105 |
106 | /// Indicates whether the podcast is locked.
107 | public let podcastLocked: PodcastLocked?
108 |
109 | /// The fundings associated with the podcast.
110 | public let podcastFundings: [PodcastFunding]
111 |
112 | /// The people associated with the podcast.
113 | public let podcastPeople: [PodcastPerson]
114 | }
115 |
116 | extension RSSChannel {
117 | /// A struct representing an Atom category.
118 | /// A computed property that returns a ``SyndicationUpdate`` object
119 | /// based on the ``syUpdatePeriod`` and ``syUpdateFrequency`` properties.
120 | ///
121 | /// - Returns: A ``SyndicationUpdate`` object.
122 | /// - SeeAlso: ``EntryCategory``
123 | public var syndication: SyndicationUpdate? {
124 | SyndicationUpdate(
125 | period: syUpdatePeriod,
126 | frequency: syUpdateFrequency?.value
127 | )
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSFeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing an Atom category.
4 | /// RSS is a Web content syndication format.
5 | ///
6 | /// Its name is an acronym for Really Simple Syndication.
7 | /// RSS is dialect of XML.
8 | /// All RSS files must conform to the XML 1.0 specification,
9 | /// as published on the World Wide Web Consortium (W3C) website.
10 | /// At the top level, a RSS document is a element,
11 | /// with a mandatory attribute called version,
12 | /// that specifies the version of RSS that the document conforms to.
13 | /// If it conforms to this specification,
14 | /// the version attribute must be 2.0.
15 | /// For more details, check out the
16 | /// [W3 sepcifications.](https://validator.w3.org/feed/docs/rss2.html)
17 | /// - SeeAlso: ``EntryCategory``
18 | public struct RSSFeed {
19 | /// Root Channel of hte RSS Feed
20 | public let channel: RSSChannel
21 | }
22 |
23 | extension RSSFeed: DecodableFeed {
24 | internal static let source: DecoderSetup = DecoderSource.xml
25 | public static let label: String = "RSS"
26 |
27 | public var youtubeChannelID: String? {
28 | nil
29 | }
30 |
31 | public var authors: [Author] {
32 | guard let author = channel.author else {
33 | return []
34 | }
35 | return [author]
36 | }
37 |
38 | public var children: [Entryable] {
39 | channel.items
40 | }
41 |
42 | public var title: String {
43 | channel.title
44 | }
45 |
46 | public var siteURL: URL? {
47 | channel.link
48 | }
49 |
50 | public var summary: String? {
51 | channel.description
52 | }
53 |
54 | public var updated: Date? {
55 | channel.lastBuildDate
56 | }
57 |
58 | public var copyright: String? {
59 | channel.copyright
60 | }
61 |
62 | public var image: URL? {
63 | channel.image?.link
64 | }
65 |
66 | public var syndication: SyndicationUpdate? {
67 | channel.syndication
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSImage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Represents a GIF, JPEG, or PNG image.
4 | public struct RSSImage: Codable {
5 | /// The URL of the image.
6 | public let url: URL
7 |
8 | /// The title or description of the image.
9 | ///
10 | /// This is used in the ``alt`` attribute of the HTML `
` tag
11 | /// when the channel is rendered in HTML.
12 | public let title: String
13 |
14 | /// The URL of the site that the image links to.
15 | ///
16 | /// In practice, the image ``title`` and ``link`` should have
17 | /// the same value as the channel's ``title`` and ``link``.
18 | public let link: URL
19 |
20 | /// The width of the image in pixels.
21 | public let width: Int?
22 |
23 | /// The height of the image in pixels.
24 | public let height: Int?
25 |
26 | /// Additional description of the image.
27 | ///
28 | /// This text is included in the ``title`` attribute of the link
29 | /// formed around the image in the HTML rendering.
30 | public let description: String?
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem+Decodings.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XMLCoder
3 |
4 | extension RSSItem {
5 | // swiftlint:disable function_body_length
6 | /// A struct representing an Atom category.
7 | /// Initializes a new ``RSSItem`` by decoding data from a decoder.
8 | ///
9 | /// - Parameter decoder: The decoder to read data from.
10 | ///
11 | /// - Throws: An error if the decoding fails.
12 | /// - SeeAlso: ``EntryCategory``
13 | public init(from decoder: Decoder) throws {
14 | let container = try decoder.container(keyedBy: CodingKeys.self)
15 | title = try container.decode(String.self, forKey: .title)
16 | link = try container.decodeIfPresent(URL.self, forKey: .link)
17 | description = try container.decodeIfPresent(CData.self, forKey: .description)
18 | guid = try container.decode(EntryID.self, forKey: .guid)
19 | pubDate = try container.decodeDateIfPresentAndValid(forKey: .pubDate)
20 | contentEncoded = try container.decodeIfPresent(CData.self, forKey: .contentEncoded)
21 | categoryTerms = try container.decode([RSSItemCategory].self, forKey: .categoryTerms)
22 | content = try container.decodeIfPresent(String.self, forKey: .content)
23 | itunesTitle = try container.decodeIfPresent(String.self, forKey: .itunesTitle)
24 | itunesEpisode = try container.decodeIfPresent(
25 | iTunesEpisode.self, forKey: .itunesEpisode
26 | )
27 | itunesAuthor = try container.decodeIfPresent(String.self, forKey: .itunesAuthor)
28 | itunesSubtitle = try container.decodeIfPresent(String.self, forKey: .itunesSubtitle)
29 | itunesSummary = try container.decodeIfPresent(CData.self, forKey: .itunesSummary)
30 | itunesExplicit = try container.decodeIfPresent(String.self, forKey: .itunesExplicit)
31 | itunesDuration = try container.decodeIfPresent(
32 | iTunesDuration.self, forKey: .itunesDuration
33 | )
34 | itunesImage = try container.decodeIfPresent(iTunesImage.self, forKey: .itunesImage)
35 |
36 | podcastPeople = try container.decodeIfPresent(
37 | [PodcastPerson].self,
38 | forKey: .podcastPeople
39 | ) ?? []
40 | podcastTranscripts = try container.decodeIfPresent(
41 | [PodcastTranscript].self,
42 | forKey: .podcastTranscripts
43 | ) ?? []
44 | podcastChapters = try container.decodeIfPresent(
45 | PodcastChapters.self,
46 | forKey: .podcastChapters
47 | )
48 | podcastSoundbites = try container.decodeIfPresent(
49 | [PodcastSoundbite].self,
50 | forKey: .podcastSoundbites
51 | ) ?? []
52 |
53 | podcastSeason = try container.decodeIfPresent(
54 | PodcastSeason.self,
55 | forKey: .podcastSeason
56 | )
57 |
58 | enclosure = try container.decodeIfPresent(Enclosure.self, forKey: .enclosure)
59 | creators = try container.decode([String].self, forKey: .creators)
60 |
61 | mediaContent =
62 | try container.decodeIfPresent(AtomMedia.self, forKey: .mediaContent)
63 | mediaThumbnail =
64 | try container.decodeIfPresent(AtomMedia.self, forKey: .mediaThumbnail)
65 |
66 | wpPostID = try container.decodeIfPresent(Int.self, forKey: .wpPostID)
67 | wpPostDate = try container.decodeIfPresent(Date.self, forKey: .wpPostDate)
68 | let wpPostDateGMT = try container.decodeIfPresent(
69 | String.self, forKey: .wpPostDateGMT
70 | )
71 | if let wpPostDateGMT = wpPostDateGMT {
72 | if wpPostDateGMT == "0000-00-00 00:00:00" {
73 | self.wpPostDateGMT = nil
74 | } else {
75 | self.wpPostDateGMT = try container.decode(
76 | Date.self, forKey: .wpPostDateGMT
77 | )
78 | }
79 | } else {
80 | self.wpPostDateGMT = nil
81 | }
82 |
83 | wpModifiedDate = try container.decodeIfPresent(
84 | Date.self, forKey: .wpModifiedDate
85 | )
86 |
87 | let wpModifiedDateGMT = try container.decodeIfPresent(
88 | String.self, forKey: .wpModifiedDateGMT
89 | )
90 | if let wpModifiedDateGMT = wpModifiedDateGMT {
91 | if wpModifiedDateGMT == "0000-00-00 00:00:00" {
92 | self.wpModifiedDateGMT = nil
93 | } else {
94 | self.wpModifiedDateGMT = try container.decode(
95 | Date.self, forKey: .wpModifiedDateGMT
96 | )
97 | }
98 | } else {
99 | self.wpModifiedDateGMT = nil
100 | }
101 |
102 | let wpAttachmentURLCDData = try container.decodeIfPresent(
103 | CData.self,
104 | forKey: .wpAttachmentURL
105 | )
106 | wpAttachmentURL = wpAttachmentURLCDData.map { $0.value }.flatMap(URL.init(string:))
107 |
108 | wpPostName = try container.decodeIfPresent(CData.self, forKey: .wpPostName)
109 | wpPostType = try container.decodeIfPresent(CData.self, forKey: .wpPostType)
110 | wpPostMeta = try container.decodeIfPresent(
111 | [WordPressElements.PostMeta].self,
112 | forKey: .wpPostMeta
113 | ) ?? []
114 | wpCommentStatus = try container.decodeIfPresent(CData.self, forKey: .wpCommentStatus)
115 | wpPingStatus = try container.decodeIfPresent(CData.self, forKey: .wpPingStatus)
116 | wpStatus = try container.decodeIfPresent(CData.self, forKey: .wpStatus)
117 | wpPostParent = try container.decodeIfPresent(Int.self, forKey: .wpPostParent)
118 | wpMenuOrder = try container.decodeIfPresent(Int.self, forKey: .wpMenuOrder)
119 | wpIsSticky = try container.decodeIfPresent(Int.self, forKey: .wpIsSticky)
120 | wpPostPassword = try container.decodeIfPresent(
121 | CData.self, forKey: .wpPostPassword
122 | )
123 | }
124 |
125 | // swiftlint:enable function_body_length
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSItem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XMLCoder
3 |
4 | public struct RSSItem: Codable {
5 | public enum CodingKeys: String, CodingKey {
6 | case title
7 | case link
8 | case description
9 | case guid
10 | case pubDate
11 | case categoryTerms = "category"
12 | case enclosure
13 | case contentEncoded = "content:encoded"
14 | case content
15 | case itunesTitle = "itunes:title"
16 | case itunesEpisode = "itunes:episode"
17 | case itunesAuthor = "itunes:author"
18 | case itunesSubtitle = "itunes:subtitle"
19 | case itunesSummary = "itunes:summary"
20 | case itunesExplicit = "itunes:explicit"
21 | case podcastPeople = "podcast:person"
22 | case podcastTranscripts = "podcast:transcript"
23 | case podcastChapters = "podcast:chapters"
24 | case podcastSoundbites = "podcast:soundbite"
25 | case podcastSeason = "podcast:season"
26 | case itunesDuration = "itunes:duration"
27 | case itunesImage = "itunes:image"
28 | case creators = "dc:creator"
29 |
30 | case wpPostID = "wp:postId"
31 | case wpPostDate = "wp:postDate"
32 | case wpPostDateGMT = "wp:postDateGmt"
33 | case wpModifiedDate = "wp:postModified"
34 | case wpModifiedDateGMT = "wp:postModifiedGmt"
35 | case wpPostName = "wp:postName"
36 | case wpPostType = "wp:postType"
37 | case wpPostMeta = "wp:postmeta"
38 | case wpCommentStatus = "wp:commentStatus"
39 | case wpPingStatus = "wp:pingStatus"
40 | case wpAttachmentURL = "wp:attachmentUrl"
41 |
42 | case wpStatus = "wp:status"
43 | case wpPostParent = "wp:postParent"
44 | case wpMenuOrder = "wp:menuOrder"
45 | case wpIsSticky = "wp:isSticky"
46 | case wpPostPassword = "wp:postPassword"
47 |
48 | case mediaContent = "media:content"
49 | case mediaThumbnail = "media:thumbnail"
50 | }
51 |
52 | public let title: String
53 | public let link: URL?
54 | public let description: CData?
55 | public let guid: EntryID
56 | public let pubDate: Date?
57 | public let contentEncoded: CData?
58 | public let categoryTerms: [RSSItemCategory]
59 | public let content: String?
60 | public let itunesTitle: String?
61 | public let itunesEpisode: iTunesEpisode?
62 | public let itunesAuthor: String?
63 | public let itunesSubtitle: String?
64 | public let itunesSummary: CData?
65 | public let itunesExplicit: String?
66 | public let itunesDuration: iTunesDuration?
67 | public let itunesImage: iTunesImage?
68 | public let podcastPeople: [PodcastPerson]
69 | public let podcastTranscripts: [PodcastTranscript]
70 | public let podcastChapters: PodcastChapters?
71 | public let podcastSoundbites: [PodcastSoundbite]
72 | public let podcastSeason: PodcastSeason?
73 | public let enclosure: Enclosure?
74 | public let creators: [String]
75 | public let wpCommentStatus: CData?
76 | public let wpPingStatus: CData?
77 | public let wpStatus: CData?
78 | public let wpPostParent: Int?
79 | public let wpMenuOrder: Int?
80 | public let wpIsSticky: Int?
81 | public let wpPostPassword: CData?
82 | public let wpPostID: Int?
83 | public let wpPostDate: Date?
84 | public let wpPostDateGMT: Date?
85 | public let wpModifiedDate: Date?
86 | public let wpModifiedDateGMT: Date?
87 | public let wpPostName: CData?
88 | public let wpPostType: CData?
89 | public let wpPostMeta: [WordPressElements.PostMeta]
90 | public let wpAttachmentURL: URL?
91 | public let mediaContent: AtomMedia?
92 | public let mediaThumbnail: AtomMedia?
93 | }
94 |
95 | extension RSSItem: Entryable {
96 | public var categories: [EntryCategory] {
97 | categoryTerms
98 | }
99 |
100 | public var url: URL? {
101 | link
102 | }
103 |
104 | public var contentHtml: String? {
105 | contentEncoded?.value ?? content ?? description?.value
106 | }
107 |
108 | public var summary: String? {
109 | description?.value
110 | }
111 |
112 | public var authors: [Author] {
113 | let authors = creators.map(Author.init)
114 | guard authors.isEmpty else {
115 | return authors
116 | }
117 | guard let author = itunesAuthor.map(Author.init) else {
118 | return []
119 | }
120 | return [author]
121 | }
122 |
123 | public var id: EntryID {
124 | guid
125 | }
126 |
127 | public var published: Date? {
128 | pubDate
129 | }
130 |
131 | public var media: MediaContent? {
132 | PodcastEpisodeProperties(rssItem: self).map(MediaContent.podcast)
133 | }
134 |
135 | public var imageURL: URL? {
136 | itunesImage?.href ??
137 | mediaThumbnail?.url ??
138 | mediaContent?.url
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Feeds/RSS/RSSItemCategory.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing a category for an RSS item.
3 | ///
4 | /// This struct conforms to the ``Codable`` and ``EntryCategory`` protocols.
5 | ///
6 | /// - Note: The ``CodingKeys`` enum is used to specify the coding keys for the struct.
7 | ///
8 | /// - SeeAlso: ``EntryCategory``
9 | ///
10 | /// - Remark: This struct is ``public`` to allow access from other modules.
11 | ///
12 | /// - Warning: Do not modify the ``CodingKeys`` enum.
13 | ///
14 | /// - Version: 1.0
15 | /// - SeeAlso: ``EntryCategory``
16 | public struct RSSItemCategory: Codable, EntryCategory {
17 | /// The coding keys for the struct.
18 | internal enum CodingKeys: String, CodingKey {
19 | case value = "#CDATA"
20 | case domain
21 | case nicename
22 | }
23 |
24 | /// The term of the category.
25 | public var term: String {
26 | value
27 | }
28 |
29 | /// The value of the category.
30 | public let value: String
31 |
32 | /// The domain of the category.
33 | public let domain: String?
34 |
35 | /// The nicename of the category.
36 | public let nicename: String?
37 |
38 | /// A struct representing an Atom category.
39 | /// Initializes a new instance of ``RSSItemCategory``.
40 | ///
41 | /// - Parameters:
42 | /// - value: The value of the category.
43 | /// - domain: The domain of the category. Default value is ``nil``.
44 | /// - nicename: The nicename of the category. Default value is ``nil``.
45 | ///
46 | /// - Returns: A new instance of ``RSSItemCategory``.
47 | /// - SeeAlso: ``EntryCategory``
48 | public init(value: String, domain: String? = nil, nicename: String? = nil) {
49 | self.value = value
50 | self.domain = domain
51 | self.nicename = nicename
52 | }
53 |
54 | /// A struct representing an Atom category.
55 | /// Initializes a new instance of ``RSSItemCategory`` from a decoder.
56 | ///
57 | /// - Parameter decoder: The decoder to use for decoding.
58 | ///
59 | /// - Throws: An error if the decoding fails.
60 | ///
61 | /// - Returns: A new instance of ``RSSItemCategory``.
62 | /// - SeeAlso: ``EntryCategory``
63 | public init(from decoder: Decoder) throws {
64 | let value: String
65 | let container: KeyedDecodingContainer?
66 | do {
67 | let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
68 | value = try keyedContainer.decode(String.self, forKey: .value)
69 | container = keyedContainer
70 | } catch {
71 | let singleValueContainer = try decoder.singleValueContainer()
72 | value = try singleValueContainer.decode(String.self)
73 | container = nil
74 | }
75 | self.value = value
76 | domain = try container?.decodeIfPresent(String.self, forKey: .domain)
77 | nicename = try container?.decodeIfPresent(String.self, forKey: .nicename)
78 | }
79 | }
80 |
81 | extension RSSItemCategory: Equatable {
82 | /// A struct representing an Atom category.
83 | /// Checks if two ``RSSItemCategory`` instances are equal.
84 | ///
85 | /// - Parameters:
86 | /// - lhs: The left-hand side ``RSSItemCategory`` instance.
87 | /// - rhs: The right-hand side ``RSSItemCategory`` instance.
88 | ///
89 | /// - Returns: ``true`` if the instances are equal, otherwise ``false``.
90 | /// - SeeAlso: ``EntryCategory``
91 | public static func == (lhs: RSSItemCategory, rhs: RSSItemCategory) -> Bool {
92 | lhs.value == rhs.value
93 | && lhs.domain == rhs.domain
94 | && lhs.nicename == rhs.nicename
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/MediaContent.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// Represents different types of media content.
3 | ///
4 | /// - podcast: A podcast episode.
5 | /// - video: A video.
6 | /// - SeeAlso: ``EntryCategory``
7 | public enum MediaContent {
8 | /// A podcast episode.
9 | case podcast(PodcastEpisode)
10 |
11 | /// A video.
12 | case video(Video)
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters+MimeType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension PodcastChapters {
4 | /// A private enum representing known MIME types for podcast chapters.
5 | private enum KnownMimeType: String, Codable {
6 | case json = "application/json+chapters"
7 |
8 | /// Initializes a ``KnownMimeType`` from a case-insensitive string.
9 | init?(caseInsensitive: String) {
10 | self.init(rawValue: caseInsensitive)
11 | }
12 |
13 | /// Initializes a ``KnownMimeType`` from a ``MimeType``.
14 | init?(mimeType: MimeType) {
15 | switch mimeType {
16 | case .json:
17 | self = .json
18 |
19 | case .unknown:
20 | return nil
21 | }
22 | }
23 | }
24 |
25 | /// An enum representing the MIME type of podcast chapters.
26 | public enum MimeType: Codable, Equatable, RawRepresentable {
27 | case json
28 | case unknown(String)
29 |
30 | /// The raw value of the MIME type.
31 | public var rawValue: String {
32 | if let knownMimeType = KnownMimeType(mimeType: self) {
33 | return knownMimeType.rawValue
34 | } else if case let .unknown(string) = self {
35 | return string
36 | } else {
37 | fatalError(
38 | // swiftlint:disable:next line_length
39 | "Type attribute of with value: \(self) should either be ``KnownMimeType``, or unknown!"
40 | )
41 | }
42 | }
43 |
44 | /// Initializes a ``MimeType`` from a raw value.
45 | public init?(rawValue: String) {
46 | self.init(caseInsensitive: rawValue)
47 | }
48 |
49 | /// Initializes a ``MimeType`` from a case-insensitive string.
50 | public init(caseInsensitive: String) {
51 | if let knownMimeType = KnownMimeType(caseInsensitive: caseInsensitive) {
52 | self = .init(knownMimeType: knownMimeType)
53 | } else {
54 | self = .unknown(caseInsensitive)
55 | }
56 | }
57 |
58 | /// Initializes a ``MimeType`` from a ``KnownMimeType``.
59 | private init(knownMimeType: KnownMimeType) {
60 | switch knownMimeType {
61 | case .json:
62 | self = .json
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastChapters.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing chapters of a podcast.
4 | public struct PodcastChapters: Codable, Equatable {
5 | /// The coding keys for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case url
8 | case type
9 | }
10 |
11 | /// The URL of the chapter file.
12 | public let url: URL
13 |
14 | /// The MIME type of the chapter file.
15 | public let type: MimeType
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastEpisode.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing properties of a podcast episode.
4 | internal struct PodcastEpisodeProperties: PodcastEpisode {
5 | /// The title of the episode.
6 | internal let title: String?
7 |
8 | /// The episode number.
9 | internal let episode: Int?
10 |
11 | /// The author of the episode.
12 | internal let author: String?
13 |
14 | /// The subtitle of the episode.
15 | internal let subtitle: String?
16 |
17 | /// A summary of the episode.
18 | internal let summary: String?
19 |
20 | /// Indicates if the episode contains explicit content.
21 | internal let explicit: String?
22 |
23 | /// The duration of the episode.
24 | internal let duration: TimeInterval?
25 |
26 | /// The image associated with the episode.
27 | internal let image: iTunesImage?
28 |
29 | /// The enclosure of the episode.
30 | internal let enclosure: Enclosure
31 |
32 | /// The people involved in the episode.
33 | internal let people: [PodcastPerson]
34 |
35 | /// A struct representing an Atom category.
36 | /// Initializes a ``PodcastEpisodeProperties`` instance from an ``RSSItem``.
37 | ///
38 | /// - Parameter rssItem: The ``RSSItem`` to extract the properties from.
39 | ///
40 | /// - Returns: An initialized ``PodcastEpisodeProperties`` instance,
41 | /// or ``nil`` if the ``enclosure`` property is missing.
42 | /// - SeeAlso: ``EntryCategory``
43 | internal init?(rssItem: RSSItem) {
44 | guard let enclosure = rssItem.enclosure else {
45 | return nil
46 | }
47 | title = rssItem.itunesTitle
48 | episode = rssItem.itunesEpisode?.value
49 | author = rssItem.itunesAuthor
50 | subtitle = rssItem.itunesSubtitle
51 | summary = rssItem.itunesSummary?.value
52 | explicit = rssItem.itunesExplicit
53 | duration = rssItem.itunesDuration?.value
54 | image = rssItem.itunesImage
55 | self.enclosure = enclosure
56 | people = rssItem.podcastPeople
57 | }
58 | }
59 |
60 | /// A protocol representing a podcast episode.
61 | public protocol PodcastEpisode {
62 | /// The title of the episode.
63 | var title: String? { get }
64 |
65 | /// The episode number.
66 | var episode: Int? { get }
67 |
68 | /// The author of the episode.
69 | var author: String? { get }
70 |
71 | /// The subtitle of the episode.
72 | var subtitle: String? { get }
73 |
74 | /// A summary of the episode.
75 | var summary: String? { get }
76 |
77 | /// Indicates if the episode contains explicit content.
78 | var explicit: String? { get }
79 |
80 | /// The duration of the episode.
81 | var duration: TimeInterval? { get }
82 |
83 | /// The image associated with the episode.
84 | var image: iTunesImage? { get }
85 |
86 | /// The enclosure of the episode.
87 | var enclosure: Enclosure { get }
88 |
89 | /// The people involved in the episode.
90 | var people: [PodcastPerson] { get }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastFunding.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing funding information for a podcast.
4 | public struct PodcastFunding: Codable, Equatable {
5 | /// The coding keys used for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case url
8 | case description = ""
9 | }
10 |
11 | /// The URL for the funding source.
12 | public let url: URL
13 |
14 | /// A description of the funding source.
15 | public let description: String?
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+GeoURI.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension PodcastLocation {
4 | /// A ``struct`` representing a geographic URI for a podcast location.
5 | public struct GeoURI: Codable, Equatable, LosslessStringConvertible {
6 | /// The latitude coordinate.
7 | public let latitude: Double
8 |
9 | /// The longitude coordinate.
10 | public let longitude: Double
11 |
12 | /// The altitude coordinate, if available.
13 | public let altitude: Double?
14 |
15 | /// The accuracy of the coordinates, if available.
16 | public let accuracy: Double?
17 |
18 | /// A string representation of the geographic URI.
19 | public var description: String {
20 | var description = "geo:\(latitude),\(longitude)"
21 |
22 | if let altitude = altitude {
23 | description += ",\(altitude)"
24 | }
25 |
26 | if let accuracy = accuracy {
27 | description += ";u=\(accuracy)"
28 | }
29 |
30 | return description
31 | }
32 |
33 | /// Initializes a ``GeoURI`` instance with the specified coordinates.
34 | ///
35 | /// - Parameters:
36 | /// - latitude: The latitude coordinate.
37 | /// - longitude: The longitude coordinate.
38 | /// - altitude: The altitude coordinate, if available.
39 | /// - accuracy: The accuracy of the coordinates, if available.
40 | public init(
41 | latitude: Double,
42 | longitude: Double,
43 | altitude: Double? = nil,
44 | accuracy: Double? = nil
45 | ) {
46 | self.latitude = latitude
47 | self.longitude = longitude
48 | self.altitude = altitude
49 | self.accuracy = accuracy
50 | }
51 |
52 | /// Initializes a ``GeoURI`` instance from a string representation.
53 | ///
54 | /// - Parameter description: The string representation of the geographic URI.
55 | public init?(_ description: String) {
56 | try? self.init(singleValue: description)
57 | }
58 |
59 | // swiftlint:disable function_body_length
60 | /// Initializes a ``GeoURI`` instance from a single value string.
61 | ///
62 | /// - Parameter singleValue: The single value string representing the geographic URI.
63 | /// - Throws: A ``DecodingError`` if the single value string is invalid.
64 | public init(singleValue: String) throws {
65 | let pathComponents = try Self.pathComponents(from: singleValue)
66 |
67 | guard
68 | let geoCoords = pathComponents[safe: 0]?.split(separator: ","),
69 | let latitude = geoCoords[safe: 0]?.asDouble(),
70 | let longitude = geoCoords[safe: 1]?.asDouble()
71 | else {
72 | throw DecodingError.dataCorrupted(
73 | codingKey: PodcastLocation.CodingKeys.geo,
74 | debugDescription: "Invalid coordinates for geo attribute: \(singleValue)"
75 | )
76 | }
77 |
78 | let altitude = geoCoords[safe: 2]?.asDouble()
79 |
80 | let accuracy = pathComponents[safe: 1]?
81 | .split(separator: "=")[safe: 1]?
82 | .asDouble()
83 |
84 | self.init(
85 | latitude: latitude,
86 | longitude: longitude,
87 | altitude: altitude,
88 | accuracy: accuracy
89 | )
90 | }
91 |
92 | // swiftlint:enable function_body_length
93 |
94 | /// Initializes a ``GeoURI`` instance from a decoder.
95 | ///
96 | /// - Parameter decoder: The decoder to read data from.
97 | /// - Throws: A ``DecodingError`` if the decoding process fails.
98 | public init(from decoder: Decoder) throws {
99 | let container = try decoder.singleValueContainer()
100 | let singleValue = try container.decode(String.self)
101 |
102 | try self.init(singleValue: singleValue)
103 | }
104 |
105 | private static func pathComponents(from string: String) throws -> [Substring] {
106 | let components = string.split(separator: ":")
107 |
108 | guard
109 | components[safe: 0] == "geo" else {
110 | throw DecodingError.dataCorrupted(
111 | codingKey: PodcastLocation.CodingKeys.geo,
112 | debugDescription: "Invalid prefix for geo attribute: \(string)"
113 | )
114 | }
115 | guard let geoPath = components[safe: 1] else {
116 | throw DecodingError.dataCorrupted(
117 | codingKey: PodcastLocation.CodingKeys.geo,
118 | debugDescription: "Invalid path for geo attribute: \(string)"
119 | )
120 | }
121 |
122 | return geoPath.split(separator: ";")
123 | }
124 |
125 | /// Encodes the ``GeoURI`` instance into the given encoder.
126 | ///
127 | /// - Parameter encoder: The encoder to write data to.
128 | /// - Throws: An error if the encoding process fails.
129 | public func encode(to encoder: Encoder) throws {
130 | var container = encoder.singleValueContainer()
131 | try container.encode(description)
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation+OsmQuery.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension PodcastLocation {
4 | /// Represents a query for OpenStreetMap (OSM) data.
5 | public struct OsmQuery: Codable, Equatable {
6 | // swiftlint:disable nesting
7 | /// The type of OSM element.
8 | public enum OsmType: String, Codable, CaseIterable {
9 | case node = "N"
10 | case way = "W"
11 | case relation = "R"
12 | }
13 |
14 | // swiftlint:enable nesting
15 |
16 | /// The ID of the OSM element.
17 | public let id: Int
18 |
19 | /// The type of the OSM element.
20 | public let type: OsmType
21 |
22 | /// The revision number of the OSM element.
23 | public let revision: Int?
24 |
25 | /// Initializes an ``OsmQuery`` instance from a decoder.
26 | ///
27 | /// - Parameter decoder: The decoder to read data from.
28 | /// - Throws: `DecodingError.dataCorrupted` if the data is invalid.
29 | public init(from decoder: Decoder) throws {
30 | let container = try decoder.singleValueContainer()
31 |
32 | var osmStr = try container.decode(String.self)
33 |
34 | guard let osmType = osmStr.removeFirst().asOsmType() else {
35 | throw DecodingError.dataCorrupted(
36 | codingKey: PodcastLocation.CodingKeys.osmQuery,
37 | debugDescription: "Invalid type for osm attribute: \(osmStr)"
38 | )
39 | }
40 | guard let osmID = osmStr.split(separator: "#")[safe: 0]?.asExactInt() else {
41 | throw DecodingError.dataCorrupted(
42 | codingKey: PodcastLocation.CodingKeys.osmQuery,
43 | debugDescription: "Invalid id of type Int for osm attribute: \(osmStr)"
44 | )
45 | }
46 | let osmRevision = osmStr.split(separator: "#")[safe: 1]?.asInt()
47 |
48 | id = osmID
49 | type = osmType
50 | revision = osmRevision
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing the location of a podcast.
4 | public struct PodcastLocation: Codable, Equatable {
5 | /// The geographic coordinates of the location.
6 | internal enum CodingKeys: String, CodingKey {
7 | case geo
8 | case osmQuery = "osm"
9 |
10 | case name = ""
11 | }
12 |
13 | /// The geographic coordinates of the location.
14 | public let geo: GeoURI?
15 |
16 | /// The OpenStreetMap query for the location.
17 | public let osmQuery: OsmQuery?
18 |
19 | /// The name of the location.
20 | public let name: String
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastLocked.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a locked podcast.
4 | public struct PodcastLocked: Codable, Equatable {
5 | /// Coding keys for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case owner
8 | case isLocked = ""
9 | }
10 |
11 | /// The owner of the podcast.
12 | public let owner: String?
13 |
14 | /// Indicates whether the podcast is locked.
15 | public let isLocked: Bool
16 |
17 | /// Initializes a new instance of ``PodcastLocked`` from a decoder.
18 | ///
19 | /// - Parameter decoder: The decoder to read data from.
20 | /// - Throws: An error if the decoding process fails.
21 | public init(from decoder: Decoder) throws {
22 | let container = try decoder.container(keyedBy: CodingKeys.self)
23 | owner = try container.decodeIfPresent(String.self, forKey: .owner)
24 | isLocked = try container.decode(String.self, forKey: .isLocked).lowercased() == "yes"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson+Role.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension PodcastPerson {
4 | /// A private enum representing known roles for a podcast person.
5 | private enum KnownRole: String {
6 | case guest
7 | case host
8 | case editor
9 | case writer
10 | case designer
11 | case composer
12 | case producer
13 |
14 | /// Initializes a ``KnownRole`` with a case-insensitive string.
15 | init?(caseInsensitive: String) {
16 | self.init(rawValue: caseInsensitive.lowercased())
17 | }
18 |
19 | // swiftlint:disable function_body_length cyclomatic_complexity
20 | /// Initializes a ``KnownRole`` with a ``Role`` value.
21 | init?(role: Role) {
22 | switch role {
23 | case .guest:
24 | self = .guest
25 |
26 | case .host:
27 | self = .host
28 |
29 | case .editor:
30 | self = .editor
31 |
32 | case .writer:
33 | self = .writer
34 |
35 | case .designer:
36 | self = .designer
37 |
38 | case .composer:
39 | self = .composer
40 |
41 | case .producer:
42 | self = .producer
43 |
44 | case .unknown:
45 | return nil
46 | }
47 | }
48 | }
49 |
50 | // swiftlint:enable function_body_length cyclomatic_complexity
51 |
52 | /// An enum representing the role of a podcast person.
53 | public enum Role: Codable, Equatable, RawRepresentable {
54 | case guest
55 | case host
56 | case editor
57 | case writer
58 | case designer
59 | case composer
60 | case producer
61 | case unknown(String)
62 |
63 | /// The raw value of the role.
64 | public var rawValue: String {
65 | if let knownRole = KnownRole(role: self) {
66 | return knownRole.rawValue
67 | } else if case let .unknown(string) = self {
68 | return string
69 | } else {
70 | fatalError(
71 | // swiftlint:disable:next line_length
72 | "Role attribute of with value: \(self) should either be a ``KnownRole``, or unknown!"
73 | )
74 | }
75 | }
76 |
77 | /// Initializes a ``Role`` with a raw value.
78 | public init?(rawValue: String) {
79 | self.init(caseInsensitive: rawValue)
80 | }
81 |
82 | /// Initializes a ``Role`` with a case-insensitive string.
83 | public init(caseInsensitive: String) {
84 | if let knownRole = KnownRole(caseInsensitive: caseInsensitive) {
85 | self = .init(knownRole: knownRole)
86 | } else {
87 | self = .unknown(caseInsensitive)
88 | }
89 | }
90 |
91 | // swiftlint:disable:next cyclomatic_complexity
92 | private init(knownRole: KnownRole) {
93 | switch knownRole {
94 | case .guest:
95 | self = .guest
96 |
97 | case .host:
98 | self = .host
99 |
100 | case .editor:
101 | self = .editor
102 |
103 | case .writer:
104 | self = .writer
105 |
106 | case .designer:
107 | self = .designer
108 |
109 | case .composer:
110 | self = .composer
111 |
112 | case .producer:
113 | self = .producer
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastPerson.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a person associated with a podcast.
4 | public struct PodcastPerson: Codable, Equatable {
5 | /// The role of the person.
6 | public enum CodingKeys: String, CodingKey {
7 | case role
8 | case group
9 | case href
10 | case img
11 | case fullname = ""
12 | }
13 |
14 | /// The role of the person.
15 | public let role: Role?
16 |
17 | /// The group the person belongs to.
18 | public let group: String?
19 |
20 | /// The URL associated with the person.
21 | public let href: URL?
22 |
23 | /// The URL of the person's image.
24 | public let img: URL?
25 |
26 | /// The full name of the person.
27 | public let fullname: String
28 |
29 | /// Initializes a new instance of ``PodcastPerson``
30 | /// by decoding data from the given decoder.
31 | ///
32 | /// - Parameter decoder: The decoder to read data from.
33 | /// - Throws: An error if the decoding process fails.
34 | public init(from decoder: Decoder) throws {
35 | let container = try decoder.container(keyedBy: CodingKeys.self)
36 | role = try container.decodeIfPresent(Role.self, forKey: .role)
37 | group = try container.decodeIfPresent(String.self, forKey: .group)
38 | fullname = try container.decode(String.self, forKey: .fullname)
39 |
40 | let hrefUrl = try container.decodeIfPresent(String.self, forKey: .href) ?? ""
41 | href = hrefUrl.isEmpty ? nil : URL(string: hrefUrl)
42 |
43 | let imgUrl = try container.decodeIfPresent(String.self, forKey: .img) ?? ""
44 | img = imgUrl.isEmpty ? nil : URL(string: imgUrl)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastSeason.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a season of a podcast.
4 | public struct PodcastSeason: Codable, Equatable {
5 | /// The coding keys for the ``PodcastSeason`` struct.
6 | public enum CodingKeys: String, CodingKey {
7 | case name
8 | case number = ""
9 | }
10 |
11 | /// The name of the season.
12 | public let name: String?
13 |
14 | /// The number of the season.
15 | public let number: Int
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastSoundbite.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a soundbite from a podcast.
4 | public struct PodcastSoundbite: Codable, Equatable {
5 | /// The coding keys used for encoding and decoding.
6 | public enum CodingKeys: String, CodingKey {
7 | case startTime
8 | case duration
9 |
10 | case title = ""
11 | }
12 |
13 | /// The start time of the soundbite.
14 | public let startTime: TimeInterval
15 |
16 | /// The duration of the soundbite.
17 | public let duration: TimeInterval
18 |
19 | /// The title of the soundbite.
20 | public let title: String?
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript+MimeType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension PodcastTranscript {
4 | /// A private enum representing known MIME types for the transcript.
5 | private enum KnownMimeType: String, Codable {
6 | case plain = "text/plain"
7 | case html = "text/html"
8 | case srt = "text/srt"
9 | case vtt = "text/vtt"
10 | case json = "application/json"
11 | case subrip = "application/x-subrip"
12 |
13 | /// Initializes a ``KnownMimeType`` with a case-insensitive string.
14 | init?(caseInsensitive: String) {
15 | self.init(rawValue: caseInsensitive)
16 | }
17 |
18 | // swiftlint:disable cyclomatic_complexity
19 | /// Initializes a ``KnownMimeType`` with a ``MimeType``.
20 | init?(mimeType: MimeType) {
21 | switch mimeType {
22 | case .plain:
23 | self = .plain
24 |
25 | case .html:
26 | self = .html
27 |
28 | case .srt:
29 | self = .srt
30 |
31 | case .vtt:
32 | self = .vtt
33 |
34 | case .json:
35 | self = .json
36 |
37 | case .subrip:
38 | self = .subrip
39 |
40 | case .unknown:
41 | return nil
42 | }
43 | }
44 |
45 | // swiftlint:enable cyclomatic_complexity
46 | }
47 |
48 | /// An enum representing the MIME type of the transcript.
49 | public enum MimeType: Codable, Equatable, RawRepresentable {
50 | case plain
51 | case html
52 | case srt
53 | case vtt
54 | case json
55 | case subrip
56 | case unknown(String)
57 |
58 | /// The raw value of the MIME type.
59 | public var rawValue: String {
60 | if let knownMimeType = KnownMimeType(mimeType: self) {
61 | return knownMimeType.rawValue
62 | } else if case let .unknown(string) = self {
63 | return string
64 | } else {
65 | fatalError(
66 | // swiftlint:disable:next line_length
67 | "Type attribute of with value: \(self) should either be a ``KnownMimeType``, or unknown!"
68 | )
69 | }
70 | }
71 |
72 | /// Initializes a ``MimeType`` with a raw value.
73 | public init?(rawValue: String) {
74 | self.init(caseInsensitive: rawValue)
75 | }
76 |
77 | /// Initializes a ``MimeType`` with a case-insensitive string.
78 | public init(caseInsensitive: String) {
79 | if let knownMimeType = KnownMimeType(caseInsensitive: caseInsensitive) {
80 | self = .init(knownMimeType: knownMimeType)
81 | } else {
82 | self = .unknown(caseInsensitive)
83 | }
84 | }
85 |
86 | /// Initializes a ``MimeType`` with a ``KnownMimeType``.
87 | private init(knownMimeType: KnownMimeType) {
88 | switch knownMimeType {
89 | case .plain:
90 | self = .plain
91 |
92 | case .html:
93 | self = .html
94 |
95 | case .srt:
96 | self = .srt
97 |
98 | case .vtt:
99 | self = .vtt
100 |
101 | case .json:
102 | self = .json
103 |
104 | case .subrip:
105 | self = .subrip
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Podcast/PodcastTranscript.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing a podcast transcript.
4 | public struct PodcastTranscript: Codable, Equatable {
5 | /// The coding keys for the podcast transcript.
6 | public enum CodingKeys: String, CodingKey {
7 | case url
8 | case type
9 | case language
10 | case rel
11 | }
12 |
13 | /// The relationship between the podcast transcript and the podcast.
14 | public enum Relationship: String, Codable {
15 | case captions
16 | }
17 |
18 | /// The URL of the podcast transcript.
19 | public let url: URL
20 |
21 | /// The MIME type of the podcast transcript.
22 | public let type: MimeType
23 |
24 | /// The language of the podcast transcript.
25 | public let language: String?
26 |
27 | /// The relationship of the podcast transcript.
28 | public let rel: Relationship?
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Video.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// An enumeration representing different types of videos.
3 | /// - SeeAlso: ``EntryCategory``
4 | public enum Video {
5 | /// A video from YouTube.
6 | /// - Parameters:
7 | /// - id: The ID of the YouTube video.
8 | case youtube(YouTubeID)
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Wordpress/WPCategory.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A typealias for the `WordPressElements.Category` type.
4 | public typealias WPCategory = WordPressElements.Category
5 |
6 | // swiftlint:disable nesting
7 | extension WordPressElements {
8 | /// A struct representing a category in WordPress.
9 | public struct Category: Codable {
10 | /// The coding keys for the ``Category`` struct.
11 | internal enum CodingKeys: String, CodingKey {
12 | case termID = "wp:termId"
13 | case niceName = "wp:categoryNicename"
14 | case parent = "wp:categoryParent"
15 | case name = "wp:catName"
16 | }
17 |
18 | /// The unique identifier of the category.
19 | public let termID: Int
20 |
21 | /// The nice name of the category.
22 | public let niceName: CData
23 |
24 | /// The parent category of the category.
25 | public let parent: CData
26 |
27 | /// The name of the category.
28 | public let name: String
29 |
30 | /// A struct representing an Atom category.
31 | /// Initializes a new ``Category`` instance.
32 | ///
33 | /// - Parameters:
34 | /// - termID: The unique identifier of the category.
35 | /// - niceName: The nice name of the category.
36 | /// - parent: The parent category of the category.
37 | /// - name: The name of the category.
38 | /// - SeeAlso: ``EntryCategory``
39 | public init(termID: Int, niceName: CData, parent: CData, name: String) {
40 | self.termID = termID
41 | self.niceName = niceName
42 | self.parent = parent
43 | self.name = name
44 | }
45 | }
46 | }
47 |
48 | extension WordPressElements.Category: Equatable {
49 | /// Checks if two ``Category`` instances are equal.
50 | public static func == (
51 | lhs: WordPressElements.Category,
52 | rhs: WordPressElements.Category
53 | ) -> Bool {
54 | lhs.termID == rhs.termID
55 | && lhs.niceName == rhs.niceName
56 | && lhs.parent == rhs.parent
57 | && lhs.name == rhs.name
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Wordpress/WPPostMeta.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A typealias for `WordPressElements.Category`.
4 | public typealias WPPostMeta = WordPressElements.Category
5 |
6 | // swiftlint:disable nesting
7 | extension WordPressElements {
8 | /// A struct representing metadata for a WordPress post.
9 | public struct PostMeta: Codable {
10 | /// The coding keys for encoding and decoding.
11 | internal enum CodingKeys: String, CodingKey {
12 | case key = "wp:metaKey"
13 | case value = "wp:metaValue"
14 | }
15 |
16 | /// The key of the metadata.
17 | public let key: CData
18 |
19 | /// The value of the metadata.
20 | public let value: CData
21 |
22 | /// A struct representing an Atom category.
23 | /// Initializes a new ``PostMeta`` instance.
24 | ///
25 | /// - Parameters:
26 | /// - key: The key of the metadata.
27 | /// - value: The value of the metadata.
28 | /// - SeeAlso: ``EntryCategory``
29 | public init(key: String, value: String) {
30 | self.key = .init(stringLiteral: key)
31 | self.value = .init(stringLiteral: value)
32 | }
33 | }
34 | }
35 |
36 | extension WordPressElements.PostMeta: Equatable {
37 | /// A struct representing an Atom category.
38 | /// Checks if two ``PostMeta`` instances are equal.
39 | ///
40 | /// - Parameters:
41 | /// - lhs: The left-hand side ``PostMeta`` instance.
42 | /// - rhs: The right-hand side ``PostMeta`` instance.
43 | ///
44 | /// - Returns: ``true`` if the two instances are equal, ``false`` otherwise.
45 | /// - SeeAlso: ``EntryCategory``
46 | public static func == (
47 | lhs: WordPressElements.PostMeta,
48 | rhs: WordPressElements.PostMeta
49 | ) -> Bool {
50 | lhs.key == rhs.key
51 | && lhs.value == rhs.value
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Wordpress/WPTag.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A typealias for `WordPressElements.Tag`
4 | public typealias WPTag = WordPressElements.Tag
5 |
6 | // swiftlint:disable nesting
7 | extension WordPressElements {
8 | /// A struct representing a tag in WordPress.
9 | public struct Tag: Codable {
10 | /// The term ID of the tag.
11 | internal enum CodingKeys: String, CodingKey {
12 | case termID = "wp:termId"
13 | case slug = "wp:tagSlug"
14 | case name = "wp:tagName"
15 | }
16 |
17 | /// The term ID of the tag.
18 | public let termID: Int
19 |
20 | /// The slug of the tag.
21 | public let slug: CData
22 |
23 | /// The name of the tag.
24 | public let name: CData
25 |
26 | /// A struct representing an Atom category.
27 | /// Initializes a new ``Tag`` instance.
28 | ///
29 | /// - Parameters:
30 | /// - termID: The term ID of the tag.
31 | /// - slug: The slug of the tag.
32 | /// - name: The name of the tag.
33 | /// - SeeAlso: ``EntryCategory``
34 | public init(termID: Int, slug: CData, name: CData) {
35 | self.termID = termID
36 | self.slug = slug
37 | self.name = name
38 | }
39 | }
40 | }
41 |
42 | extension WordPressElements.Tag: Equatable {
43 | /// Checks if two ``Tag`` instances are equal.
44 | public static func == (lhs: WordPressElements.Tag, rhs: WordPressElements.Tag) -> Bool {
45 | lhs.termID == rhs.termID
46 | && lhs.slug == rhs.slug
47 | && lhs.name == rhs.name
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost+RSSItem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension WordPressPost {
4 | // swiftlint:disable cyclomatic_complexity function_body_length
5 | /// A struct representing an Atom category.
6 | /// Initializes a ``WordPressPost`` instance from an ``RSSItem``.
7 | ///
8 | /// - Parameter item: The ``RSSItem`` to initialize from.
9 | ///
10 | /// - Throws: `WordPressError.missingField` if any required field is missing.
11 | ///
12 | /// - Note: This initializer is marked as ``public`` to allow external usage.
13 | ///
14 | /// - SeeAlso: ``EntryCategory``
15 | public init(item: RSSItem) throws {
16 | guard let name = item.wpPostName else {
17 | throw WordPressError.missingField(.name)
18 | }
19 | guard let type = item.wpPostType else {
20 | throw WordPressError.missingField(.type)
21 | }
22 | guard let creator = item.creators.first else {
23 | throw WordPressError.missingField(.creator)
24 | }
25 | guard let body = item.contentEncoded else {
26 | throw WordPressError.missingField(.body)
27 | }
28 | guard let status = item.wpStatus else {
29 | throw WordPressError.missingField(.status)
30 | }
31 | guard let commentStatus = item.wpCommentStatus else {
32 | throw WordPressError.missingField(.commentStatus)
33 | }
34 | guard let pingStatus = item.wpPingStatus else {
35 | throw WordPressError.missingField(.pingStatus)
36 | }
37 | guard let parentID = item.wpPostParent else {
38 | throw WordPressError.missingField(.parentID)
39 | }
40 | guard let menuOrder = item.wpMenuOrder else {
41 | throw WordPressError.missingField(.menuOrder)
42 | }
43 | guard let id = item.wpPostID else {
44 | throw WordPressError.missingField(.id)
45 | }
46 | guard let isSticky = item.wpIsSticky else {
47 | throw WordPressError.missingField(.isSticky)
48 | }
49 | guard let postDate = item.wpPostDate else {
50 | throw WordPressError.missingField(.postDate)
51 | }
52 | guard let modifiedDate = item.wpModifiedDate else {
53 | throw WordPressError.missingField(.modifiedDate)
54 | }
55 | guard let link = item.link else {
56 | throw WordPressError.missingField(.link)
57 | }
58 |
59 | let title = item.title
60 | let categoryTerms = item.categoryTerms
61 | let meta = item.wpPostMeta
62 | let pubDate = item.pubDate
63 |
64 | let categoryDictionary = Dictionary(
65 | grouping: categoryTerms) {
66 | $0.domain
67 | }
68 |
69 | modifiedDateGMT = item.wpModifiedDateGMT
70 | self.name = name.value
71 | self.title = title
72 | self.type = type.value
73 | self.link = link
74 | self.pubDate = pubDate
75 | self.creator = creator
76 | self.body = body.value
77 | tags = categoryDictionary["post_tag", default: []].map { $0.value }
78 | categories = categoryDictionary["category", default: []].map { $0.value }
79 | self.meta = Dictionary(grouping: meta) { $0.key.value }
80 | .compactMapValues { $0.last?.value.value }
81 | self.status = status.value
82 | self.commentStatus = commentStatus.value
83 | self.pingStatus = pingStatus.value
84 | self.parentID = parentID
85 | self.menuOrder = menuOrder
86 | self.id = id
87 | self.isSticky = (isSticky != 0)
88 | self.postDate = postDate
89 | postDateGMT = item.wpPostDateGMT
90 | self.modifiedDate = modifiedDate
91 | attachmentURL = item.wpAttachmentURL
92 | }
93 |
94 | // swiftlint:enable cyclomatic_complexity function_body_length
95 | }
96 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/Wordpress/WordPressPost.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A namespace for WordPress related elements.
4 | public enum WordPressElements {}
5 |
6 | /// An error type representing a missing field in a WordPress post.
7 | public enum WordPressError: Error, Equatable {
8 | case missingField(WordPressPost.Field)
9 | }
10 |
11 | /// A struct representing a WordPress post.
12 | public struct WordPressPost {
13 | /// The type of the post.
14 | public typealias PostType = String
15 |
16 | /// The comment status of the post.
17 | public typealias CommentStatus = String
18 |
19 | /// The ping status of the post.
20 | public typealias PingStatus = String
21 |
22 | /// The status of the post.
23 | public typealias Status = String
24 |
25 | /// An enum representing the fields of a WordPress post.
26 | public enum Field: Equatable {
27 | case name
28 | case title
29 | case type
30 | case link
31 | case pubDate
32 | case creator
33 | case body
34 | case tags
35 | case categories
36 | case meta
37 | case status
38 | case commentStatus
39 | case pingStatus
40 | case parentID
41 | case menuOrder
42 | case id
43 | case isSticky
44 | case postDate
45 | case postDateGMT
46 | case modifiedDate
47 | case modifiedDateGMT
48 | }
49 |
50 | /// The name of the post.
51 | public let name: String
52 |
53 | /// The title of the post.
54 | public let title: String
55 |
56 | /// The type of the post.
57 | public let type: PostType
58 |
59 | /// The link of the post.
60 | public let link: URL
61 |
62 | /// The publication date of the post.
63 | public let pubDate: Date?
64 |
65 | /// The creator of the post.
66 | public let creator: String
67 |
68 | /// The body of the post.
69 | public let body: String
70 |
71 | /// The tags of the post.
72 | public let tags: [String]
73 |
74 | /// The categories of the post.
75 | public let categories: [String]
76 |
77 | /// The meta data of the post.
78 | public let meta: [String: String]
79 |
80 | /// The status of the post.
81 | public let status: Status
82 |
83 | /// The comment status of the post.
84 | public let commentStatus: CommentStatus
85 |
86 | /// The ping status of the post.
87 | public let pingStatus: PingStatus
88 |
89 | /// The parent ID of the post.
90 | public let parentID: Int?
91 |
92 | /// The menu order of the post.
93 | public let menuOrder: Int?
94 |
95 | /// The ID of the post.
96 | public let id: Int
97 |
98 | /// A boolean indicating if the post is sticky.
99 | public let isSticky: Bool
100 |
101 | /// The post date of the post.
102 | public let postDate: Date
103 |
104 | /// The post date in GMT of the post.
105 | public let postDateGMT: Date?
106 |
107 | /// The modified date of the post.
108 | public let modifiedDate: Date
109 |
110 | /// The modified date in GMT of the post.
111 | public let modifiedDateGMT: Date?
112 |
113 | /// The attachment URL of the post.
114 | public let attachmentURL: URL?
115 | }
116 |
117 | extension WordPressPost: Hashable {
118 | public static func == (lhs: WordPressPost, rhs: WordPressPost) -> Bool {
119 | lhs.id == rhs.id
120 | }
121 |
122 | public func hash(into hasher: inout Hasher) {
123 | hasher.combine(id)
124 | }
125 | }
126 |
127 | extension Entryable {
128 | /// Returns a WordPress post if the entry is an RSS item.
129 | public var wpPost: WordPressPost? {
130 | guard let rssItem = self as? RSSItem else {
131 | return nil
132 | }
133 |
134 | return try? WordPressPost(item: rssItem)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/YouTube/YouTubeID.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing the properties of a YouTube ID.
3 | ///
4 | /// - Note: This struct conforms to the ``YouTubeID`` protocol.
5 | ///
6 | /// - SeeAlso: ``YouTubeID``
7 | ///
8 | /// - Important: This struct is internal.
9 | ///
10 | /// - Parameters:
11 | /// - videoID: The YouTube video ID.
12 | /// - channelID: The YouTube channel ID.
13 | /// - SeeAlso: ``EntryCategory``
14 | internal struct YouTubeIDProperties: YouTubeID {
15 | internal let videoID: String
16 | internal let channelID: String
17 |
18 | /// A struct representing an Atom category.
19 | /// Initializes a ``YouTubeIDProperties`` instance with the given AtomEntry.
20 | ///
21 | /// - Parameters:
22 | /// - entry: The AtomEntry containing the YouTube ID properties.
23 | ///
24 | /// - Returns: A new ``YouTubeIDProperties`` instance,
25 | /// or ``nil`` if the required properties are missing.
26 | /// - SeeAlso: ``EntryCategory``
27 | internal init?(entry: AtomEntry) {
28 | guard
29 | let channelID = entry.youtubeChannelID,
30 | let videoID = entry.youtubeVideoID else {
31 | return nil
32 | }
33 | self.channelID = channelID
34 | self.videoID = videoID
35 | }
36 | }
37 |
38 | /// A struct representing an Atom category.
39 | /// A protocol abstracting the ID properties of a YouTube RSS Feed.
40 | ///
41 | /// - Note: This protocol is public.
42 | ///
43 | /// - SeeAlso: ``YouTubeIDProperties``
44 | ///
45 | /// - Important: This protocol is specific to YouTube.
46 | ///
47 | /// - Requires: Conforming types must provide a ``videoID`` and a ``channelID``.
48 | /// - SeeAlso: ``EntryCategory``
49 | public protocol YouTubeID {
50 | /// The YouTube video ID.
51 | var videoID: String { get }
52 |
53 | /// The YouTube channel ID.
54 | var channelID: String { get }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/iTunes/iTunesDuration.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A struct representing the duration of an iTunes track.
4 | public struct iTunesDuration: Codable, ExpressibleByFloatLiteral {
5 | /// The type used to represent a floating-point literal.
6 | public typealias FloatLiteralType = TimeInterval
7 |
8 | /// The value of the duration in seconds.
9 | public let value: TimeInterval
10 |
11 | /// Creates a new instance with the specified floating-point literal value.
12 | ///
13 | /// - Parameter value: The value of the duration in seconds.
14 | public init(floatLiteral value: TimeInterval) {
15 | self.value = value
16 | }
17 |
18 | /// Creates a new instance by decoding from the given decoder.
19 | ///
20 | /// - Parameter decoder: The decoder to read data from.
21 | /// - Throws: An error if reading from the decoder fails,
22 | /// or if the data is corrupted or invalid.
23 | public init(from decoder: Decoder) throws {
24 | let container = try decoder.singleValueContainer()
25 | let stringValue = try container.decode(String.self)
26 | guard let value = Self.timeInterval(stringValue) else {
27 | let context = DecodingError.Context(
28 | codingPath: decoder.codingPath,
29 | debugDescription: "Invalid time value",
30 | underlyingError: nil
31 | )
32 | throw DecodingError.dataCorrupted(context)
33 | }
34 | self.value = value
35 | }
36 |
37 | /// Creates a new instance from the given description string.
38 | ///
39 | /// - Parameter description: The description string representing the duration.
40 | /// - Returns: A new instance if the description is valid, otherwise ``nil``.
41 | public init?(_ description: String) {
42 | guard let value = Self.timeInterval(description) else {
43 | return nil
44 | }
45 | self.value = value
46 | }
47 |
48 | /// Converts a time string to a ``TimeInterval`` value.
49 | ///
50 | /// - Parameter timeString: The time string to convert.
51 | /// - Returns: The ``TimeInterval`` value representing the time string,
52 | /// or ``nil`` if the string is invalid.
53 | internal static func timeInterval(_ timeString: String) -> TimeInterval? {
54 | let timeStrings = timeString.components(separatedBy: ":").prefix(3)
55 | let doubles = timeStrings.compactMap(Double.init)
56 | guard doubles.count == timeStrings.count else {
57 | return nil
58 | }
59 | return doubles.reduce(0) { partialResult, value in
60 | partialResult * 60.0 + value
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/iTunes/iTunesEpisode.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A type alias for an iTunes episode.
3 | ///
4 | /// - Note: This type is an alias for ``XMLStringInt``.
5 | ///
6 | /// - SeeAlso: ``XMLStringInt``
7 | /// - SeeAlso: ``EntryCategory``
8 | public typealias iTunesEpisode = XMLStringInt
9 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/iTunes/iTunesImage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A type alias for iTunes image links.
4 | public typealias iTunesImage = Link
5 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/Media/iTunes/iTunesOwner.swift:
--------------------------------------------------------------------------------
1 | /// A struct representing an Atom category.
2 | /// A struct representing the owner of an iTunes account.
3 | ///
4 | /// - Note: This struct conforms to the ``Codable`` protocol.
5 | ///
6 | /// - Warning: Do not modify the ``CodingKeys`` enum.
7 | ///
8 | /// - SeeAlso: ``CodingKeys``
9 | ///
10 | /// - Remark: The ``email`` property is optional.
11 | ///
12 | /// - Important: The ``name`` property is required.
13 | ///
14 | /// - Version: 1.0
15 | /// - SeeAlso: ``EntryCategory``
16 | public struct iTunesOwner: Codable {
17 | /// The coding keys used to encode and decode the struct.
18 | internal enum CodingKeys: String, CodingKey {
19 | case name = "itunes:name"
20 | case email = "itunes:email"
21 | }
22 |
23 | /// The name of the iTunes owner.
24 | public let name: String
25 |
26 | /// The email address of the iTunes owner.
27 | public let email: String?
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/OPML/OPML+Body.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension OPML {
4 | public struct Body: Codable, Equatable {
5 | // swiftlint:disable:next nesting
6 | internal enum CodingKeys: String, CodingKey {
7 | case outlines = "outline"
8 | }
9 |
10 | public let outlines: [Outline]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/OPML/OPML+Head.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension OPML {
4 | public struct Head: Codable, Equatable {
5 | public let title: String?
6 | public let dateCreated: String?
7 | public let dateModified: String?
8 | public let ownerName: String?
9 | public let ownerEmail: String?
10 | public let ownerId: String?
11 | public let docs: String?
12 | public let expansionStates: ListString?
13 | public let vertScrollState: Int?
14 | public let windowTop: Int?
15 | public let windowLeft: Int?
16 | public let windowBottom: Int?
17 | public let windowRight: Int?
18 |
19 | // swiftlint:disable:next nesting
20 | internal enum CodingKeys: String, CodingKey {
21 | case title
22 | case dateCreated
23 | case dateModified
24 | case ownerName
25 | case ownerEmail
26 | case ownerId
27 | case docs
28 | case expansionStates = "expansionState"
29 | case vertScrollState
30 | case windowTop
31 | case windowLeft
32 | case windowBottom
33 | case windowRight
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/OPML/OPML+Outline.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // swiftlint:disable nesting discouraged_optional_boolean
4 |
5 | extension OPML {
6 | public struct Outline: Codable, Equatable {
7 | public enum CodingKeys: String, CodingKey {
8 | case text
9 | case title
10 | case description
11 | case type
12 | case url
13 | case htmlUrl
14 | case xmlUrl
15 | case language
16 | case created
17 | case categories = "category"
18 | case isComment
19 | case isBreakpoint
20 | case version
21 |
22 | case outlines = "outline"
23 | }
24 |
25 | public let text: String
26 | public let title: String?
27 | public let description: String?
28 | public let type: OutlineType?
29 | public let url: URL?
30 | public let htmlUrl: URL?
31 | public let xmlUrl: URL?
32 | public let language: String?
33 | public let created: String?
34 | public let categories: ListString?
35 | public let isComment: Bool?
36 | public let isBreakpoint: Bool?
37 | public let version: String?
38 |
39 | public let outlines: [Outline]?
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/OPML/OPML.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct OPML: Codable, Equatable {
4 | internal enum CodingKeys: String, CodingKey {
5 | case version
6 | case head
7 | case body
8 | }
9 |
10 | public let version: String
11 |
12 | public let head: Head
13 | public let body: Body
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/OPML/OutlineType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum OutlineType: String, Codable {
4 | case rss
5 | case link
6 | case include
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/SyndicationUpdate/SyndicationUpdate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // swiftlint:disable line_length
4 | /// Properties concerning how often it is updated a feed is updated.
5 | ///
6 | /// These properties come from
7 | /// [the RDF Site Summary Syndication Module](https://web.resource.org/rss/1.0/modules/syndication/).
8 | public struct SyndicationUpdate: Codable, Equatable {
9 | // swiftlint:enable line_length
10 |
11 | /// Describes the period over which the channel format is updated.
12 | /// The default value is ``SyndicationUpdatePeriod/daily``.
13 | public let period: SyndicationUpdatePeriod
14 |
15 | /// Used to describe the frequency of updates in relation to the update period.
16 | /// The default value is 1.
17 | ///
18 | /// A positive integer indicates how many times in that period the channel is updated.
19 | /// For example, an updatePeriod of daily, and an updateFrequency of 2
20 | /// indicates the channel format is updated twice daily.
21 | public let frequency: Int
22 |
23 | /// Defines a base date
24 | /// to be used in concert with
25 | /// ``SyndicationUpdate/period`` and ``SyndicationUpdate/frequency``
26 | /// to calculate the publishing schedule.
27 | public let base: Date?
28 |
29 | internal init?(
30 | period: SyndicationUpdatePeriod? = nil,
31 | frequency: Int? = nil,
32 | base: Date? = nil
33 | ) {
34 | guard period != nil || frequency != nil else {
35 | return nil
36 | }
37 | self.period = period ?? .daily
38 | self.frequency = frequency ?? 1
39 | self.base = base
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/SyndicationUpdate/SyndicationUpdateFrequency.swift:
--------------------------------------------------------------------------------
1 | /// Used to describe the frequency of updates
2 | /// in relation to the update period.
3 | /// A positive integer indicates
4 | /// how many times in that period the channel is updated.
5 | public typealias SyndicationUpdateFrequency = XMLStringInt
6 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Formats/SyndicationUpdate/SyndicationUpdatePeriod.swift:
--------------------------------------------------------------------------------
1 | /// Describes the period over which the channel format is updated.
2 | public enum SyndicationUpdatePeriod: String, Codable {
3 | case hourly, daily, weekly, monthly, yearly
4 |
5 | public init(from decoder: Decoder) throws {
6 | let container = try decoder.singleValueContainer()
7 | let stringValue = try container
8 | .decode(String.self)
9 | .trimmingCharacters(in: .whitespacesAndNewlines)
10 | guard let value = Self(rawValue: stringValue) else {
11 | let context = DecodingError.Context(
12 | codingPath: decoder.codingPath,
13 | debugDescription: "Invalid Enum",
14 | underlyingError: nil
15 | )
16 | throw DecodingError.dataCorrupted(context)
17 | }
18 | self = value
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/KeyedDecodingContainerProtocol.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension KeyedDecodingContainerProtocol {
4 | internal func decodeDateIfPresentAndValid(forKey key: Key) throws -> Date? {
5 | if let pubDateString =
6 | try decodeIfPresent(String.self, forKey: key),
7 | !pubDateString.isEmpty {
8 | return DateFormatterDecoder.RSS.decoder.decodeString(pubDateString)
9 | }
10 | return nil
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/Substring.SubSequence.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Substring.SubSequence {
4 | internal func asDouble() -> Double? {
5 | Double(self)
6 | }
7 |
8 | internal func asInt() -> Int? {
9 | guard let double = Double(self) else {
10 | return nil
11 | }
12 |
13 | return Int(double)
14 | }
15 |
16 | internal func asExactInt() -> Int? {
17 | guard let double = Double(self) else {
18 | return nil
19 | }
20 |
21 | return Int(exactly: double)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/SyndiKit.docc/Resources/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/Sources/SyndiKit/SyndiKit.docc/Resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brightdigit/SyndiKit/468f15671688bcede7db40b51cbdb78df949e7e1/Sources/SyndiKit/SyndiKit.docc/Resources/logo.png
--------------------------------------------------------------------------------
/Sources/SyndiKit/URL.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension URL {
4 | internal init?(strict string: String) {
5 | guard string.starts(with: "http") else {
6 | return nil
7 | }
8 |
9 | self.init(string: string)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/BlogTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 | import XMLCoder
4 |
5 | public final class BlogTests: XCTestCase {
6 | func testBlogs() throws {
7 | let blogs = Content.blogs
8 | let sites = SiteCollectionDirectory(blogs: blogs)
9 |
10 | for languageContent in blogs {
11 | for category in languageContent.categories {
12 | let expectedCount = sites.sites(
13 | withLanguage: languageContent.language,
14 | withCategory: category.slug
15 | )
16 | .count
17 | XCTAssertEqual(
18 | category.sites.count,
19 | expectedCount,
20 | "mismatch count for \(languageContent.language):\(category.slug)"
21 | )
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Content.Directories.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import SyndiKit
3 |
4 | extension Content {
5 | internal enum Directories {
6 | static let data = URL(fileURLWithPath: #file)
7 | .deletingLastPathComponent()
8 | .deletingLastPathComponent()
9 | .deletingLastPathComponent()
10 | .appendingPathComponent("Data")
11 | static let XML = data.appendingPathComponent("XML")
12 | static let JSON = data.appendingPathComponent("JSON")
13 | static let OPML = data.appendingPathComponent("OPML")
14 | static let WordPress = data.appendingPathComponent("WordPress")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Content.ResultDictionary.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import SyndiKit
3 | import XMLCoder
4 |
5 | enum Content {
6 | typealias ResultDictionary = [String: Result]
7 |
8 | fileprivate static func resultDictionaryFrom(
9 | directoryURL: URL,
10 | by closure: @escaping (Data) throws -> SuccessValueType
11 | ) throws -> ResultDictionary {
12 | let xmlDataSet = Result {
13 | try FileManager.default.dataFromDirectory(at: directoryURL)
14 | }
15 |
16 | return try xmlDataSet.map { xmlDataSet in
17 | xmlDataSet.flatResultMapValue(closure)
18 | }.map(Dictionary.init(uniqueKeysWithValues:)).get()
19 | }
20 |
21 | static let synDecoder = SynDecoder()
22 | static let xmlDecoder = XMLDecoder()
23 |
24 | static let xmlFeeds = try! Content.resultDictionaryFrom(
25 | directoryURL: Directories.XML,
26 | by: Self.synDecoder.decode(_:)
27 | )
28 | static let jsonFeeds = try! Content.resultDictionaryFrom(
29 | directoryURL: Directories.JSON,
30 | by: Self.synDecoder.decode(_:)
31 | )
32 | static let opml = try! Content.resultDictionaryFrom(
33 | directoryURL: Directories.OPML,
34 | by: Self.xmlDecoder.decodeOPML(_:)
35 | )
36 | static let wordpressDataSet = try! FileManager.default.dataFromDirectory(
37 | at: Directories.WordPress
38 | )
39 | static let blogs: SiteCollection = try! .init(contentsOf: Directories.data.appendingPathComponent("blogs.json"))
40 | // swiftlint:enable force_try line_length
41 | }
42 |
43 | extension XMLDecoder {
44 | func decodeOPML(_ data: Data) throws -> OPML {
45 | try decode(OPML.self, from: data)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/DecodingErrorTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import SyndiKit
3 | import XCTest
4 |
5 | public final class DecodingErrorTests: XCTestCase {
6 | func testErrorsEmpty() {
7 | let error = DecodingError.failedAttempts([:])
8 |
9 | guard case let DecodingError.dataCorrupted(context) = error else {
10 | XCTFail()
11 | return
12 | }
13 |
14 | XCTAssertNil(context.underlyingError)
15 | }
16 |
17 | func testErrorsOne() {
18 | let debugDescription = UUID().uuidString
19 | let error = DecodingError.failedAttempts([
20 | "Test": .dataCorrupted(.init(codingPath: [], debugDescription: debugDescription))
21 | ])
22 |
23 | guard case let DecodingError.dataCorrupted(parentContext) = error else {
24 | XCTFail()
25 | return
26 | }
27 |
28 | guard let decodingError = parentContext.underlyingError as? DecodingError else {
29 | XCTFail()
30 | return
31 | }
32 |
33 | guard case let DecodingError.dataCorrupted(childContext) = decodingError else {
34 | XCTFail()
35 | return
36 | }
37 |
38 | XCTAssertEqual(childContext.debugDescription, debugDescription)
39 | }
40 |
41 | func testErrorsMany() {
42 | let errors = [
43 | "Test1": DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "")),
44 | "Test2": DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: ""))
45 | ]
46 | let error = DecodingError.failedAttempts(errors)
47 |
48 | guard case let DecodingError.dataCorrupted(context) = error else {
49 | XCTFail()
50 | return
51 | }
52 |
53 | guard let collection = context.underlyingError as? DecodingError.Dictionary else {
54 | XCTFail()
55 | return
56 | }
57 |
58 | XCTAssertEqual(collection.errors.count, errors.count)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/FileManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension FileManager {
4 | internal func dataFromDirectory(at sourceURL: URL) throws -> [(String, Result)] {
5 | let urls = try contentsOfDirectory(
6 | at: sourceURL,
7 | includingPropertiesForKeys: nil,
8 | options: []
9 | )
10 |
11 | return urls.mapPairResult { try Data(contentsOf: $0) }
12 | .map { ($0.0.deletingPathExtension().lastPathComponent, $0.1) }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/JSONFeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SyndiKit
3 |
4 | extension JSONFeed {
5 | internal var homePageURLHttp: URL? {
6 | var components = URLComponents(url: homePageUrl, resolvingAgainstBaseURL: false)
7 | components?.scheme = "http"
8 | return components?.url
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/Sequence.swift:
--------------------------------------------------------------------------------
1 | extension Sequence {
2 | internal func mapPairResult(
3 | _ transform: @escaping (Element) throws -> Success
4 | ) -> [(Element, Result)] {
5 | map { element in
6 | (element, Result { try transform(element) })
7 | }
8 | }
9 |
10 | internal func mapResult(
11 | _ transform: @escaping (Element) throws -> Success
12 | ) -> [Result] {
13 | map { element in
14 | Result { try transform(element) }
15 | }
16 | }
17 |
18 | internal func flatResultMapValue(
19 | _ transform: @escaping (SuccessValue) throws -> NewSuccess
20 | ) -> [(SuccessKey, Result)]
21 | where Element == (SuccessKey, Result) {
22 | map {
23 | let value = $0.1.flatMap { value in
24 | Result { try transform(value) }
25 | }
26 | return ($0.0, value)
27 | }
28 | }
29 |
30 | internal func flatResultMap(
31 | _ transform: @escaping (Success) throws -> NewSuccess
32 | ) -> [Result]
33 | where Element == Result {
34 | map {
35 | $0.flatMap { success in
36 | Result {
37 | try transform(success)
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/SiteCollection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import SyndiKit
3 |
4 | extension SiteCollection {
5 | internal init(contentsOf url: URL, using decoder: JSONDecoder = .init()) throws {
6 | let data = try Data(contentsOf: url)
7 | self = try decoder.decode(SiteCollection.self, from: data)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/String.swift:
--------------------------------------------------------------------------------
1 | extension String {
2 | internal func trimAndNilIfEmpty() -> String? {
3 | let text = trimmingCharacters(in: .whitespacesAndNewlines)
4 | return text.isEmpty ? nil : text
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/Extensions/URL.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension URL {
4 | var remainingPath: String {
5 | let path = self.path
6 |
7 | return path == "/" ? "" : path
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/OPMLTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 |
4 | internal final class OPMLTests: XCTestCase {
5 | internal func testSubscriptionList() throws {
6 | let opml = try Content.opml["subscriptionList"]?.get()
7 |
8 | XCTAssertEqual(opml?.head.title, "mySubscriptions.opml")
9 | XCTAssertEqual(opml?.head.ownerEmail, "dave@scripting.com")
10 | XCTAssertEqual(opml?.body.outlines.count, 13)
11 |
12 | let firstOutline = opml?.body.outlines.first
13 |
14 | XCTAssertEqual(firstOutline?.text, "CNET News.com")
15 | 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.")
16 | XCTAssertEqual(firstOutline?.htmlUrl, URL(string: "http://news.com.com/")!)
17 | XCTAssertEqual(firstOutline?.language, "unknown")
18 | XCTAssertEqual(firstOutline?.title, "CNET News.com")
19 | XCTAssertEqual(firstOutline?.type, .rss)
20 | XCTAssertEqual(firstOutline?.version, "RSS2")
21 | XCTAssertEqual(firstOutline?.xmlUrl, URL(string: "http://news.com.com/2547-1_3-0-5.xml")!)
22 | }
23 |
24 | internal func testStates() throws {
25 | let opml = try Content.opml["states"]?.get()
26 |
27 | XCTAssertEqual(opml?.head.title, "states.opml")
28 | XCTAssertEqual(opml?.head.ownerEmail, "dave@scripting.com")
29 | XCTAssertEqual(opml?.body.outlines.count, 1)
30 |
31 | let usOutline = opml?.body.outlines.first
32 |
33 | XCTAssertEqual(usOutline?.text, "United States")
34 |
35 | XCTAssertEqual(usOutline?.outlines?.count, 8)
36 |
37 | let farWestOutline = usOutline?.outlines?.first
38 |
39 | XCTAssertEqual(farWestOutline?.text, "Far West")
40 | XCTAssertEqual(farWestOutline?.outlines?.count, 6)
41 |
42 | let nevadaOutline = farWestOutline?.outlines?[3]
43 | XCTAssertEqual(nevadaOutline?.outlines?.count, 4)
44 | }
45 |
46 | internal func testCategory() throws {
47 | let opml = try Content.opml["category"]?.get()
48 |
49 | XCTAssertEqual(opml?.head.title, "Illustrating the category attribute")
50 | XCTAssertEqual(opml?.body.outlines.count, 1)
51 |
52 | let outline = opml?.body.outlines.first
53 |
54 | XCTAssertEqual(outline?.text, "The Mets are the best team in baseball.")
55 | XCTAssertEqual(outline?.categories?.values.count, 2)
56 | XCTAssertEqual(outline?.categories?.values[0], "/Philosophy/Baseball/Mets")
57 | XCTAssertEqual(outline?.categories?.values[1], "/Tourism/New York")
58 | }
59 |
60 | internal func testPlacesLived() throws {
61 | let opml = try Content.opml["placesLived"]?.get()
62 |
63 | XCTAssertEqual(opml?.head.title, "placesLived.opml")
64 | XCTAssertEqual(opml?.head.ownerId, "http://www.opml.org/profiles/sendMail?usernum=1")
65 | XCTAssertEqual(opml?.head.expansionStates?.values.count, 6)
66 | XCTAssertEqual(opml?.head.expansionStates?.values[0], 1)
67 | XCTAssertEqual(opml?.head.expansionStates?.values[3], 10)
68 | }
69 |
70 | internal func testSimpleScript() throws {
71 | let opml = try Content.opml["simpleScript"]?.get()
72 |
73 | XCTAssertEqual(opml?.head.title, "workspace.userlandsamples.doSomeUpstreaming")
74 | XCTAssertEqual(opml?.head.expansionStates?.values.count, 3)
75 | XCTAssertEqual(opml?.head.expansionStates?.values[0], 1)
76 | XCTAssertEqual(opml?.head.expansionStates?.values[2], 4)
77 |
78 | XCTAssertEqual(opml?.body.outlines.count, 4)
79 |
80 | let isCommentOutline = opml?.body.outlines.first
81 | XCTAssertEqual(isCommentOutline?.text, "Changes")
82 | XCTAssertEqual(isCommentOutline?.isComment, true)
83 |
84 | let isBreakpointOutline = opml?.body.outlines[1].outlines?.first
85 | XCTAssertEqual(isBreakpointOutline?.text, "file.surefilepath (f)")
86 | XCTAssertEqual(isBreakpointOutline?.isBreakpoint, true)
87 | }
88 |
89 | internal func testInvalidExpansionStateType() throws {
90 | XCTAssertThrowsError(try Content.opml["category_invalidExpansionState"]?.get()) { error in
91 | guard case let .typeMismatch(type, context) = error as? DecodingError else {
92 | XCTFail("Expected typeMismatch error.")
93 | return
94 | }
95 |
96 | XCTAssertTrue(type is Int.Type)
97 | XCTAssertEqual(
98 | context.debugDescription,
99 | "Invalid value: one"
100 | )
101 | }
102 | }
103 |
104 | internal func testType() throws {
105 | var opml = try Content.opml["subscriptionList"]?.get()
106 |
107 | XCTAssertEqual(opml?.body.outlines.first?.type, .rss)
108 | XCTAssertNotNil(opml?.body.outlines.first?.text)
109 | XCTAssertNotNil(opml?.body.outlines.first?.xmlUrl)
110 |
111 | opml = try Content.opml["directory"]?.get()
112 |
113 | XCTAssertEqual(opml?.body.outlines.first?.type, .link)
114 | XCTAssertNotNil(opml?.body.outlines.first?.url)
115 |
116 | opml = try Content.opml["placesLived"]?.get()
117 | let floridaOutline = opml?.body.outlines.first?
118 | .outlines?.first(
119 | where: { $0.text == "Florida" }
120 | )
121 |
122 | XCTAssertEqual(floridaOutline?.type, .include)
123 | XCTAssertNotNil(floridaOutline?.url)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/RSSCoded.Durations.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SyndiKitTests {
4 | internal static let durationSets: [String: [TimeInterval]] = [
5 | "empowerapps-show": [
6 | 2_746,
7 | 3_500,
8 | 5_145,
9 | 2_589,
10 | 1_796,
11 | 2_401,
12 | 2_052,
13 | 2_323,
14 | 2_631,
15 | 1_971,
16 | 1_877,
17 | 2_080,
18 | 2_240,
19 | 2_656,
20 | 1_843,
21 | 2_237,
22 | 3_421,
23 | 2_788,
24 | 3_041,
25 | 2_138,
26 | 1_460,
27 | 2_768,
28 | 2_372,
29 | 2_309,
30 | 1_804,
31 | 1_916,
32 | 3_022,
33 | 2_541,
34 | 2_729,
35 | 3_386,
36 | 2_281,
37 | 2_962,
38 | 3_307,
39 | 2_648,
40 | 2_667,
41 | 2_783,
42 | 2_693,
43 | 2_290,
44 | 1_741,
45 | 3_341,
46 | 1_607,
47 | 889,
48 | 2_535,
49 | 1_762,
50 | 2_799,
51 | 2_956,
52 | 2_976,
53 | 5_011,
54 | 2_140,
55 | 2_535,
56 | 2_497,
57 | 3_405,
58 | 1_210,
59 | 2_405,
60 | 2_991,
61 | 3_540,
62 | 1_868,
63 | 2_248,
64 | 4_199,
65 | 1_289,
66 | 3_155,
67 | 2_787,
68 | 2_222,
69 | 2_555,
70 | 479,
71 | 2_607,
72 | 1_985,
73 | 2_565,
74 | 2_761,
75 | 2_026,
76 | 2_452,
77 | 3_163,
78 | 1_127,
79 | 3_195,
80 | 3_890,
81 | 1_358,
82 | 2_489,
83 | 2_465,
84 | 2_083,
85 | 2_824,
86 | 2_137,
87 | 2_452,
88 | 2_242,
89 | 1_622,
90 | 1_081,
91 | 1_979,
92 | 2_080,
93 | 1_225,
94 | 2_204,
95 | 1_703,
96 | 2_495,
97 | 922,
98 | 1_433,
99 | 1_776
100 | ],
101 | "raywenderlich": [
102 | 96.0,
103 | 2_893.0,
104 | 2_735.0,
105 | 2_653.0,
106 | 2_497.0,
107 | 2_592.0,
108 | 2_542.0,
109 | 2_577.0,
110 | 2_455.0,
111 | 1_935.0,
112 | 2_689.0,
113 | 2_819.0,
114 | 2_871.0,
115 | 653.0,
116 | 2_454.0,
117 | 2_696.0,
118 | 2_849.0,
119 | 2_778.0,
120 | 2_742.0,
121 | 2_705.0,
122 | 2_750.0,
123 | 2_734.0,
124 | 2_684.0,
125 | 2_676.0,
126 | 2_698.0,
127 | 2_696.0,
128 | 2_698.0,
129 | 600.0,
130 | 2_549.0,
131 | 2_398.0,
132 | 2_344.0,
133 | 2_401.0,
134 | 2_419.0,
135 | 2_391.0,
136 | 3_576.0,
137 | 2_402.0,
138 | 2_422.0,
139 | 2_385.0,
140 | 2_374.0,
141 | 2_378.0,
142 | 2_494.0,
143 | 698.0,
144 | 2_798.0,
145 | 2_456.0,
146 | 2_402.0,
147 | 2_417.0,
148 | 2_407.0,
149 | 3_597.0,
150 | 2_481.0,
151 | 2_481.0,
152 | 3_725.0,
153 | 2_395.0,
154 | 2_380.0,
155 | 2_398.0,
156 | 316.0,
157 | 1_949.0,
158 | 2_572.0,
159 | 2_472.0,
160 | 2_412.0,
161 | 2_415.0,
162 | 2_378.0,
163 | 2_361.0,
164 | 2_423.0,
165 | 2_389.0,
166 | 917.0,
167 | 2_578.0,
168 | 2_622.0,
169 | 2_585.0,
170 | 2_462.0,
171 | 1_087.0,
172 | 2_697.0,
173 | 2_584.0,
174 | 3_005.0,
175 | 2_431.0,
176 | 2_547.0,
177 | 2_547.0,
178 | 2_510.0,
179 | 2_471.0,
180 | 2_476.0,
181 | 2_424.0,
182 | 2_516.0,
183 | 2_408.0,
184 | 2_281.0,
185 | 2_502.0,
186 | 2_483.0,
187 | 2_521.0,
188 | 2_436.0,
189 | 2_456.0,
190 | 2_364.0,
191 | 2_550.0,
192 | 2_411.0,
193 | 2_419.0,
194 | 2_556.0,
195 | 2_282.0,
196 | 2_312.0,
197 | 2_243.0,
198 | 2_376.0,
199 | 2_373.0,
200 | 2_153.0,
201 | 2_305.0
202 | ]
203 | ]
204 | }
205 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/RSSGUIDTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SyndiKit
3 | import XCTest
4 |
5 | final class RSSGUIDTests: XCTestCase {
6 | func testGUIDURL() {
7 | let urlString = "https://developer.apple.com/news/?id=jxky8h89"
8 |
9 | let urlGUID = EntryID(string: urlString)
10 |
11 | guard case let .url(url) = urlGUID else {
12 | XCTFail()
13 | return
14 | }
15 | XCTAssertEqual(url, URL(string: urlString))
16 | }
17 |
18 | func testGUIDUUID() {
19 | let expectedUUID = UUID()
20 |
21 | let expectedUUIDString = expectedUUID.uuidString
22 | let uuidGUID = EntryID(string: expectedUUIDString)
23 |
24 | guard case let .uuid(actualUUID) = uuidGUID else {
25 | XCTFail()
26 | return
27 | }
28 | XCTAssertEqual(actualUUID, expectedUUID)
29 | }
30 |
31 | func testGUIDYouTube() {
32 | let expectedPath = ["yt", "video", "3hccNoPE59U"]
33 |
34 | let pathGUID = EntryID(string: expectedPath.joined(separator: ":"))
35 |
36 | guard case let .path(actualPath, ":") = pathGUID else {
37 | XCTFail()
38 | return
39 | }
40 | XCTAssertEqual(actualPath, expectedPath)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/RSSItemCategoryTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 |
4 | final class RSSItemCategoryTests: XCTestCase {
5 | func testTwoEqualCategories() {
6 | let c1 = RSSItemCategory(
7 | value: "Top Menu",
8 | domain: "nav_menu",
9 | nicename: "top-menu"
10 | )
11 |
12 | let c2 = RSSItemCategory(
13 | value: "Top Menu",
14 | domain: "nav_menu",
15 | nicename: "top-menu"
16 | )
17 |
18 | XCTAssertEqual(c1, c2)
19 | }
20 |
21 | func testTwoUnequalCategories() {
22 | let c1 = RSSItemCategory(
23 | value: "Uncategorized",
24 | domain: "category",
25 | nicename: "uncategorized"
26 | )
27 |
28 | let c2 = RSSItemCategory(
29 | value: "Top Menu",
30 | domain: "nav_menu",
31 | nicename: "top-menu"
32 | )
33 |
34 | XCTAssertNotEqual(c1, c2)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/UTF8EncodedURLTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 | import XMLCoder
4 |
5 | internal final class UTF8EncodedURLTests: XCTestCase {
6 | internal func testDecode() throws {
7 | let expectedURL = URL(strict: "http://www.example.com/index.php")!
8 | let urlStr = """
9 | "\(expectedURL)"
10 | """
11 |
12 | guard let data = urlStr.data(using: .utf8) else {
13 | XCTFail("Expected data out of \(urlStr)")
14 | return
15 | }
16 |
17 | let sut = try JSONDecoder().decode(UTF8EncodedURL.self, from: data)
18 |
19 | XCTAssertEqual(sut.value, expectedURL)
20 | XCTAssertNil(sut.string)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/WordPressElementsTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 |
4 | final class WordPressElementsTests: XCTestCase {
5 | func testCategoryEquatable() {
6 | let c1 = WordPressElements.Category(
7 | termID: 1,
8 | niceName: .init(stringLiteral: "uncategorized"),
9 | parent: .init(stringLiteral: ""),
10 | name: "Uncategorized"
11 | )
12 |
13 | let c2 = WordPressElements.Category(
14 | termID: 2,
15 | niceName: .init(stringLiteral: "podcasting"),
16 | parent: .init(stringLiteral: ""),
17 | name: "Podcasting"
18 | )
19 |
20 | XCTAssertNotEqual(c1, c2)
21 | }
22 |
23 | func testTagEquatable() {
24 | let t1 = WordPressElements.Tag(
25 | termID: 1,
26 | slug: .init(stringLiteral: "uncategorized"),
27 | name: .init(stringLiteral: "uncategorized")
28 | )
29 |
30 | let t2 = WordPressElements.Tag(
31 | termID: 2,
32 | slug: .init(stringLiteral: "podcasting"),
33 | name: .init(stringLiteral: "Podcasting")
34 | )
35 |
36 | XCTAssertNotEqual(t1, t2)
37 | }
38 |
39 | func testPostMetaEquatable() {
40 | let pm1 = WordPressElements.PostMeta(
41 | key: "_edit_last",
42 | value: "1"
43 | )
44 |
45 | let pm2 = WordPressElements.PostMeta(
46 | key: "_thumbnail_id",
47 | value: "57"
48 | )
49 |
50 | XCTAssertNotEqual(pm1, pm2)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/SyndiKitTests/XMLStringIntTests.swift:
--------------------------------------------------------------------------------
1 | @testable import SyndiKit
2 | import XCTest
3 | import XMLCoder
4 |
5 | internal final class XMLStringIntTests: XCTestCase {
6 | internal func testDecodeValidXMLValue() throws {
7 | let expectedAge = 10
8 | let xmlStr = """
9 | \(expectedAge)
10 | """
11 |
12 | guard let data = xmlStr.data(using: .utf8) else {
13 | XCTFail("Expected data out of \(xmlStr)")
14 | return
15 | }
16 |
17 | let sut = try XMLDecoder().decode(XMLStringInt.self, from: data)
18 |
19 | XCTAssertEqual(sut.value, expectedAge)
20 | }
21 |
22 | internal func testDecodeInvalidXMLValue() throws {
23 | let xmlStr = """
24 | invalid
25 | """
26 |
27 | guard let data = xmlStr.data(using: .utf8) else {
28 | XCTFail("Expected data out of \(xmlStr)")
29 | return
30 | }
31 |
32 | XCTAssertThrowsError(try XMLDecoder().decode(XMLStringInt.self, from: data)) { error in
33 | XCTAssertNotNil(error as? DecodingError)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "Tests/**/*"
3 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "DerivedData/Build/Products/Debug/SyndiKit.doccarchive/"
3 |
4 | [[redirects]]
5 | from = "/documentation/*"
6 | status = 200
7 | to = "/index.html"
8 |
9 | [[redirects]]
10 | from = "/tutorials/*"
11 | status = 200
12 | to = "/index.html"
13 |
14 | [[redirects]]
15 | from = "/data/documentation.json"
16 | status = 200
17 | to = "/data/documentation/syndikit.json"
18 |
19 | [[redirects]]
20 | force = true
21 | from = "/"
22 | status = 302
23 | to = "/documentation/"
24 |
25 | [[redirects]]
26 | force = true
27 | from = "/documentation"
28 | status = 302
29 | to = "/documentation/"
30 |
31 | [[redirects]]
32 | force = true
33 | from = "/tutorials"
34 | status = 302
35 | to = "/tutorials/"
--------------------------------------------------------------------------------
/project.yml:
--------------------------------------------------------------------------------
1 | name: SyndiKit
2 | settings:
3 | LINT_MODE: ${LINT_MODE}
4 | packages:
5 | SyndiKit:
6 | path: .
7 | aggregateTargets:
8 | Lint:
9 | buildScripts:
10 | - path: Scripts/lint.sh
11 | name: Lint
12 | basedOnDependencyAnalysis: false
13 | schemes: {}
--------------------------------------------------------------------------------