├── .github └── workflows │ ├── build.yml │ ├── clippy.yml │ ├── coverage.yml │ └── docs.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches ├── read.rs └── write.rs ├── clippy.toml ├── rustfmt.toml ├── src ├── category.rs ├── channel.rs ├── cloud.rs ├── enclosure.rs ├── error.rs ├── extension │ ├── atom.rs │ ├── dublincore.rs │ ├── itunes │ │ ├── itunes_category.rs │ │ ├── itunes_channel_extension.rs │ │ ├── itunes_item_extension.rs │ │ ├── itunes_owner.rs │ │ └── mod.rs │ ├── mod.rs │ ├── syndication.rs │ └── util.rs ├── guid.rs ├── image.rs ├── item.rs ├── lib.rs ├── source.rs ├── textinput.rs ├── toxml.rs ├── util.rs └── validation.rs └── tests ├── data ├── category.xml ├── channel.xml ├── cloud.xml ├── content.xml ├── dublincore.xml ├── dublincore_altprefix.xml ├── enclosure.xml ├── extension.xml ├── guid.xml ├── image.xml ├── item.xml ├── itunes.xml ├── mixed_content.xml ├── rss090.xml ├── rss091.xml ├── rss092.xml ├── rss1.xml ├── rss2_with_atom.xml ├── rss2sample.xml ├── source.xml ├── syndication.xml ├── textinput.xml └── verify_write_format.xml ├── read.rs └── write.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | - macOS-latest 14 | - windows-latest 15 | rust: 16 | - nightly 17 | - beta 18 | - stable 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: dtolnay/rust-toolchain@v1 23 | with: 24 | toolchain: ${{ matrix.rust }} 25 | - name: Build 26 | run: | 27 | cargo build --no-default-features --verbose 28 | cargo build --all-features --verbose 29 | cargo build --benches --verbose 30 | - name: Run tests 31 | run: cargo test --all-features --no-fail-fast --verbose --all -- --nocapture 32 | env: 33 | RUST_BACKTRACE: 1 34 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Code check 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | clippy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: dtolnay/rust-toolchain@stable 11 | - run: cargo clippy --all --all-features 12 | fmt: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: dtolnay/rust-toolchain@stable 17 | - run: cargo fmt --all -- --check 18 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | coverage: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: xd009642/tarpaulin:develop-nightly 14 | options: --security-opt seccomp=unconfined 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Measure coverage 18 | run: | 19 | cargo +nightly tarpaulin \ 20 | --forward \ 21 | --timeout 5 \ 22 | --run-types Tests --run-types Doctests \ 23 | --all-features \ 24 | --out xml \ 25 | --verbose \ 26 | -- \ 27 | --test-threads=1 28 | - name: Upload coverage report to Codecov 29 | uses: codecov/codecov-action@v3 30 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: dtolnay/rust-toolchain@stable 17 | 18 | - name: Generate docs 19 | run: cargo doc --features "validation serde" 20 | 21 | - name: Deploy documentation 🚀 22 | uses: JamesIves/github-pages-deploy-action@releases/v3 23 | with: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | BRANCH: gh-pages 26 | FOLDER: target/doc 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Linux 2 | *~ 3 | .Trash-* 4 | 5 | # Generated by Cargo 6 | target 7 | Cargo.lock 8 | target-install 9 | 10 | # Gnome Builder 11 | .buildconfig 12 | 13 | # Rustfmt 14 | *.bk 15 | *.backup 16 | 17 | # IntelliJ 18 | .idea 19 | *.iml 20 | 21 | # vscode 22 | .vscode 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 2.0.12 - 2025-02-17 6 | 7 | - Add a test to ensure that `Error` satisfies `Send` and `Sync`. 8 | - Publish tests. [`#179`](https://github.com/rust-syndication/rss/pull/179) 9 | 10 | ## 2.0.11 - 2024-11-22 11 | 12 | - Fix `]]>` escaping in `CDATA` sections. [`#174`](https://github.com/rust-syndication/rss/pull/174) 13 | 14 | ## 2.0.10 - 2024-11-16 15 | 16 | - Remove ambiguous statements about escaping from documentation. [`#171`](https://github.com/rust-syndication/rss/pull/171) 17 | - Update `quick-xml` to 0.37. [`#172`](https://github.com/rust-syndication/rss/pull/172) 18 | 19 | ## 2.0.9 - 2024-08-28 20 | 21 | - Fix Clippy Warnings. [`#164`](https://github.com/rust-syndication/rss/pull/164) 22 | - Add `From` constructor for `Category`. [`#165`](https://github.com/rust-syndication/rss/pull/165) 23 | - Update `quick-xml` to 0.36. [`#166`](https://github.com/rust-syndication/rss/pull/166) 24 | 25 | ## 2.0.8 - 2024-05-11 26 | 27 | - Update quick-xml and derive_builder dependencies. [`#162`](https://github.com/rust-syndication/rss/pull/162) 28 | 29 | ## 2.0.7 - 2024-01-13 30 | 31 | - Update `chrono` to 0.4.31 [`#160`](https://github.com/rust-syndication/rss/pull/160) 32 | - Change how iTunes extension is detected. Use case insensitive comparison of a namespace [`#159`](https://github.com/rust-syndication/rss/pull/159) 33 | 34 | ## 2.0.6 - 2023-08-12 35 | 36 | - Take into account namespaces declared locally [`#155`](https://github.com/rust-syndication/rss/pull/155) 37 | 38 | ## 2.0.5 - 2023-07-26 39 | 40 | - Upgrade `quick_xml` to `0.30` [`#153`](https://github.com/rust-syndication/rss/pull/153) 41 | 42 | ## 2.0.4 - 2023-05-29 43 | 44 | - Fix iTunes extension writing [`#151`](https://github.com/rust-syndication/rss/pull/151) 45 | 46 | ## 2.0.3 - 2023-03-27 47 | 48 | - Upgrade `quick_xml` to `0.28` [`#146`](https://github.com/rust-syndication/rss/pull/146) 49 | - Switch to Rust 2021 [`#147`](https://github.com/rust-syndication/rss/pull/147) 50 | 51 | ## 2.0.2 - 2023-01-14 52 | 53 | - Upgrade `quick_xml` to `0.27`, `derive_builder` to `0.12`, and `atom_syndication` to `0.12` [`#143`](https://github.com/rust-syndication/rss/pull/143) 54 | - Correct serialization of atom extension [`#144`](https://github.com/rust-syndication/rss/pull/144) 55 | - Read non-blank links only [`#145`](https://github.com/rust-syndication/rss/pull/145) 56 | 57 | ## 2.0.1 - 2022-04-17 58 | 59 | - check if update_period and frequency are valid [`#135`](https://github.com/rust-syndication/rss/pull/135) 60 | 61 | ## 2.0.0 - 2021-10-21 62 | 63 | - Disable clock feature of chrono to mitigate RUSTSEC-2020-0159 [`#130`](https://github.com/rust-syndication/rss/pull/130) 64 | - Update quick_xml to 0.22 [`0daf20b`](https://github.com/rust-syndication/rss/commit/0daf20b6f19411450f79090d687d796414193327) 65 | - Fix issues found by clippy [`f3283a1`](https://github.com/rust-syndication/rss/commit/f3283a13808f41f0c10cd64720e493f18a286967) 66 | - Replace HashMap with BTreeMap to have a stable order of tags/attributes [`8b088b1`](https://github.com/rust-syndication/rss/commit/8b088b147c0801a950b5197d6faa475ca766f257) 67 | - Update atom_syndication to 0.10.0 [`975e4aa`](https://github.com/rust-syndication/rss/commit/975e4aa9914985ff4af7ee9834294c15691f0b92) 68 | - Infallible builders [`f736a24`](https://github.com/rust-syndication/rss/commit/f736a2480b3d13114f223048b36f47641bf64858) 69 | 70 | ## 1.10.0 - 2021-01-07 71 | 72 | - Add the itunes channel "itunes:type" tag. [`#101`](https://github.com/rust-syndication/rss/pull/101) 73 | - Add itunes season tag [`#100`](https://github.com/rust-syndication/rss/pull/100) 74 | - Fix typo in item.rs [`#97`](https://github.com/rust-syndication/rss/pull/97) 75 | - fix benches [`#96`](https://github.com/rust-syndication/rss/pull/96) 76 | - make fields public [`#94`](https://github.com/rust-syndication/rss/pull/94) 77 | - change badges [`#95`](https://github.com/rust-syndication/rss/pull/95) 78 | - fix clippy warnings [`#93`](https://github.com/rust-syndication/rss/pull/93) 79 | - migrate to github actions [`#91`](https://github.com/rust-syndication/rss/pull/91) 80 | - remove from_url feature [`#88`](https://github.com/rust-syndication/rss/pull/88) 81 | - remove deprecated Error description and cause [`#89`](https://github.com/rust-syndication/rss/pull/89) 82 | - implement Atom extension [`6b6eac1`](https://github.com/rust-syndication/rss/commit/6b6eac1699ec63a7274f8ca0ad2088d5d4b38804) 83 | - reformat all code [`72dbe42`](https://github.com/rust-syndication/rss/commit/72dbe42c42c49670b88a96957445d13bfed3bce7) 84 | - initial github actions [`3557c96`](https://github.com/rust-syndication/rss/commit/3557c9606422a7e1670ab0f7e4188661b0c77955) 85 | 86 | ## 1.9.0 - 2020-01-23 87 | 88 | - Add a default builders feature that can be disabled [`#83`](https://github.com/rust-syndication/rss/pull/83) 89 | - Remove dependency on failure [`#82`](https://github.com/rust-syndication/rss/pull/82) 90 | - migrate to 2018 edition [`a836e15`](https://github.com/rust-syndication/rss/commit/a836e158759f8a13763bd1078395a5f23207f194) 91 | - Update dependencies [`e76ec24`](https://github.com/rust-syndication/rss/commit/e76ec245509be124d791f12c4eca28796cc96ac7) 92 | - work with clippy and rustfmt [`b1a3ee3`](https://github.com/rust-syndication/rss/commit/b1a3ee3982244899ee77407104bee3791400444c) 93 | 94 | ## 1.8.0 - 2019-05-25 95 | 96 | - Syndication support [`#78`](https://github.com/rust-syndication/rss/pull/78) 97 | - Bump quick-xml to 0.14 [`#77`](https://github.com/rust-syndication/rss/pull/77) 98 | - Add support for RSS syndication module [`baa9b36`](https://github.com/rust-syndication/rss/commit/baa9b3636364f47648b287d352f010ed01fb87cd) 99 | - Parse via namespace and prefix [`91c0c03`](https://github.com/rust-syndication/rss/commit/91c0c03d58d26b775a6e9c634f482be18163b8ee) 100 | - Static analysis fixes [`4b975c7`](https://github.com/rust-syndication/rss/commit/4b975c76c78f0eb21f43429d329f57c821ab5124) 101 | 102 | ## 1.7.0 - 2019-03-26 103 | 104 | - read url to bytes buffer instead of a string [`#76`](https://github.com/rust-syndication/rss/pull/76) 105 | - Prepare for 1.7.0 release [`3a551a6`](https://github.com/rust-syndication/rss/commit/3a551a664354f5bdcd7b396e634a131c796f062c) 106 | 107 | ## 1.6.1 - 2018-11-18 108 | 109 | - Update derive_builder and bump version [`#73`](https://github.com/rust-syndication/rss/pull/73) 110 | - Add badge from deps.rs and prepare for 1.6.1 release [`59bc1ab`](https://github.com/rust-syndication/rss/commit/59bc1ab40f2677fe891cfa8f277dee2b55686868) 111 | 112 | ## 1.6.0 - 2018-10-13 113 | 114 | - Update to reqwest version 0.9.2 [`#72`](https://github.com/rust-syndication/rss/pull/72) 115 | - Update dependencies and prepare for 1.6 release [`705fa6f`](https://github.com/rust-syndication/rss/commit/705fa6f616cdd621fe366ef688532cee2f4341dd) 116 | - updated reqwest version [`8fc6a83`](https://github.com/rust-syndication/rss/commit/8fc6a83b658cb180eb602d7ead20b446371a2111) 117 | - fixed typo [`e566647`](https://github.com/rust-syndication/rss/commit/e5666477612e46ada6ab6574073851f3a1588917) 118 | 119 | ## 1.5.0 - 2018-04-18 120 | 121 | - Prepare for 1.5.0 release. [`97ffcd4`](https://github.com/rust-syndication/rss/commit/97ffcd474eec332523634debfe3aa2efa27032f7) 122 | - Merge #68 [`2cee413`](https://github.com/rust-syndication/rss/commit/2cee4136629dc57ee00fe8a41ad31a42d62b6a0e) 123 | - Channel: Add items_owned() method [`b4a5ff5`](https://github.com/rust-syndication/rss/commit/b4a5ff563bf3ab6f1c83b5c79e625ff0292aa480) 124 | 125 | ## 1.4.0 - 2018-03-10 126 | 127 | - Make the output prettier. [`#66`](https://github.com/rust-syndication/rss/pull/66) 128 | - rustfmt. [`67724b3`](https://github.com/rust-syndication/rss/commit/67724b3b361357b016a23421718fde91c8a1d1dc) 129 | - RustFmt the code. [`fbd8334`](https://github.com/rust-syndication/rss/commit/fbd83344ecbd200d2e81f4d241676fc49f4074a4) 130 | - Bump quick_xml: 0.11 -> 0.12. [`b22649e`](https://github.com/rust-syndication/rss/commit/b22649e9d51c8c9f0ceb5eb5d6898ba0da14abb2) 131 | 132 | ## 1.3.0 - 2018-02-11 133 | 134 | - Add opt-in serde serialization. [`#55`](https://github.com/rust-syndication/rss/pull/55) 135 | 136 | ## 1.2.1 - 2017-11-27 137 | 138 | - Update quick-xml to 0.10.0 [`#64`](https://github.com/rust-syndication/rss/pull/64) 139 | 140 | ## 1.2.0 - 2017-11-27 141 | 142 | - Prepare for 1.2.0 release. [`80824a1`](https://github.com/rust-syndication/rss/commit/80824a186d6140afe88134e4953546d8bd9af0c6) 143 | 144 | ## 1.1.0 - 2017-09-10 145 | 146 | - Merge #62 [`ac6ea2c`](https://github.com/rust-syndication/rss/commit/ac6ea2c51eeb8b413fa330ea31e5ea36a45116d6) 147 | - Add mutable slice variants of all slice-returning methods. [`6311dac`](https://github.com/rust-syndication/rss/commit/6311daca6fdec5e9a6574e7ac8f0641f147e97a1) 148 | 149 | ### 1.0.0 - 2017-08-28 150 | 151 | - Merge #56 [`#32`](https://github.com/rust-syndication/rss/issues/32) 152 | - Merge #60 [`5709f23`](https://github.com/rust-syndication/rss/commit/5709f2324d9f874cad5911ae33369d0deef64b5d) 153 | - Merge #58 #59 [`0d21138`](https://github.com/rust-syndication/rss/commit/0d21138a257e7785192d8116611cbac4dc0b4bfe) 154 | - Add regression test for escaped test. [`321d0e9`](https://github.com/rust-syndication/rss/commit/321d0e966145d3c3ec14d917aa6a73b2709c6e89) 155 | 156 | ## 0.7.0 - 2017-07-12 157 | 158 | - Added setters to structs [`#52`](https://github.com/rust-syndication/rss/pull/52) 159 | - Format using rustfmt-nightly [`#50`](https://github.com/rust-syndication/rss/pull/50) 160 | - Update readme to match lib doc [`#48`](https://github.com/rust-syndication/rss/pull/48) 161 | - Added constructors to builders that consume their immutable counterparts [`#45`](https://github.com/rust-syndication/rss/pull/45) 162 | - Fix some typos [`#47`](https://github.com/rust-syndication/rss/pull/47) 163 | - Added constructors to builders that consume their immutable counterparts [`#41`](https://github.com/rust-syndication/rss/issues/41) 164 | - Add setters to structs [`260623c`](https://github.com/rust-syndication/rss/commit/260623c15570176d14e37caaf5b7bf74b8a48666) 165 | - Reformatted with rustfmt 0.9.0 [`f3a3de8`](https://github.com/rust-syndication/rss/commit/f3a3de8675859698c24892175196fb4d9a042e5a) 166 | - Added validation module [`9a755f9`](https://github.com/rust-syndication/rss/commit/9a755f93eab2ac20b73942fbd14d66f9aa4af350) 167 | 168 | ## 0.6.0 - 2017-05-30 169 | 170 | - Added read support for RSS 0.90, 0.91, 0.92, 1.0 [`#40`](https://github.com/rust-syndication/rss/pull/40) 171 | - Shortened some really long example strings, more validation docs [`#43`](https://github.com/rust-syndication/rss/pull/43) 172 | - Add rating for from xml and to xml [`#44`](https://github.com/rust-syndication/rss/pull/44) 173 | - Revised docs and made builders a bit nicer to use [`#42`](https://github.com/rust-syndication/rss/pull/42) 174 | - Doc Updates [`#39`](https://github.com/rust-syndication/rss/pull/39) 175 | - Documentation cleanup, removed new() methods on builders in favor of default() [`c0760d9`](https://github.com/rust-syndication/rss/commit/c0760d909f0386edee96fe6b599428ebe36195ca) 176 | - Changed finalize to return T instead of Result<T, E> [`a62f972`](https://github.com/rust-syndication/rss/commit/a62f97251e343ee7129236e5a1c6a5bab4afa482) 177 | - Changed all builder methods to take Into<T> [`83e478b`](https://github.com/rust-syndication/rss/commit/83e478b382b24731abf1e7b96854c111e37bd7da) 178 | 179 | ## 0.5.1 - 2017-05-28 180 | 181 | - Upgrade to quick-xml 0.7.3 [`#36`](https://github.com/rust-syndication/rss/pull/36) 182 | - upgrade to quick-xml 0.6.0 [`e951c6b`](https://github.com/rust-syndication/rss/commit/e951c6b1a15626b364884110b4769fdde6f232ae) 183 | - refactor fromxml [`d7e73fc`](https://github.com/rust-syndication/rss/commit/d7e73fc7e24b4eaae44f06489fdeee0dfa9620e9) 184 | - rustfmt [`9705dcf`](https://github.com/rust-syndication/rss/commit/9705dcf66a5c0be4e803d8a48b6d9803426b2579) 185 | 186 | ## 0.5.0 - 2017-05-22 187 | 188 | - Cleanup following #35 [`#38`](https://github.com/rust-syndication/rss/pull/38) 189 | - Merge feed functionality into rss. [`#35`](https://github.com/rust-syndication/rss/pull/35) 190 | - Updates to extensions [`3feed5f`](https://github.com/rust-syndication/rss/commit/3feed5f78a1b6f6e66b053113e5aa62b0e736079) 191 | - Use rustfmt defaults [`593b7aa`](https://github.com/rust-syndication/rss/commit/593b7aa8ef84e340b6f2311290a5531bcf10e9f1) 192 | - Updates to Channel, [`902df10`](https://github.com/rust-syndication/rss/commit/902df101939b2d1ee04fb2560fa6607ea0558425) 193 | 194 | ## 0.4.0 - 2016-09-05 195 | 196 | - Store and write namespaces [`#27`](https://github.com/rust-syndication/rss/pull/27) 197 | - Fixed writing and parsing bug with <rss> tag [`#26`](https://github.com/rust-syndication/rss/pull/26) 198 | - Writing support [`#25`](https://github.com/rust-syndication/rss/pull/25) 199 | - Replace project codebase with rss-rs [`#24`](https://github.com/rust-syndication/rss/pull/24) 200 | - Address suggestions made by rust-clippy [`#20`](https://github.com/rust-syndication/rss/pull/20) 201 | - Add category read test [`#19`](https://github.com/rust-syndication/rss/pull/19) 202 | - Removed previous files [`6c8aa8e`](https://github.com/rust-syndication/rss/commit/6c8aa8ea4791fdaa4a1690c814255318b5ec2cf5) 203 | - Full implementation of RSS 2.0 specification [`fc1eee8`](https://github.com/rust-syndication/rss/commit/fc1eee84e7d971b22d0fbdb82d1cad3fbfa59e85) 204 | 205 | ## 0.3.1 - 2015-11-15 206 | 207 | - Remove unnecessary unwraps [`#18`](https://github.com/rust-syndication/rss/pull/18) 208 | - Get docs building again (requires sudo) [`#17`](https://github.com/rust-syndication/rss/pull/17) 209 | 210 | ## 0.3.0 - 2015-11-14 211 | 212 | - Implement <image> struct [`#16`](https://github.com/rust-syndication/rss/pull/16) 213 | - Implement <image> struct [`#13`](https://github.com/rust-syndication/rss/issues/13) 214 | 215 | ## 0.2.3 - 2015-10-31 216 | 217 | - Implement item/guid spec [`#14`](https://github.com/rust-syndication/rss/pull/14) 218 | 219 | ## 0.2.2 - 2015-10-29 220 | 221 | - Fix inaccurate reading of <channel> properties [`#12`](https://github.com/rust-syndication/rss/pull/12) 222 | - Fix inaccurate reading of <channel> properties [`#11`](https://github.com/rust-syndication/rss/issues/11) [`#12`](https://github.com/rust-syndication/rss/issues/12) 223 | 224 | ## 0.2.1 - 2015-10-16 225 | 226 | - Fix non sized warning for ViaXml [`#9`](https://github.com/rust-syndication/rss/pull/9) 227 | - Make ViaXml sized [`9409223`](https://github.com/rust-syndication/rss/commit/9409223d20cc93d120636a4ed55b034c22626cd5) 228 | 229 | ## 0.2.0 - 2015-07-29 230 | 231 | - Replace static str errors with an error enum [`#7`](https://github.com/rust-syndication/rss/pull/7) and [`#6`](https://github.com/rust-syndication/rss/issues/6) 232 | 233 | ## 0.1.2 - 2015-07-22 234 | 235 | - Return Error instead of panicking when parsing something that isn't an RSS feed [`#5`](https://github.com/rust-syndication/rss/pull/5) 236 | - Return Error instead of panicking [`ce89c6b`](https://github.com/rust-syndication/rss/commit/ce89c6b17c53ff3f8dcb9dc6ec2bc44ca97b43f9) 237 | - Also test on Rust beta channel [`0a435b9`](https://github.com/rust-syndication/rss/commit/0a435b90b031d30fccf1d4431f8f30d5d22442bb) 238 | 239 | ## 0.1.1 - 2015-06-10 240 | 241 | - Lock on to stable channel for travis [`#4`](https://github.com/rust-syndication/rss/pull/4) 242 | - Add Apache license headers to source files [`001dc0d`](https://github.com/rust-syndication/rss/commit/001dc0d385ce7f5ca2261e1257c2098d2e94d4e8) 243 | - Derive Debug, Clone for Channel, Item, Category, TextInput [`d29a4f2`](https://github.com/rust-syndication/rss/commit/d29a4f288977e78ff1f064d691424552cee9dae5) 244 | - Update reading/writing examples to have same channel title [`cb37502`](https://github.com/rust-syndication/rss/commit/cb3750266a71c0ed6a010ae045a644f1c8cb5fc1) 245 | 246 | ## 0.1.0 - 2015-05-09 247 | 248 | - Move things into submodule [`b834d0b`](https://github.com/rust-syndication/rss/commit/b834d0bf5c9892e7f149737071291d2903036fe7) 249 | - Add more examples [`4851f17`](https://github.com/rust-syndication/rss/commit/4851f176c972cdbefd3fe14e7e71ca36dd3a4e44) 250 | - Replace from_reader with from_str trait [`2bc231a`](https://github.com/rust-syndication/rss/commit/2bc231a8033ff81db6c3d47ea235223cdf1e671d) 251 | 252 | ## 0.0.7 - 2015-04-25 253 | 254 | - Shorten 'element' variables to 'elem' [`163304b`](https://github.com/rust-syndication/rss/commit/163304b479a26970d2c609d3454058e8d98a28ef) 255 | - Add pub_date field on Item [`c695fa0`](https://github.com/rust-syndication/rss/commit/c695fa00121b41c987e98e6d90b86bdd108de67e) 256 | 257 | ## 0.0.6 - 2015-04-23 258 | 259 | - Save <textInput> upon export [`37eb057`](https://github.com/rust-syndication/rss/commit/37eb057d64476f12eae1ce315e38b79fcbcc1b61) 260 | 261 | ## 0.0.5 - 2015-04-23 262 | 263 | - Implement <textInput> sub-element of <channel> [`c6e961f`](https://github.com/rust-syndication/rss/commit/c6e961f2d61c58cf24d357c587f1070269019ba0) 264 | - Clean up tests a little bit [`0bd8516`](https://github.com/rust-syndication/rss/commit/0bd8516b83b961919bb5e0925420cab6f8eee8fc) 265 | - Add test ensuring PI XML node gets ignored [`298c830`](https://github.com/rust-syndication/rss/commit/298c8303b79509bbfd735a67287ee51ce925531f) 266 | 267 | ## 0.0.4 - 2015-04-16 268 | 269 | - Add license [`c7d935e`](https://github.com/rust-syndication/rss/commit/c7d935eabecfbfbe762a3270c0c695434081db8c) 270 | - Rename constructor [`884439a`](https://github.com/rust-syndication/rss/commit/884439aeba805a655749734a8e5ce8fe8156814b) 271 | 272 | ## 0.0.3 - 2015-04-10 273 | 274 | - Rename method to be more descriptive [`152a168`](https://github.com/rust-syndication/rss/commit/152a168d77283bdecb683ef839482fea618430f2) 275 | - Read Channel properties from xml [`a9be5ec`](https://github.com/rust-syndication/rss/commit/a9be5ecd02112669e8912660a7df3c85f9ab4eb7) 276 | - Simplify a few Option::map [`f9f475b`](https://github.com/rust-syndication/rss/commit/f9f475b0ae00ecfd1fd67fefd7cde945b620a451) 277 | 278 | ## 0.0.2 - 2015-03-31 279 | 280 | - Implement Rss.from_str [`#1`](https://github.com/rust-syndication/rss/pull/1) 281 | - Add basic test to read RSS file [`39b23d5`](https://github.com/rust-syndication/rss/commit/39b23d5c8b62956b93029bb599046b40f455f90a) 282 | - Swap xml-rs out with rustyxml [`f261c40`](https://github.com/rust-syndication/rss/commit/f261c4033fab8ecc110e7a855a242290d63c3437) 283 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rss" 3 | version = "2.0.12" 4 | authors = ["James Hurst ", "Corey Farwell ", "Chris Palmer "] 5 | description = "Library for serializing the RSS web content syndication format" 6 | repository = "https://github.com/rust-syndication/rss" 7 | documentation = "https://docs.rs/rss/" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["rss", "feed", "parser", "parsing"] 11 | edition = "2021" 12 | 13 | [package.metadata.docs.rs] 14 | all-features = false 15 | 16 | [features] 17 | default = ["builders"] 18 | atom = ["atom_syndication"] 19 | builders = ["derive_builder", "never", "atom_syndication/builders"] 20 | validation = ["chrono", "chrono/std", "url", "mime"] 21 | with-serde = ["serde", "atom_syndication/with-serde"] 22 | 23 | [dependencies] 24 | quick-xml = { version = "0.37.1", features = ["encoding"] } 25 | atom_syndication = { version = "0.12", optional = true } 26 | chrono = { version = "0.4.31", optional = true, default-features = false, features = ["alloc"] } 27 | derive_builder = { version = "0.20", optional = true } 28 | mime = { version = "0.3", optional = true } 29 | never = { version = "0.1", optional = true } 30 | serde = { version = "1.0", optional = true, features = ["derive"] } 31 | url = { version = "2.1", optional = true } 32 | 33 | [dev-dependencies] 34 | bencher = "0.1" 35 | 36 | [[bench]] 37 | name = "read" 38 | path = "benches/read.rs" 39 | harness = false 40 | 41 | [[bench]] 42 | name = "write" 43 | path = "benches/write.rs" 44 | harness = false 45 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015-2021 The rust-syndication Developers 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2015-2021 The rust-syndication Developers 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rss 2 | 3 | [![Build status](https://github.com/rust-syndication/rss/workflows/Build/badge.svg)](https://github.com/rust-syndication/rss/actions?query=branch%3Amaster) 4 | [![Codecov](https://codecov.io/gh/rust-syndication/rss/branch/master/graph/badge.svg)](https://codecov.io/gh/rust-syndication/rss) 5 | [![crates.io Status](https://img.shields.io/crates/v/rss.svg)](https://crates.io/crates/rss) 6 | [![Docs](https://docs.rs/rss/badge.svg)](https://docs.rs/rss) 7 | 8 | Library for deserializing and serializing the RSS web content syndication format. 9 | 10 | ### Supported Versions 11 | 12 | Reading from the following RSS versions is supported: 13 | 14 | * RSS 0.90 15 | * RSS 0.91 16 | * RSS 0.92 17 | * RSS 1.0 18 | * RSS 2.0 19 | 20 | Writing support is limited to RSS 2.0. 21 | 22 | ### Documentation 23 | 24 | - [Released](https://docs.rs/rss/) 25 | - [Master](https://rust-syndication.github.io/rss/rss/) 26 | 27 | ## Usage 28 | 29 | Add the dependency to your `Cargo.toml`. 30 | 31 | ```toml 32 | [dependencies] 33 | rss = "2.0" 34 | ``` 35 | 36 | ## Reading 37 | 38 | A channel can be read from any object that implements the `BufRead` trait. 39 | 40 | ### From a file 41 | 42 | ```rust 43 | use std::fs::File; 44 | use std::io::BufReader; 45 | use rss::Channel; 46 | 47 | let file = File::open("example.xml").unwrap(); 48 | let channel = Channel::read_from(BufReader::new(file)).unwrap(); 49 | ``` 50 | 51 | ### From a buffer 52 | 53 | **Note**: This example requires [reqwest](https://crates.io/crates/reqwest) crate. 54 | 55 | ```rust 56 | use std::error::Error; 57 | use rss::Channel; 58 | 59 | async fn example_feed() -> Result> { 60 | let content = reqwest::get("http://example.com/feed.xml") 61 | .await? 62 | .bytes() 63 | .await?; 64 | let channel = Channel::read_from(&content[..])?; 65 | Ok(channel) 66 | } 67 | ``` 68 | 69 | ## Writing 70 | 71 | A channel can be written to any object that implements the `Write` trait or converted to an XML string using the `ToString` trait. 72 | 73 | ```rust 74 | use rss::Channel; 75 | 76 | let channel = Channel::default(); 77 | channel.write_to(::std::io::sink()).unwrap(); // // write to the channel to a writer 78 | let string = channel.to_string(); // convert the channel to a string 79 | ``` 80 | 81 | ## Creation 82 | 83 | Builder methods are provided to assist in the creation of channels. 84 | 85 | **Note**: This requires the `builders` feature, which is enabled by default. 86 | 87 | ```rust 88 | use rss::ChannelBuilder; 89 | 90 | let channel = ChannelBuilder::default() 91 | .title("Channel Title") 92 | .link("http://example.com") 93 | .description("An RSS feed.") 94 | .build() 95 | .unwrap(); 96 | ``` 97 | 98 | ## Validation 99 | 100 | Validation methods are provided to validate the contents of a channel against the RSS specification. 101 | 102 | **Note**: This requires enabling the `validation` feature. 103 | 104 | ```rust 105 | use rss::Channel; 106 | use rss::validation::Validate; 107 | 108 | let channel = Channel::default(); 109 | channel.validate().unwrap(); 110 | ``` 111 | 112 | ## Extensions 113 | 114 | Elements which have non-default namespaces will be considered extensions. Extensions are stored in `Channel.extensions` and `Item.extensions`. 115 | 116 | For convenience, [Dublin Core](http://dublincore.org/documents/dces/), [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) and [iTunes](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) extensions are extracted to structs and stored in as properties on channels and items. 117 | 118 | ## Invalid Feeds 119 | 120 | As a best effort to parse invalid feeds `rss` will default elements declared as "required" by the RSS 2.0 specification to an empty string. 121 | 122 | ## License 123 | 124 | Licensed under either of 125 | 126 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 127 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 128 | 129 | at your option. 130 | -------------------------------------------------------------------------------- /benches/read.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use bencher::{benchmark_group, benchmark_main, Bencher}; 9 | use rss::Channel; 10 | 11 | fn read_rss2sample(b: &mut Bencher) { 12 | let input: &[u8] = include_bytes!("../tests/data/rss2sample.xml"); 13 | b.iter(|| { 14 | let _ = Channel::read_from(input).expect("failed to parse feed"); 15 | }); 16 | } 17 | 18 | fn read_itunes(b: &mut Bencher) { 19 | let input: &[u8] = include_bytes!("../tests/data/itunes.xml"); 20 | b.iter(|| { 21 | let _ = Channel::read_from(input).expect("failed to parse feed"); 22 | }); 23 | } 24 | 25 | fn read_dublincore(b: &mut Bencher) { 26 | let input: &[u8] = include_bytes!("../tests/data/dublincore.xml"); 27 | b.iter(|| { 28 | let _ = Channel::read_from(input).expect("failed to parse feed"); 29 | }); 30 | } 31 | 32 | fn read_syndication(b: &mut Bencher) { 33 | let input: &[u8] = include_bytes!("../tests/data/syndication.xml"); 34 | b.iter(|| { 35 | let _ = Channel::read_from(input).expect("failed to parse feed"); 36 | }); 37 | } 38 | 39 | benchmark_group!( 40 | benches, 41 | read_rss2sample, 42 | read_itunes, 43 | read_dublincore, 44 | read_syndication, 45 | ); 46 | benchmark_main!(benches); 47 | -------------------------------------------------------------------------------- /benches/write.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use bencher::{benchmark_group, benchmark_main, Bencher}; 9 | use rss::Channel; 10 | use std::io::sink; 11 | 12 | fn write_rss2sample(b: &mut Bencher) { 13 | let input: &[u8] = include_bytes!("../tests/data/rss2sample.xml"); 14 | let channel = Channel::read_from(input).expect("failed to parse feed"); 15 | b.iter(|| { 16 | let _ = channel.write_to(sink()).expect("failed to write"); 17 | }); 18 | } 19 | 20 | fn write_itunes(b: &mut Bencher) { 21 | let input: &[u8] = include_bytes!("../tests/data/itunes.xml"); 22 | let channel = Channel::read_from(input).expect("failed to parse feed"); 23 | b.iter(|| { 24 | let _ = channel.write_to(sink()).expect("failed to write"); 25 | }); 26 | } 27 | 28 | fn write_dublincore(b: &mut Bencher) { 29 | let input: &[u8] = include_bytes!("../tests/data/dublincore.xml"); 30 | let channel = Channel::read_from(input).expect("failed to parse feed"); 31 | b.iter(|| { 32 | let _ = channel.write_to(sink()).expect("failed to write"); 33 | }); 34 | } 35 | 36 | fn write_syndication(b: &mut Bencher) { 37 | let input: &[u8] = include_bytes!("../tests/data/syndication.xml"); 38 | let channel = Channel::read_from(input).expect("failed to parse feed"); 39 | b.iter(|| { 40 | let _ = channel.write_to(sink()).expect("failed to write"); 41 | }); 42 | } 43 | 44 | benchmark_group!( 45 | benches, 46 | write_rss2sample, 47 | write_itunes, 48 | write_dublincore, 49 | write_syndication, 50 | ); 51 | benchmark_main!(benches); 52 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-syndication/rss/7d9627a92e93632a6458970b353c440195fc43eb/clippy.toml -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-syndication/rss/7d9627a92e93632a6458970b353c440195fc43eb/rustfmt.toml -------------------------------------------------------------------------------- /src/category.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::attributes::Attributes; 11 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Reader; 14 | use quick_xml::Writer; 15 | 16 | use crate::error::Error; 17 | use crate::toxml::ToXml; 18 | use crate::util::{attr_value, decode, element_text}; 19 | 20 | /// Represents a category in an RSS feed. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug, Default, Clone, PartialEq)] 23 | #[cfg_attr(feature = "builders", derive(Builder))] 24 | #[cfg_attr( 25 | feature = "builders", 26 | builder( 27 | setter(into), 28 | default, 29 | build_fn(name = "build_impl", private, error = "never::Never") 30 | ) 31 | )] 32 | pub struct Category { 33 | /// The name of the category. 34 | pub name: String, 35 | /// The domain for the category. 36 | pub domain: Option, 37 | } 38 | 39 | impl Category { 40 | /// Return the name of this category. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ``` 45 | /// use rss::Category; 46 | /// 47 | /// let mut category = Category::default(); 48 | /// category.set_name("Technology"); 49 | /// assert_eq!(category.name(), "Technology"); 50 | /// ``` 51 | pub fn name(&self) -> &str { 52 | self.name.as_str() 53 | } 54 | 55 | /// Set the name of this category. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ``` 60 | /// use rss::Category; 61 | /// 62 | /// let mut category = Category::default(); 63 | /// category.set_name("Technology"); 64 | /// ``` 65 | pub fn set_name(&mut self, name: V) 66 | where 67 | V: Into, 68 | { 69 | self.name = name.into(); 70 | } 71 | 72 | /// Return the domain of this category. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ``` 77 | /// use rss::Category; 78 | /// 79 | /// let mut category = Category::default(); 80 | /// category.set_domain("http://example.com".to_string()); 81 | /// assert_eq!(category.domain(), Some("http://example.com")); 82 | /// ``` 83 | pub fn domain(&self) -> Option<&str> { 84 | self.domain.as_deref() 85 | } 86 | 87 | /// Set the domain of this category. 88 | /// 89 | /// # Examples 90 | /// 91 | /// ``` 92 | /// use rss::Category; 93 | /// 94 | /// let mut category = Category::default(); 95 | /// category.set_domain("http://example.com".to_string()); 96 | /// ``` 97 | pub fn set_domain(&mut self, domain: V) 98 | where 99 | V: Into>, 100 | { 101 | self.domain = domain.into(); 102 | } 103 | } 104 | 105 | impl Category { 106 | /// Builds a Category from source XML 107 | pub fn from_xml( 108 | reader: &mut Reader, 109 | mut atts: Attributes, 110 | ) -> Result { 111 | let mut category = Category::default(); 112 | 113 | for attr in atts.with_checks(false).flatten() { 114 | if decode(attr.key.as_ref(), reader)?.as_ref() == "domain" { 115 | category.domain = Some(attr_value(&attr, reader)?.to_string()); 116 | break; 117 | } 118 | } 119 | 120 | category.name = element_text(reader)?.unwrap_or_default(); 121 | Ok(category) 122 | } 123 | } 124 | 125 | impl ToXml for Category { 126 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 127 | let name = "category"; 128 | let mut element = BytesStart::new(name); 129 | if let Some(ref domain) = self.domain { 130 | element.push_attribute(("domain", &**domain)); 131 | } 132 | writer.write_event(Event::Start(element))?; 133 | writer.write_event(Event::Text(BytesText::new(&self.name)))?; 134 | writer.write_event(Event::End(BytesEnd::new(name)))?; 135 | Ok(()) 136 | } 137 | } 138 | 139 | impl From for Category { 140 | fn from(name: String) -> Self { 141 | Self { name, domain: None } 142 | } 143 | } 144 | 145 | impl From<&str> for Category { 146 | fn from(name: &str) -> Self { 147 | Self { 148 | name: name.to_string(), 149 | domain: None, 150 | } 151 | } 152 | } 153 | 154 | #[cfg(feature = "builders")] 155 | impl CategoryBuilder { 156 | /// Builds a new `Category`. 157 | pub fn build(&self) -> Category { 158 | self.build_impl().unwrap() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/cloud.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::{BytesStart, Event}; 11 | use quick_xml::Error as XmlError; 12 | use quick_xml::Reader; 13 | use quick_xml::Writer; 14 | 15 | use crate::error::Error; 16 | use crate::toxml::ToXml; 17 | use crate::util::{attr_value, decode, skip}; 18 | 19 | /// Represents a cloud in an RSS feed. 20 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 21 | #[derive(Debug, Default, Clone, PartialEq)] 22 | #[cfg_attr(feature = "builders", derive(Builder))] 23 | #[cfg_attr( 24 | feature = "builders", 25 | builder( 26 | setter(into), 27 | default, 28 | build_fn(name = "build_impl", private, error = "never::Never") 29 | ) 30 | )] 31 | pub struct Cloud { 32 | /// The domain to register with. 33 | pub domain: String, 34 | /// The port to register with. 35 | pub port: String, 36 | /// The path to register with. 37 | pub path: String, 38 | /// The procedure to register with. 39 | pub register_procedure: String, 40 | /// The protocol to register with. 41 | pub protocol: String, 42 | } 43 | 44 | impl Cloud { 45 | /// Return the domain for this cloud. 46 | /// 47 | /// # Examples 48 | /// 49 | /// ``` 50 | /// use rss::Cloud; 51 | /// 52 | /// let mut cloud = Cloud::default(); 53 | /// cloud.set_domain("http://example.com"); 54 | /// assert_eq!(cloud.domain(), "http://example.com"); 55 | /// ``` 56 | pub fn domain(&self) -> &str { 57 | self.domain.as_str() 58 | } 59 | 60 | /// Set the domain for this cloud. 61 | /// 62 | /// # Examples 63 | /// 64 | /// ``` 65 | /// use rss::Cloud; 66 | /// 67 | /// let mut cloud = Cloud::default(); 68 | /// cloud.set_domain("http://example.com"); 69 | /// ``` 70 | pub fn set_domain(&mut self, domain: V) 71 | where 72 | V: Into, 73 | { 74 | self.domain = domain.into(); 75 | } 76 | 77 | /// Return the port for this cloud. 78 | /// 79 | /// # Examples 80 | /// 81 | /// ``` 82 | /// use rss::Cloud; 83 | /// 84 | /// let mut cloud = Cloud::default(); 85 | /// cloud.set_port("80"); 86 | /// assert_eq!(cloud.port(), "80"); 87 | /// ``` 88 | pub fn port(&self) -> &str { 89 | self.port.as_str() 90 | } 91 | 92 | /// Set the port for this cloud. 93 | /// 94 | /// # Examples 95 | /// 96 | /// ``` 97 | /// use rss::Cloud; 98 | /// 99 | /// let mut cloud = Cloud::default(); 100 | /// cloud.set_port("80"); 101 | /// ``` 102 | pub fn set_port(&mut self, port: V) 103 | where 104 | V: Into, 105 | { 106 | self.port = port.into(); 107 | } 108 | 109 | /// Return the path for this cloud. 110 | /// 111 | /// # Examples 112 | /// 113 | /// ``` 114 | /// use rss::Cloud; 115 | /// 116 | /// let mut cloud = Cloud::default(); 117 | /// cloud.set_port("/rpc"); 118 | /// assert_eq!(cloud.port(), "/rpc"); 119 | /// ``` 120 | pub fn path(&self) -> &str { 121 | self.path.as_str() 122 | } 123 | 124 | /// Set the path for this cloud. 125 | /// 126 | /// # Examples 127 | /// 128 | /// ``` 129 | /// use rss::Cloud; 130 | /// 131 | /// let mut cloud = Cloud::default(); 132 | /// cloud.set_path("/rpc"); 133 | /// ``` 134 | pub fn set_path(&mut self, path: V) 135 | where 136 | V: Into, 137 | { 138 | self.path = path.into(); 139 | } 140 | 141 | /// Return the register procedure for this cloud. 142 | /// 143 | /// # Examples 144 | /// 145 | /// ``` 146 | /// use rss::Cloud; 147 | /// 148 | /// let mut cloud = Cloud::default(); 149 | /// cloud.set_register_procedure("pingMe"); 150 | /// assert_eq!(cloud.register_procedure(), "pingMe"); 151 | /// ``` 152 | pub fn register_procedure(&self) -> &str { 153 | self.register_procedure.as_str() 154 | } 155 | 156 | /// Set the register procedure for this cloud. 157 | /// 158 | /// # Examples 159 | /// 160 | /// ``` 161 | /// use rss::Cloud; 162 | /// 163 | /// let mut cloud = Cloud::default(); 164 | /// cloud.set_register_procedure("pingMe"); 165 | /// ``` 166 | pub fn set_register_procedure(&mut self, register_procedure: V) 167 | where 168 | V: Into, 169 | { 170 | self.register_procedure = register_procedure.into(); 171 | } 172 | 173 | /// Return the protocol for this cloud. 174 | /// 175 | /// # Examples 176 | /// 177 | /// ``` 178 | /// use rss::Cloud; 179 | /// 180 | /// let mut cloud = Cloud::default(); 181 | /// cloud.set_protocol("xml-rpc"); 182 | /// assert_eq!(cloud.protocol(), "xml-rpc"); 183 | /// ``` 184 | pub fn protocol(&self) -> &str { 185 | self.protocol.as_str() 186 | } 187 | 188 | /// Set the protocol for this cloud. 189 | /// 190 | /// # Examples 191 | /// 192 | /// ``` 193 | /// use rss::Cloud; 194 | /// 195 | /// let mut cloud = Cloud::default(); 196 | /// cloud.set_protocol("xml-rpc"); 197 | /// ``` 198 | pub fn set_protocol(&mut self, protocol: V) 199 | where 200 | V: Into, 201 | { 202 | self.protocol = protocol.into(); 203 | } 204 | } 205 | 206 | impl Cloud { 207 | /// Builds a Cloud from source XML 208 | pub fn from_xml<'s, R: BufRead>( 209 | reader: &mut Reader, 210 | element: &'s BytesStart<'s>, 211 | ) -> Result { 212 | let mut cloud = Cloud::default(); 213 | 214 | for att in element.attributes().with_checks(false).flatten() { 215 | match decode(att.key.as_ref(), reader)?.as_ref() { 216 | "domain" => cloud.domain = attr_value(&att, reader)?.to_string(), 217 | "port" => cloud.port = attr_value(&att, reader)?.to_string(), 218 | "path" => cloud.path = attr_value(&att, reader)?.to_string(), 219 | "registerProcedure" => { 220 | cloud.register_procedure = attr_value(&att, reader)?.to_string() 221 | } 222 | "protocol" => cloud.protocol = attr_value(&att, reader)?.to_string(), 223 | _ => {} 224 | } 225 | } 226 | 227 | skip(element.name(), reader)?; 228 | 229 | Ok(cloud) 230 | } 231 | } 232 | 233 | impl ToXml for Cloud { 234 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 235 | let name = "cloud"; 236 | let mut element = BytesStart::new(name); 237 | 238 | element.push_attribute(("domain", self.domain.as_str())); 239 | element.push_attribute(("port", self.port.as_str())); 240 | element.push_attribute(("path", self.path.as_str())); 241 | element.push_attribute(("registerProcedure", self.register_procedure.as_str())); 242 | element.push_attribute(("protocol", self.protocol.as_str())); 243 | 244 | writer.write_event(Event::Empty(element))?; 245 | Ok(()) 246 | } 247 | } 248 | 249 | #[cfg(feature = "builders")] 250 | impl CloudBuilder { 251 | /// Builds a new `Cloud`. 252 | pub fn build(&self) -> Cloud { 253 | self.build_impl().unwrap() 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/enclosure.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::{BytesStart, Event}; 11 | use quick_xml::Error as XmlError; 12 | use quick_xml::Reader; 13 | use quick_xml::Writer; 14 | 15 | use crate::error::Error; 16 | use crate::toxml::ToXml; 17 | use crate::util::{attr_value, decode, skip}; 18 | 19 | /// Represents an enclosure in an RSS item. 20 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 21 | #[derive(Debug, Default, Clone, PartialEq)] 22 | #[cfg_attr(feature = "builders", derive(Builder))] 23 | #[cfg_attr( 24 | feature = "builders", 25 | builder( 26 | setter(into), 27 | default, 28 | build_fn(name = "build_impl", private, error = "never::Never") 29 | ) 30 | )] 31 | pub struct Enclosure { 32 | /// The URL of the enclosure. 33 | pub url: String, 34 | /// The length of the enclosure in bytes. 35 | pub length: String, 36 | /// The MIME type of the enclosure. 37 | pub mime_type: String, 38 | } 39 | 40 | impl Enclosure { 41 | /// Return the URL of this enclosure. 42 | /// 43 | /// # Examples 44 | /// 45 | /// ``` 46 | /// use rss::Enclosure; 47 | /// 48 | /// let mut enclosure = Enclosure::default(); 49 | /// enclosure.set_url("http://example.com/audio.mp3"); 50 | /// assert_eq!(enclosure.url(), "http://example.com/audio.mp3"); 51 | /// ``` 52 | pub fn url(&self) -> &str { 53 | self.url.as_str() 54 | } 55 | 56 | /// Set the URL of this enclosure. 57 | /// 58 | /// # Examples 59 | /// 60 | /// ``` 61 | /// use rss::Enclosure; 62 | /// 63 | /// let mut enclosure = Enclosure::default(); 64 | /// enclosure.set_url("http://example.com/audio.mp3"); 65 | /// ``` 66 | pub fn set_url(&mut self, url: V) 67 | where 68 | V: Into, 69 | { 70 | self.url = url.into(); 71 | } 72 | 73 | /// Return the content length of this enclosure. 74 | /// 75 | /// # Examples 76 | /// 77 | /// ``` 78 | /// use rss::Enclosure; 79 | /// 80 | /// let mut enclosure = Enclosure::default(); 81 | /// enclosure.set_length("1000"); 82 | /// assert_eq!(enclosure.length(), "1000"); 83 | /// ``` 84 | pub fn length(&self) -> &str { 85 | self.length.as_str() 86 | } 87 | 88 | /// Set the content length of this enclosure. 89 | /// 90 | /// # Examples 91 | /// 92 | /// ``` 93 | /// use rss::Enclosure; 94 | /// 95 | /// let mut enclosure = Enclosure::default(); 96 | /// enclosure.set_length("1000"); 97 | /// ``` 98 | pub fn set_length(&mut self, length: V) 99 | where 100 | V: Into, 101 | { 102 | self.length = length.into(); 103 | } 104 | 105 | /// Return the MIME type of this enclosure. 106 | /// 107 | /// # Examples 108 | /// 109 | /// ``` 110 | /// use rss::Enclosure; 111 | /// 112 | /// let mut enclosure = Enclosure::default(); 113 | /// enclosure.set_mime_type("audio/mpeg"); 114 | /// assert_eq!(enclosure.mime_type(), "audio/mpeg"); 115 | /// ``` 116 | pub fn mime_type(&self) -> &str { 117 | self.mime_type.as_str() 118 | } 119 | 120 | /// Set the MIME type of this enclosure. 121 | /// 122 | /// # Examples 123 | /// 124 | /// ``` 125 | /// use rss::Enclosure; 126 | /// 127 | /// let mut enclosure = Enclosure::default(); 128 | /// enclosure.set_mime_type("audio/mpeg"); 129 | /// ``` 130 | pub fn set_mime_type(&mut self, mime_type: V) 131 | where 132 | V: Into, 133 | { 134 | self.mime_type = mime_type.into(); 135 | } 136 | } 137 | 138 | impl Enclosure { 139 | /// Builds an Enclosure from source XML 140 | pub fn from_xml<'s, R: BufRead>( 141 | reader: &mut Reader, 142 | element: &'s BytesStart<'s>, 143 | ) -> Result { 144 | let mut enclosure = Enclosure::default(); 145 | for attr in element.attributes().with_checks(false).flatten() { 146 | match decode(attr.key.as_ref(), reader)?.as_ref() { 147 | "url" => enclosure.url = attr_value(&attr, reader)?.to_string(), 148 | "length" => enclosure.length = attr_value(&attr, reader)?.to_string(), 149 | "type" => enclosure.mime_type = attr_value(&attr, reader)?.to_string(), 150 | _ => {} 151 | } 152 | } 153 | skip(element.name(), reader)?; 154 | Ok(enclosure) 155 | } 156 | } 157 | 158 | impl ToXml for Enclosure { 159 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 160 | let name = "enclosure"; 161 | 162 | let mut element = BytesStart::new(name); 163 | 164 | element.push_attribute(("url", self.url.as_str())); 165 | element.push_attribute(("length", self.length.as_str())); 166 | element.push_attribute(("type", self.mime_type.as_str())); 167 | 168 | writer.write_event(Event::Empty(element))?; 169 | Ok(()) 170 | } 171 | } 172 | 173 | #[cfg(feature = "builders")] 174 | impl EnclosureBuilder { 175 | /// Builds a new `Enclosure`. 176 | pub fn build(&self) -> Enclosure { 177 | self.build_impl().unwrap() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::error::Error as StdError; 9 | use std::fmt; 10 | use std::io; 11 | use std::str::Utf8Error; 12 | use std::sync::Arc; 13 | 14 | use quick_xml::Error as XmlError; 15 | 16 | #[derive(Debug)] 17 | /// Errors that occur during parsing. 18 | pub enum Error { 19 | /// An error while converting bytes to UTF8. 20 | Utf8(Utf8Error), 21 | /// An XML parsing error. 22 | Xml(XmlError), 23 | /// The input didn't begin with an opening `` tag. 24 | InvalidStartTag, 25 | /// The end of the input was reached without finding a complete channel element. 26 | Eof, 27 | } 28 | 29 | impl StdError for Error { 30 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 31 | match *self { 32 | Error::Utf8(ref err) => Some(err), 33 | Error::Xml(ref err) => Some(err), 34 | Error::InvalidStartTag | Error::Eof => None, 35 | } 36 | } 37 | } 38 | 39 | impl fmt::Display for Error { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | match *self { 42 | Error::Utf8(ref err) => fmt::Display::fmt(err, f), 43 | Error::Xml(ref err) => fmt::Display::fmt(err, f), 44 | Error::InvalidStartTag => write!(f, "the input did not begin with an rss tag"), 45 | Error::Eof => write!(f, "reached end of input without finding a complete channel"), 46 | } 47 | } 48 | } 49 | 50 | impl From for Error { 51 | fn from(err: XmlError) -> Error { 52 | Error::Xml(err) 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(err: quick_xml::encoding::EncodingError) -> Error { 58 | Error::Xml(XmlError::Encoding(err)) 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(err: io::Error) -> Error { 64 | Error::Xml(XmlError::Io(Arc::new(err))) 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(err: Utf8Error) -> Error { 70 | Error::Utf8(err) 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod test { 76 | use super::*; 77 | 78 | #[test] 79 | fn error_send_and_sync() { 80 | fn assert_send_sync() {} 81 | assert_send_sync::(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/extension/atom.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2020 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::Write; 10 | 11 | pub use atom_syndication::Link; 12 | use quick_xml::events::{BytesStart, Event}; 13 | use quick_xml::Error as XmlError; 14 | use quick_xml::Writer; 15 | 16 | use crate::extension::Extension; 17 | use crate::toxml::ToXml; 18 | 19 | /// The Atom XML namespace. 20 | pub const NAMESPACE: &str = "http://www.w3.org/2005/Atom"; 21 | 22 | /// An Atom element extension. 23 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 24 | #[derive(Default, Debug, Clone, PartialEq)] 25 | #[cfg_attr(feature = "builders", derive(Builder))] 26 | #[cfg_attr( 27 | feature = "builders", 28 | builder( 29 | setter(into), 30 | default, 31 | build_fn(name = "build_impl", private, error = "never::Never") 32 | ) 33 | )] 34 | pub struct AtomExtension { 35 | /// Links 36 | #[cfg_attr(feature = "builders", builder(setter(each = "link")))] 37 | pub links: Vec, 38 | } 39 | 40 | impl AtomExtension { 41 | /// Retrieve links 42 | pub fn links(&self) -> &[Link] { 43 | &self.links 44 | } 45 | 46 | /// Set links 47 | pub fn set_links(&mut self, links: V) 48 | where 49 | V: Into>, 50 | { 51 | self.links = links.into(); 52 | } 53 | } 54 | 55 | impl AtomExtension { 56 | /// Creates an `AtomExtension` using the specified `BTreeMap`. 57 | pub fn from_map(mut map: BTreeMap>) -> Self { 58 | let links = map 59 | .remove("link") 60 | .unwrap_or_default() 61 | .into_iter() 62 | .filter_map(|mut link_ext| { 63 | Some(Link { 64 | href: link_ext.attrs.remove("href")?, 65 | rel: link_ext 66 | .attrs 67 | .remove("rel") 68 | .unwrap_or_else(|| Link::default().rel), 69 | hreflang: link_ext.attrs.remove("hreflang"), 70 | mime_type: link_ext.attrs.remove("type"), 71 | title: link_ext.attrs.remove("title"), 72 | length: link_ext.attrs.remove("length"), 73 | }) 74 | }) 75 | .collect(); 76 | 77 | Self { links } 78 | } 79 | } 80 | 81 | impl ToXml for AtomExtension { 82 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 83 | for link in &self.links { 84 | let mut element = BytesStart::new("atom:link"); 85 | element.push_attribute(("href", &*link.href)); 86 | element.push_attribute(("rel", &*link.rel)); 87 | 88 | if let Some(ref hreflang) = link.hreflang { 89 | element.push_attribute(("hreflang", &**hreflang)); 90 | } 91 | 92 | if let Some(ref mime_type) = link.mime_type { 93 | element.push_attribute(("type", &**mime_type)); 94 | } 95 | 96 | if let Some(ref title) = link.title { 97 | element.push_attribute(("title", &**title)); 98 | } 99 | 100 | if let Some(ref length) = link.length { 101 | element.push_attribute(("length", &**length)); 102 | } 103 | 104 | writer.write_event(Event::Empty(element))?; 105 | } 106 | Ok(()) 107 | } 108 | 109 | fn used_namespaces(&self) -> BTreeMap { 110 | let mut namespaces = BTreeMap::new(); 111 | namespaces.insert("atom".to_owned(), NAMESPACE.to_owned()); 112 | namespaces 113 | } 114 | } 115 | 116 | #[cfg(feature = "builders")] 117 | impl AtomExtensionBuilder { 118 | /// Builds a new `AtomExtension`. 119 | pub fn build(&self) -> AtomExtension { 120 | self.build_impl().unwrap() 121 | } 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | 128 | #[test] 129 | #[cfg(feature = "builders")] 130 | #[cfg(feature = "atom")] 131 | fn test_builder() { 132 | use atom_syndication::LinkBuilder; 133 | assert_eq!( 134 | AtomExtensionBuilder::default() 135 | .link( 136 | LinkBuilder::default() 137 | .rel("self") 138 | .href("http://example.com/feed") 139 | .build(), 140 | ) 141 | .link( 142 | LinkBuilder::default() 143 | .rel("alternate") 144 | .href("http://example.com") 145 | .build(), 146 | ) 147 | .build(), 148 | AtomExtension { 149 | links: vec![ 150 | Link { 151 | rel: "self".to_string(), 152 | href: "http://example.com/feed".to_string(), 153 | ..Default::default() 154 | }, 155 | Link { 156 | rel: "alternate".to_string(), 157 | href: "http://example.com".to_string(), 158 | ..Default::default() 159 | } 160 | ] 161 | } 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/extension/dublincore.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::Write; 10 | 11 | use quick_xml::Error as XmlError; 12 | use quick_xml::Writer; 13 | 14 | use crate::extension::util::get_extension_values; 15 | use crate::extension::Extension; 16 | 17 | use crate::toxml::{ToXml, WriterExt}; 18 | 19 | /// The Dublin Core XML namespace. 20 | pub const NAMESPACE: &str = "http://purl.org/dc/elements/1.1/"; 21 | 22 | /// A Dublin Core element extension. 23 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 24 | #[derive(Debug, Default, Clone, PartialEq)] 25 | #[cfg_attr(feature = "builders", derive(Builder))] 26 | #[cfg_attr( 27 | feature = "builders", 28 | builder( 29 | setter(into), 30 | default, 31 | build_fn(name = "build_impl", private, error = "never::Never") 32 | ) 33 | )] 34 | pub struct DublinCoreExtension { 35 | /// An entity responsible for making contributions to the resource. 36 | #[cfg_attr(feature = "builders", builder(setter(each = "contributor")))] 37 | pub contributors: Vec, 38 | /// The spatial or temporal topic of the resource, the spatial applicability of the resource, 39 | /// or the jurisdiction under which the resource is relevant. 40 | #[cfg_attr(feature = "builders", builder(setter(each = "coverage")))] 41 | pub coverages: Vec, 42 | /// An entity primarily responsible for making the resource. 43 | #[cfg_attr(feature = "builders", builder(setter(each = "creator")))] 44 | pub creators: Vec, 45 | /// A point or period of time associated with an event in the lifecycle of the resource. 46 | #[cfg_attr(feature = "builders", builder(setter(each = "date")))] 47 | pub dates: Vec, 48 | /// An account of the resource. 49 | #[cfg_attr(feature = "builders", builder(setter(each = "description")))] 50 | pub descriptions: Vec, 51 | /// The file format, physical medium, or dimensions of the resource. 52 | #[cfg_attr(feature = "builders", builder(setter(each = "format")))] 53 | pub formats: Vec, 54 | /// An unambiguous reference to the resource within a given context. 55 | #[cfg_attr(feature = "builders", builder(setter(each = "identifier")))] 56 | pub identifiers: Vec, 57 | /// A language of the resource. 58 | #[cfg_attr(feature = "builders", builder(setter(each = "language")))] 59 | pub languages: Vec, 60 | /// An entity responsible for making the resource available. 61 | #[cfg_attr(feature = "builders", builder(setter(each = "publisher")))] 62 | pub publishers: Vec, 63 | /// A related resource. 64 | #[cfg_attr(feature = "builders", builder(setter(each = "relation")))] 65 | pub relations: Vec, 66 | /// Information about rights held in and over the resource. 67 | #[cfg_attr(feature = "builders", builder(setter(each = "right")))] 68 | pub rights: Vec, 69 | /// A related resource from which the described resource is derived. 70 | #[cfg_attr(feature = "builders", builder(setter(each = "source")))] 71 | pub sources: Vec, 72 | /// The topic of the resource. 73 | #[cfg_attr(feature = "builders", builder(setter(each = "subject")))] 74 | pub subjects: Vec, 75 | /// A name given to the resource. 76 | #[cfg_attr(feature = "builders", builder(setter(each = "title")))] 77 | pub titles: Vec, 78 | /// The nature or genre of the resource. 79 | #[cfg_attr(feature = "builders", builder(setter(each = "r#type")))] 80 | pub types: Vec, 81 | } 82 | 83 | impl DublinCoreExtension { 84 | /// Return the contributors to the resource. 85 | pub fn contributors(&self) -> &[String] { 86 | &self.contributors 87 | } 88 | 89 | /// Return a mutable slice of the contributors to the resource. 90 | pub fn contributors_mut(&mut self) -> &mut [String] { 91 | &mut self.contributors 92 | } 93 | 94 | /// Set the contributors to the resource. 95 | pub fn set_contributors(&mut self, contributors: V) 96 | where 97 | V: Into>, 98 | { 99 | self.contributors = contributors.into(); 100 | } 101 | 102 | /// Return the spatial or temporal topics of the resource, the spatial applicabilities of the 103 | /// resource, or the jurisdictions under which the resource is relevant. 104 | pub fn coverages(&self) -> &[String] { 105 | &self.coverages 106 | } 107 | 108 | /// Return a mutable slice of the spatial or temporal topics of the resource, the spatial 109 | /// applicabilities of the resource, or the jurisdictions under which the resource is relevant. 110 | pub fn coverages_mut(&mut self) -> &mut [String] { 111 | &mut self.coverages 112 | } 113 | 114 | /// Set the spatial or temporal topics of the resource, the spatial applicabilities of the 115 | /// resource, or the jurisdictions under which the resource is relevant. 116 | pub fn set_coverages(&mut self, coverages: V) 117 | where 118 | V: Into>, 119 | { 120 | self.coverages = coverages.into(); 121 | } 122 | 123 | /// Return the creators of the resource. 124 | pub fn creators(&self) -> &[String] { 125 | &self.creators 126 | } 127 | 128 | /// Return a mutable slice of the creators of the resource. 129 | pub fn creators_mut(&mut self) -> &mut [String] { 130 | &mut self.creators 131 | } 132 | 133 | /// Set the creators of the resource. 134 | pub fn set_creators(&mut self, creators: V) 135 | where 136 | V: Into>, 137 | { 138 | self.creators = creators.into(); 139 | } 140 | 141 | /// Return the times associated with the resource. 142 | pub fn dates(&self) -> &[String] { 143 | &self.dates 144 | } 145 | 146 | /// Return a mutable slice of the times associated with the resource. 147 | pub fn dates_mut(&mut self) -> &mut [String] { 148 | &mut self.dates 149 | } 150 | 151 | /// Set the times associated with the resource. 152 | pub fn set_dates(&mut self, dates: V) 153 | where 154 | V: Into>, 155 | { 156 | self.dates = dates.into(); 157 | } 158 | 159 | /// Return the descriptions of the resource. 160 | pub fn descriptions(&self) -> &[String] { 161 | &self.descriptions 162 | } 163 | 164 | /// Return a mutable slice of the descriptions of the resource. 165 | pub fn descriptions_mut(&mut self) -> &mut [String] { 166 | &mut self.descriptions 167 | } 168 | 169 | /// Set the descriptions of the resource. 170 | pub fn set_descriptions(&mut self, descriptions: V) 171 | where 172 | V: Into>, 173 | { 174 | self.descriptions = descriptions.into(); 175 | } 176 | 177 | /// Return the file formats, physical mediums, or dimensions of the resource. 178 | pub fn formats(&self) -> &[String] { 179 | &self.formats 180 | } 181 | 182 | /// Return a mutable slice of the file formats, physical mediums, or 183 | /// dimensions of the resource. 184 | pub fn formats_mut(&mut self) -> &mut [String] { 185 | &mut self.formats 186 | } 187 | 188 | /// Set the file formats, physical mediums, or dimensions of the resource. 189 | pub fn set_formats(&mut self, formats: V) 190 | where 191 | V: Into>, 192 | { 193 | self.formats = formats.into(); 194 | } 195 | 196 | /// Return the identifiers of the resource. 197 | pub fn identifiers(&self) -> &[String] { 198 | &self.identifiers 199 | } 200 | 201 | /// Return a mutable slice of the identifiers of the resource. 202 | pub fn identifiers_mut(&mut self) -> &mut [String] { 203 | &mut self.identifiers 204 | } 205 | 206 | /// Set the identifiers of the resource. 207 | pub fn set_identifiers(&mut self, identifiers: V) 208 | where 209 | V: Into>, 210 | { 211 | self.identifiers = identifiers.into(); 212 | } 213 | 214 | /// Return the languages of the resource. 215 | pub fn languages(&self) -> &[String] { 216 | &self.languages 217 | } 218 | 219 | /// Return a mutable slice of the languages of the resource. 220 | pub fn languages_mut(&mut self) -> &mut [String] { 221 | &mut self.languages 222 | } 223 | 224 | /// Set the languages of the resource. 225 | pub fn set_languages(&mut self, languages: V) 226 | where 227 | V: Into>, 228 | { 229 | self.languages = languages.into(); 230 | } 231 | 232 | /// Return the publishers of the resource. 233 | pub fn publishers(&self) -> &[String] { 234 | &self.publishers 235 | } 236 | 237 | /// Return a mutable slice of the publishers of the resource. 238 | pub fn publishers_mut(&mut self) -> &mut [String] { 239 | &mut self.publishers 240 | } 241 | 242 | /// Set the publishers of the resource. 243 | pub fn set_publishers(&mut self, publishers: V) 244 | where 245 | V: Into>, 246 | { 247 | self.publishers = publishers.into(); 248 | } 249 | 250 | /// Return the related resources. 251 | pub fn relations(&self) -> &[String] { 252 | &self.relations 253 | } 254 | 255 | /// Return a mutable slice of the related resources. 256 | pub fn relations_mut(&mut self) -> &mut [String] { 257 | &mut self.relations 258 | } 259 | 260 | /// Set the related resources. 261 | pub fn set_relations(&mut self, relations: V) 262 | where 263 | V: Into>, 264 | { 265 | self.relations = relations.into(); 266 | } 267 | 268 | /// Return the information about rights held in and over the resource. 269 | pub fn rights(&self) -> &[String] { 270 | &self.rights 271 | } 272 | 273 | /// Return a mutable slice of the information about rights held in and over 274 | /// the resource. 275 | pub fn rights_mut(&mut self) -> &mut [String] { 276 | &mut self.rights 277 | } 278 | 279 | /// Set the information about rights held in and over the resource. 280 | pub fn set_rights(&mut self, rights: V) 281 | where 282 | V: Into>, 283 | { 284 | self.rights = rights.into(); 285 | } 286 | 287 | /// Return the sources of the resource. 288 | pub fn sources(&self) -> &[String] { 289 | &self.sources 290 | } 291 | 292 | /// Return a mutable slice of the sources of the resource. 293 | pub fn sources_mut(&mut self) -> &mut [String] { 294 | &mut self.sources 295 | } 296 | 297 | /// Set the sources of the resource. 298 | pub fn set_sources(&mut self, sources: V) 299 | where 300 | V: Into>, 301 | { 302 | self.sources = sources.into(); 303 | } 304 | 305 | /// Return the topics of the resource. 306 | pub fn subjects(&self) -> &[String] { 307 | &self.subjects 308 | } 309 | 310 | /// Return a mutable slice of the subjects of the resource. 311 | pub fn subjects_mut(&mut self) -> &mut [String] { 312 | &mut self.subjects 313 | } 314 | 315 | /// Set the topics of the resource. 316 | pub fn set_subjects(&mut self, subjects: V) 317 | where 318 | V: Into>, 319 | { 320 | self.subjects = subjects.into(); 321 | } 322 | 323 | /// Return the titles of the resource. 324 | pub fn titles(&self) -> &[String] { 325 | &self.titles 326 | } 327 | 328 | /// Return a mutable slice of the titles of the resource. 329 | pub fn titles_mut(&mut self) -> &mut [String] { 330 | &mut self.titles 331 | } 332 | 333 | /// Set the titles of the resource. 334 | pub fn set_titles(&mut self, titles: V) 335 | where 336 | V: Into>, 337 | { 338 | self.titles = titles.into(); 339 | } 340 | 341 | /// Return the natures or genres of the resource. 342 | pub fn types(&self) -> &[String] { 343 | &self.types 344 | } 345 | 346 | /// Return a mutable slice of the natures or genres of the resource. 347 | pub fn types_mut(&mut self) -> &mut [String] { 348 | &mut self.types 349 | } 350 | 351 | /// set the natures or genres of the resource. 352 | pub fn set_types(&mut self, types: V) 353 | where 354 | V: Into>, 355 | { 356 | self.types = types.into(); 357 | } 358 | } 359 | 360 | impl DublinCoreExtension { 361 | /// Creates a `DublinCoreExtension` using the specified `BTreeMap`. 362 | pub fn from_map(map: BTreeMap>) -> Self { 363 | let mut ext = DublinCoreExtension::default(); 364 | for (key, v) in map { 365 | match key.as_str() { 366 | "contributor" => ext.contributors = get_extension_values(v), 367 | "coverage" => ext.coverages = get_extension_values(v), 368 | "creator" => ext.creators = get_extension_values(v), 369 | "date" => ext.dates = get_extension_values(v), 370 | "description" => ext.descriptions = get_extension_values(v), 371 | "format" => ext.formats = get_extension_values(v), 372 | "identifier" => ext.identifiers = get_extension_values(v), 373 | "language" => ext.languages = get_extension_values(v), 374 | "publisher" => ext.publishers = get_extension_values(v), 375 | "relation" => ext.relations = get_extension_values(v), 376 | "rights" => ext.rights = get_extension_values(v), 377 | "source" => ext.sources = get_extension_values(v), 378 | "subject" => ext.subjects = get_extension_values(v), 379 | "title" => ext.titles = get_extension_values(v), 380 | "type" => ext.types = get_extension_values(v), 381 | _ => {} 382 | } 383 | } 384 | ext 385 | } 386 | } 387 | 388 | impl ToXml for DublinCoreExtension { 389 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 390 | writer.write_text_elements("dc:contributor", &self.contributors)?; 391 | writer.write_text_elements("dc:coverage", &self.coverages)?; 392 | writer.write_text_elements("dc:creator", &self.creators)?; 393 | writer.write_text_elements("dc:date", &self.dates)?; 394 | writer.write_text_elements("dc:description", &self.descriptions)?; 395 | writer.write_text_elements("dc:format", &self.formats)?; 396 | writer.write_text_elements("dc:identifier", &self.identifiers)?; 397 | writer.write_text_elements("dc:language", &self.languages)?; 398 | writer.write_text_elements("dc:publisher", &self.publishers)?; 399 | writer.write_text_elements("dc:relation", &self.relations)?; 400 | writer.write_text_elements("dc:rights", &self.rights)?; 401 | writer.write_text_elements("dc:source", &self.sources)?; 402 | writer.write_text_elements("dc:subject", &self.subjects)?; 403 | writer.write_text_elements("dc:title", &self.titles)?; 404 | writer.write_text_elements("dc:type", &self.types)?; 405 | Ok(()) 406 | } 407 | 408 | fn used_namespaces(&self) -> BTreeMap { 409 | let mut namespaces = BTreeMap::new(); 410 | namespaces.insert("dc".to_owned(), NAMESPACE.to_owned()); 411 | namespaces 412 | } 413 | } 414 | 415 | #[cfg(feature = "builders")] 416 | impl DublinCoreExtensionBuilder { 417 | /// Builds a new `DublinCoreExtension`. 418 | pub fn build(&self) -> DublinCoreExtension { 419 | self.build_impl().unwrap() 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/extension/itunes/itunes_category.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::Write; 9 | 10 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 11 | use quick_xml::Error as XmlError; 12 | use quick_xml::Writer; 13 | 14 | use crate::toxml::ToXml; 15 | 16 | /// A category for an iTunes podcast. 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | #[derive(Debug, Default, Clone, PartialEq)] 19 | #[cfg_attr(feature = "builders", derive(Builder))] 20 | #[cfg_attr( 21 | feature = "builders", 22 | builder( 23 | setter(into), 24 | default, 25 | build_fn(name = "build_impl", private, error = "never::Never") 26 | ) 27 | )] 28 | pub struct ITunesCategory { 29 | /// The name of the category. 30 | pub text: String, 31 | // This is contained within a Box to ensure it gets allocated on the heap to prevent an 32 | // infinite size. 33 | /// An optional subcategory for the category. 34 | pub subcategory: Option>, 35 | } 36 | 37 | impl ITunesCategory { 38 | /// Return the name of this category. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ``` 43 | /// use rss::extension::itunes::ITunesCategory; 44 | /// 45 | /// let mut category = ITunesCategory::default(); 46 | /// category.set_text("Technology"); 47 | /// assert_eq!(category.text(), "Technology") 48 | /// ``` 49 | pub fn text(&self) -> &str { 50 | self.text.as_str() 51 | } 52 | /// Set the name of this category. 53 | /// 54 | /// # Examples 55 | /// 56 | /// ``` 57 | /// use rss::extension::itunes::ITunesCategory; 58 | /// 59 | /// let mut category = ITunesCategory::default(); 60 | /// category.set_text("Technology"); 61 | /// ``` 62 | pub fn set_text(&mut self, text: V) 63 | where 64 | V: Into, 65 | { 66 | self.text = text.into(); 67 | } 68 | 69 | /// Return the subcategory for this category. 70 | /// 71 | /// # Examples 72 | /// 73 | /// ``` 74 | /// use rss::extension::itunes::ITunesCategory; 75 | /// 76 | /// let mut category = ITunesCategory::default(); 77 | /// category.set_subcategory(Box::new(ITunesCategory::default())); 78 | /// assert!(category.subcategory().is_some()); 79 | /// ``` 80 | pub fn subcategory(&self) -> Option<&ITunesCategory> { 81 | self.subcategory.as_deref() 82 | } 83 | 84 | /// Set the subcategory for this category. 85 | /// 86 | /// # Examples 87 | /// 88 | /// ``` 89 | /// use rss::extension::itunes::ITunesCategory; 90 | /// 91 | /// let mut category = ITunesCategory::default(); 92 | /// category.set_subcategory(Box::new(ITunesCategory::default())); 93 | /// ``` 94 | pub fn set_subcategory(&mut self, subcategory: V) 95 | where 96 | V: Into>>, 97 | { 98 | self.subcategory = subcategory.into(); 99 | } 100 | } 101 | 102 | impl ToXml for ITunesCategory { 103 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 104 | let name = "itunes:category"; 105 | let mut element = BytesStart::new(name); 106 | element.push_attribute(("text", &*self.text)); 107 | writer.write_event(Event::Start(element))?; 108 | 109 | if let Some(subcategory) = self.subcategory.as_ref() { 110 | subcategory.to_xml(writer)?; 111 | } 112 | 113 | writer.write_event(Event::End(BytesEnd::new(name)))?; 114 | Ok(()) 115 | } 116 | } 117 | 118 | #[cfg(feature = "builders")] 119 | impl ITunesCategoryBuilder { 120 | /// Builds a new `ITunesCategory`. 121 | pub fn build(&self) -> ITunesCategory { 122 | self.build_impl().unwrap() 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | 130 | #[test] 131 | #[cfg(feature = "builders")] 132 | fn test_builder() { 133 | assert_eq!( 134 | ITunesCategoryBuilder::default().text("music").build(), 135 | ITunesCategory { 136 | text: "music".to_string(), 137 | subcategory: None, 138 | } 139 | ); 140 | assert_eq!( 141 | ITunesCategoryBuilder::default() 142 | .text("music") 143 | .subcategory(Some(Box::new( 144 | ITunesCategoryBuilder::default().text("pop").build() 145 | ))) 146 | .build(), 147 | ITunesCategory { 148 | text: "music".to_string(), 149 | subcategory: Some(Box::new(ITunesCategory { 150 | text: "pop".to_string(), 151 | subcategory: None, 152 | })), 153 | } 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/extension/itunes/itunes_channel_extension.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::Write; 10 | 11 | use quick_xml::events::{BytesStart, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Writer; 14 | 15 | use super::{parse_categories, parse_image, parse_owner, NAMESPACE}; 16 | use crate::extension::itunes::{ITunesCategory, ITunesOwner}; 17 | use crate::extension::util::remove_extension_value; 18 | use crate::extension::Extension; 19 | use crate::toxml::{ToXml, WriterExt}; 20 | 21 | /// An iTunes channel element extension. 22 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 23 | #[derive(Debug, Default, Clone, PartialEq)] 24 | #[cfg_attr(feature = "builders", derive(Builder))] 25 | #[cfg_attr( 26 | feature = "builders", 27 | builder( 28 | setter(into), 29 | default, 30 | build_fn(name = "build_impl", private, error = "never::Never") 31 | ) 32 | )] 33 | pub struct ITunesChannelExtension { 34 | /// The author of the podcast. 35 | pub author: Option, 36 | /// Specifies if the podcast should be prevented from appearing in the iTunes Store. A value of 37 | /// `Yes` indicates that the podcast should not show up in the iTunes Store. All other values 38 | /// are ignored. 39 | pub block: Option, 40 | /// The iTunes categories the podcast belongs to. 41 | #[cfg_attr(feature = "builders", builder(setter(each = "category")))] 42 | pub categories: Vec, 43 | /// The artwork for the podcast. 44 | pub image: Option, 45 | /// Specifies whether the podcast contains explicit content. A value of `Yes`, `Explicit`, or 46 | /// `True` indicates that the podcast contains explicit content. A value of `Clean`, `No`, 47 | /// `False` indicates that none of the episodes contain explicit content. 48 | pub explicit: Option, 49 | /// Specifies whether the podcast is complete and no new episodes will be posted. A value of 50 | /// `Yes` indicates that the podcast is complete. 51 | pub complete: Option, 52 | /// The new URL where the podcast is located. 53 | pub new_feed_url: Option, 54 | /// The contact information for the owner of the podcast. 55 | pub owner: Option, 56 | /// A description of the podcast. 57 | pub subtitle: Option, 58 | /// A summary of the podcast. 59 | pub summary: Option, 60 | /// Keywords for the podcast. The string contains a comma separated list of keywords. 61 | pub keywords: Option, 62 | /// The type of the podcast. Usually `serial` or `episodic`. 63 | pub r#type: Option, 64 | } 65 | 66 | impl ITunesChannelExtension { 67 | /// Return the author of this podcast. 68 | /// 69 | /// # Examples 70 | /// 71 | /// ``` 72 | /// use rss::extension::itunes::ITunesChannelExtension; 73 | /// 74 | /// let mut extension = ITunesChannelExtension::default(); 75 | /// extension.set_author("John Doe".to_string()); 76 | /// assert_eq!(extension.author(), Some("John Doe")); 77 | /// ``` 78 | pub fn author(&self) -> Option<&str> { 79 | self.author.as_deref() 80 | } 81 | 82 | /// Set the author of this podcast. 83 | /// 84 | /// # Examples 85 | /// 86 | /// ``` 87 | /// use rss::extension::itunes::ITunesChannelExtension; 88 | /// 89 | /// let mut extension = ITunesChannelExtension::default(); 90 | /// extension.set_author("John Doe".to_string()); 91 | /// ``` 92 | pub fn set_author(&mut self, author: V) 93 | where 94 | V: Into>, 95 | { 96 | self.author = author.into(); 97 | } 98 | 99 | /// Return whether the podcast should be blocked from appearing in the iTunes Store. 100 | /// 101 | /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All 102 | /// other values are ignored. 103 | /// 104 | /// # Examples 105 | /// 106 | /// ``` 107 | /// use rss::extension::itunes::ITunesChannelExtension; 108 | /// 109 | /// let mut extension = ITunesChannelExtension::default(); 110 | /// extension.set_block("Yes".to_string()); 111 | /// assert_eq!(extension.block(), Some("Yes")); 112 | /// ``` 113 | pub fn block(&self) -> Option<&str> { 114 | self.block.as_deref() 115 | } 116 | 117 | /// Set whether the podcast should be blocked from appearing in the iTunes Store. 118 | /// 119 | /// A value of `Yes` indicates that the podcast should not show up in the iTunes Store. All 120 | /// other values are ignored. 121 | /// 122 | /// # Examples 123 | /// 124 | /// ``` 125 | /// use rss::extension::itunes::ITunesChannelExtension; 126 | /// 127 | /// let mut extension = ITunesChannelExtension::default(); 128 | /// extension.set_block("Yes".to_string()); 129 | /// ``` 130 | pub fn set_block(&mut self, block: V) 131 | where 132 | V: Into>, 133 | { 134 | self.block = block.into(); 135 | } 136 | 137 | /// Return the iTunes categories that the podcast belongs to. 138 | /// 139 | /// # Examples 140 | /// 141 | /// ``` 142 | /// use rss::extension::itunes::{ITunesCategory, ITunesChannelExtension}; 143 | /// 144 | /// let mut extension = ITunesChannelExtension::default(); 145 | /// extension.set_categories(vec![ITunesCategory::default()]); 146 | /// assert_eq!(extension.categories().len(), 1); 147 | /// ``` 148 | pub fn categories(&self) -> &[ITunesCategory] { 149 | &self.categories 150 | } 151 | 152 | /// Return a mutable slice of the iTunes categories that the podcast belongs to. 153 | pub fn categories_mut(&mut self) -> &mut [ITunesCategory] { 154 | &mut self.categories 155 | } 156 | 157 | /// Set the iTunes categories that the podcast belongs to. 158 | /// 159 | /// # Examples 160 | /// 161 | /// ``` 162 | /// use rss::extension::itunes::{ITunesCategory, ITunesChannelExtension}; 163 | /// 164 | /// let mut extension = ITunesChannelExtension::default(); 165 | /// extension.set_categories(vec![ITunesCategory::default()]); 166 | /// ``` 167 | pub fn set_categories(&mut self, categories: V) 168 | where 169 | V: Into>, 170 | { 171 | self.categories = categories.into(); 172 | } 173 | 174 | /// Return the artwork URL for the podcast. 175 | /// 176 | /// # Examples 177 | /// 178 | /// ``` 179 | /// use rss::extension::itunes::ITunesChannelExtension; 180 | /// 181 | /// let mut extension = ITunesChannelExtension::default(); 182 | /// extension.set_image("http://example.com/artwork.png".to_string()); 183 | /// assert_eq!(extension.image(), Some("http://example.com/artwork.png")); 184 | /// ``` 185 | pub fn image(&self) -> Option<&str> { 186 | self.image.as_deref() 187 | } 188 | 189 | /// Set the artwork URL for the podcast. 190 | /// 191 | /// # Examples 192 | /// 193 | /// ``` 194 | /// use rss::extension::itunes::ITunesChannelExtension; 195 | /// 196 | /// let mut extension = ITunesChannelExtension::default(); 197 | /// extension.set_image("http://example.com/artwork.png".to_string()); 198 | /// ``` 199 | pub fn set_image(&mut self, image: V) 200 | where 201 | V: Into>, 202 | { 203 | self.image = image.into(); 204 | } 205 | 206 | /// Return whether the podcast contains explicit content. 207 | /// 208 | /// A value of `Yes`, `Explicit`, or `True` indicates that the podcast contains explicit 209 | /// content. A value of `Clean`, `No`, `False` indicates that none of the episodes contain 210 | /// explicit content. 211 | /// 212 | /// # Examples 213 | /// 214 | /// ``` 215 | /// use rss::extension::itunes::ITunesChannelExtension; 216 | /// 217 | /// let mut extension = ITunesChannelExtension::default(); 218 | /// extension.set_explicit("Yes".to_string()); 219 | /// assert_eq!(extension.explicit(), Some("Yes")); 220 | /// ``` 221 | pub fn explicit(&self) -> Option<&str> { 222 | self.explicit.as_deref() 223 | } 224 | 225 | /// Set whether the podcast contains explicit content. 226 | /// 227 | /// A value of `Yes`, `Explicit`, or `True` indicates that the podcast contains explicit 228 | /// content. A value of `Clean`, `No`, `False` indicates that none of the episodes contain 229 | /// explicit content. 230 | /// 231 | /// # Examples 232 | /// 233 | /// ``` 234 | /// use rss::extension::itunes::ITunesChannelExtension; 235 | /// 236 | /// let mut extension = ITunesChannelExtension::default(); 237 | /// extension.set_explicit("Yes".to_string()); 238 | /// ``` 239 | pub fn set_explicit(&mut self, explicit: V) 240 | where 241 | V: Into>, 242 | { 243 | self.explicit = explicit.into(); 244 | } 245 | 246 | /// Return whether the podcast is complete and no new episodes will be posted. 247 | /// 248 | /// A value of `Yes` indicates that the podcast is complete. 249 | /// 250 | /// # Examples 251 | /// 252 | /// ``` 253 | /// use rss::extension::itunes::ITunesChannelExtension; 254 | /// 255 | /// let mut extension = ITunesChannelExtension::default(); 256 | /// extension.set_complete("Yes".to_string()); 257 | /// assert_eq!(extension.complete(), Some("Yes")); 258 | /// ``` 259 | pub fn complete(&self) -> Option<&str> { 260 | self.complete.as_deref() 261 | } 262 | 263 | /// Set whether the podcast is complete and no new episodes will be posted. 264 | /// 265 | /// A value of `Yes` indicates that the podcast is complete. 266 | /// 267 | /// # Examples 268 | /// 269 | /// ``` 270 | /// use rss::extension::itunes::ITunesChannelExtension; 271 | /// 272 | /// let mut extension = ITunesChannelExtension::default(); 273 | /// extension.set_complete("Yes".to_string()); 274 | /// ``` 275 | pub fn set_complete(&mut self, complete: V) 276 | where 277 | V: Into>, 278 | { 279 | self.complete = complete.into(); 280 | } 281 | 282 | /// Return the new feed URL for this podcast. 283 | /// 284 | /// # Examples 285 | /// 286 | /// ``` 287 | /// use rss::extension::itunes::ITunesChannelExtension; 288 | /// 289 | /// let mut extension = ITunesChannelExtension::default(); 290 | /// extension.set_new_feed_url("http://example.com/feed".to_string()); 291 | /// assert_eq!(extension.new_feed_url(), Some("http://example.com/feed")); 292 | /// ``` 293 | pub fn new_feed_url(&self) -> Option<&str> { 294 | self.new_feed_url.as_deref() 295 | } 296 | 297 | /// Set the new feed URL for this podcast. 298 | /// 299 | /// # Examples 300 | /// 301 | /// ``` 302 | /// use rss::extension::itunes::ITunesChannelExtension; 303 | /// 304 | /// let mut extension = ITunesChannelExtension::default(); 305 | /// extension.set_new_feed_url("http://example.com/feed".to_string()); 306 | /// ``` 307 | pub fn set_new_feed_url(&mut self, new_feed_url: V) 308 | where 309 | V: Into>, 310 | { 311 | self.new_feed_url = new_feed_url.into(); 312 | } 313 | 314 | /// Return the contact information for the owner of this podcast. 315 | /// 316 | /// # Examples 317 | /// 318 | /// ``` 319 | /// use rss::extension::itunes::{ITunesChannelExtension, ITunesOwner}; 320 | /// 321 | /// let mut extension = ITunesChannelExtension::default(); 322 | /// extension.set_owner(ITunesOwner::default()); 323 | /// assert!(extension.owner().is_some()); 324 | /// ``` 325 | pub fn owner(&self) -> Option<&ITunesOwner> { 326 | self.owner.as_ref() 327 | } 328 | 329 | /// Set the contact information for the owner of this podcast. 330 | /// 331 | /// # Examples 332 | /// 333 | /// ``` 334 | /// use rss::extension::itunes::{ITunesChannelExtension, ITunesOwner}; 335 | /// 336 | /// let mut extension = ITunesChannelExtension::default(); 337 | /// extension.set_owner(ITunesOwner::default()); 338 | /// ``` 339 | pub fn set_owner(&mut self, owner: V) 340 | where 341 | V: Into>, 342 | { 343 | self.owner = owner.into(); 344 | } 345 | 346 | /// Return the description of this podcast. 347 | /// 348 | /// # Examples 349 | /// 350 | /// ``` 351 | /// use rss::extension::itunes::ITunesChannelExtension; 352 | /// 353 | /// let mut extension = ITunesChannelExtension::default(); 354 | /// extension.set_subtitle("A podcast".to_string()); 355 | /// assert_eq!(extension.subtitle(), Some("A podcast")); 356 | /// ``` 357 | pub fn subtitle(&self) -> Option<&str> { 358 | self.subtitle.as_deref() 359 | } 360 | 361 | /// Set the description of this podcast. 362 | /// 363 | /// # Examples 364 | /// 365 | /// ``` 366 | /// use rss::extension::itunes::ITunesChannelExtension; 367 | /// 368 | /// let mut extension = ITunesChannelExtension::default(); 369 | /// extension.set_subtitle("A podcast".to_string()); 370 | /// ``` 371 | pub fn set_subtitle(&mut self, subtitle: V) 372 | where 373 | V: Into>, 374 | { 375 | self.subtitle = subtitle.into(); 376 | } 377 | 378 | /// Return the summary for this podcast. 379 | /// 380 | /// # Examples 381 | /// 382 | /// ``` 383 | /// use rss::extension::itunes::ITunesChannelExtension; 384 | /// 385 | /// let mut extension = ITunesChannelExtension::default(); 386 | /// extension.set_summary("A podcast".to_string()); 387 | /// assert_eq!(extension.summary(), Some("A podcast")); 388 | /// ``` 389 | pub fn summary(&self) -> Option<&str> { 390 | self.summary.as_deref() 391 | } 392 | 393 | /// Set the summary for this podcast. 394 | /// 395 | /// # Examples 396 | /// 397 | /// ``` 398 | /// use rss::extension::itunes::ITunesChannelExtension; 399 | /// 400 | /// let mut extension = ITunesChannelExtension::default(); 401 | /// extension.set_summary("A podcast about technology".to_string()); 402 | /// ``` 403 | pub fn set_summary(&mut self, summary: V) 404 | where 405 | V: Into>, 406 | { 407 | self.summary = summary.into(); 408 | } 409 | 410 | /// Return the keywords for this podcast. 411 | /// 412 | /// A comma separated list of keywords. 413 | /// 414 | /// # Examples 415 | /// 416 | /// ``` 417 | /// use rss::extension::itunes::ITunesChannelExtension; 418 | /// 419 | /// let mut extension = ITunesChannelExtension::default(); 420 | /// extension.set_keywords("technology".to_string()); 421 | /// assert_eq!(extension.keywords(), Some("technology")); 422 | /// ``` 423 | pub fn keywords(&self) -> Option<&str> { 424 | self.keywords.as_deref() 425 | } 426 | 427 | /// Set the keywords for this podcast. 428 | /// 429 | /// A comma separated list of keywords. 430 | /// 431 | /// # Examples 432 | /// 433 | /// ``` 434 | /// use rss::extension::itunes::ITunesChannelExtension; 435 | /// 436 | /// let mut extension = ITunesChannelExtension::default(); 437 | /// extension.set_keywords("technology".to_string()); 438 | /// ``` 439 | pub fn set_keywords(&mut self, keywords: V) 440 | where 441 | V: Into>, 442 | { 443 | self.keywords = keywords.into(); 444 | } 445 | 446 | /// Return the type of this podcast. 447 | /// 448 | /// A string usually "serial" or "episodic" 449 | /// 450 | /// # Examples 451 | /// 452 | /// ``` 453 | /// use rss::extension::itunes::ITunesChannelExtension; 454 | /// 455 | /// let mut extension = ITunesChannelExtension::default(); 456 | /// extension.set_type("episodic".to_string()); 457 | /// assert_eq!(extension.r#type(), Some("episodic")); 458 | /// ``` 459 | pub fn r#type(&self) -> Option<&str> { 460 | self.r#type.as_deref() 461 | } 462 | 463 | /// Set the type of this podcast. 464 | /// 465 | /// A string, usually "serial" or "episodic" 466 | /// 467 | /// # Examples 468 | /// 469 | /// ``` 470 | /// use rss::extension::itunes::ITunesChannelExtension; 471 | /// 472 | /// let mut extension = ITunesChannelExtension::default(); 473 | /// extension.set_type("serial".to_string()); 474 | /// ``` 475 | pub fn set_type(&mut self, t: V) 476 | where 477 | V: Into>, 478 | { 479 | self.r#type = t.into(); 480 | } 481 | } 482 | 483 | impl ITunesChannelExtension { 484 | /// Create an `ITunesChannelExtension` from a `BTreeMap`. 485 | pub fn from_map(mut map: BTreeMap>) -> Self { 486 | Self { 487 | author: remove_extension_value(&mut map, "author"), 488 | block: remove_extension_value(&mut map, "block"), 489 | categories: parse_categories(&mut map), 490 | image: parse_image(&mut map), 491 | explicit: remove_extension_value(&mut map, "explicit"), 492 | complete: remove_extension_value(&mut map, "complete"), 493 | new_feed_url: remove_extension_value(&mut map, "new-feed-url"), 494 | owner: parse_owner(&mut map), 495 | subtitle: remove_extension_value(&mut map, "subtitle"), 496 | summary: remove_extension_value(&mut map, "summary"), 497 | keywords: remove_extension_value(&mut map, "keywords"), 498 | r#type: remove_extension_value(&mut map, "type"), 499 | } 500 | } 501 | } 502 | 503 | impl ToXml for ITunesChannelExtension { 504 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 505 | if let Some(author) = self.author.as_ref() { 506 | writer.write_text_element("itunes:author", author)?; 507 | } 508 | 509 | if let Some(block) = self.block.as_ref() { 510 | writer.write_text_element("itunes:block", block)?; 511 | } 512 | 513 | writer.write_objects(&self.categories)?; 514 | 515 | if let Some(image) = self.image.as_ref() { 516 | let name = "itunes:image"; 517 | let mut element = BytesStart::new(name); 518 | element.push_attribute(("href", &**image)); 519 | writer.write_event(Event::Empty(element))?; 520 | } 521 | 522 | if let Some(explicit) = self.explicit.as_ref() { 523 | writer.write_text_element("itunes:explicit", explicit)?; 524 | } 525 | 526 | if let Some(complete) = self.complete.as_ref() { 527 | writer.write_text_element("itunes:complete", complete)?; 528 | } 529 | 530 | if let Some(new_feed_url) = self.new_feed_url.as_ref() { 531 | writer.write_text_element("itunes:new-feed-url", new_feed_url)?; 532 | } 533 | 534 | if let Some(owner) = self.owner.as_ref() { 535 | writer.write_object(owner)?; 536 | } 537 | 538 | if let Some(subtitle) = self.subtitle.as_ref() { 539 | writer.write_text_element("itunes:subtitle", subtitle)?; 540 | } 541 | 542 | if let Some(summary) = self.summary.as_ref() { 543 | writer.write_text_element("itunes:summary", summary)?; 544 | } 545 | 546 | if let Some(keywords) = self.keywords.as_ref() { 547 | writer.write_text_element("itunes:keywords", keywords)?; 548 | } 549 | 550 | if let Some(r#type) = self.r#type.as_ref() { 551 | writer.write_text_element("itunes:type", r#type)?; 552 | } 553 | 554 | Ok(()) 555 | } 556 | 557 | fn used_namespaces(&self) -> BTreeMap { 558 | let mut namespaces = BTreeMap::new(); 559 | namespaces.insert("itunes".to_owned(), NAMESPACE.to_owned()); 560 | namespaces 561 | } 562 | } 563 | 564 | #[cfg(feature = "builders")] 565 | impl ITunesChannelExtensionBuilder { 566 | /// Builds a new `ITunesChannelExtension`. 567 | pub fn build(&self) -> ITunesChannelExtension { 568 | self.build_impl().unwrap() 569 | } 570 | } 571 | 572 | #[cfg(test)] 573 | mod tests { 574 | use super::*; 575 | 576 | #[test] 577 | #[cfg(feature = "builders")] 578 | fn test_builder() { 579 | use crate::extension::itunes::ITunesCategoryBuilder; 580 | 581 | assert_eq!( 582 | ITunesChannelExtensionBuilder::default() 583 | .author("John Doe".to_string()) 584 | .category(ITunesCategoryBuilder::default().text("technology").build()) 585 | .category(ITunesCategoryBuilder::default().text("podcast").build()) 586 | .build(), 587 | ITunesChannelExtension { 588 | author: Some("John Doe".to_string()), 589 | categories: vec![ 590 | ITunesCategory { 591 | text: "technology".to_string(), 592 | subcategory: None, 593 | }, 594 | ITunesCategory { 595 | text: "podcast".to_string(), 596 | subcategory: None, 597 | }, 598 | ], 599 | ..Default::default() 600 | }, 601 | ); 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /src/extension/itunes/itunes_owner.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::Write; 9 | 10 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 11 | use quick_xml::Error as XmlError; 12 | use quick_xml::Writer; 13 | 14 | use crate::toxml::{ToXml, WriterExt}; 15 | 16 | /// The contact information for the owner of an iTunes podcast. 17 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 18 | #[derive(Debug, Default, Clone, PartialEq)] 19 | #[cfg_attr(feature = "builders", derive(Builder))] 20 | #[cfg_attr( 21 | feature = "builders", 22 | builder( 23 | setter(into), 24 | default, 25 | build_fn(name = "build_impl", private, error = "never::Never") 26 | ) 27 | )] 28 | pub struct ITunesOwner { 29 | /// The name of the owner. 30 | pub name: Option, 31 | /// The email of the owner. 32 | pub email: Option, 33 | } 34 | 35 | impl ITunesOwner { 36 | /// Return the name of this person. 37 | /// 38 | /// # Examples 39 | /// 40 | /// ``` 41 | /// use rss::extension::itunes::ITunesOwner; 42 | /// 43 | /// let mut owner = ITunesOwner::default(); 44 | /// owner.set_name("John Doe".to_string()); 45 | /// assert_eq!(owner.name(), Some("John Doe")); 46 | /// ``` 47 | pub fn name(&self) -> Option<&str> { 48 | self.name.as_deref() 49 | } 50 | 51 | /// Set the name of this person. 52 | /// 53 | /// # Examples 54 | /// 55 | /// ``` 56 | /// use rss::extension::itunes::ITunesOwner; 57 | /// 58 | /// let mut owner = ITunesOwner::default(); 59 | /// owner.set_name("John Doe".to_string()); 60 | /// ``` 61 | pub fn set_name(&mut self, name: V) 62 | where 63 | V: Into>, 64 | { 65 | self.name = name.into(); 66 | } 67 | 68 | /// Return the email of this person. 69 | /// 70 | /// # Examples 71 | /// 72 | /// ``` 73 | /// use rss::extension::itunes::ITunesOwner; 74 | /// 75 | /// let mut owner = ITunesOwner::default(); 76 | /// owner.set_email("johndoe@example.com".to_string()); 77 | /// assert_eq!(owner.email(), Some("johndoe@example.com")); 78 | /// ``` 79 | pub fn email(&self) -> Option<&str> { 80 | self.email.as_deref() 81 | } 82 | 83 | /// Set the email of this person. 84 | /// 85 | /// # Examples 86 | /// 87 | /// ``` 88 | /// use rss::extension::itunes::ITunesOwner; 89 | /// 90 | /// let mut owner = ITunesOwner::default(); 91 | /// owner.set_email("johndoe@example.com".to_string()); 92 | /// ``` 93 | pub fn set_email(&mut self, email: V) 94 | where 95 | V: Into>, 96 | { 97 | self.email = email.into(); 98 | } 99 | } 100 | 101 | impl ToXml for ITunesOwner { 102 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 103 | let name = "itunes:owner"; 104 | 105 | writer.write_event(Event::Start(BytesStart::new(name)))?; 106 | 107 | if let Some(name) = self.name.as_ref() { 108 | writer.write_text_element("itunes:name", name)?; 109 | } 110 | 111 | if let Some(email) = self.email.as_ref() { 112 | writer.write_text_element("itunes:email", email)?; 113 | } 114 | 115 | writer.write_event(Event::End(BytesEnd::new(name)))?; 116 | Ok(()) 117 | } 118 | } 119 | 120 | #[cfg(feature = "builders")] 121 | impl ITunesOwnerBuilder { 122 | /// Builds a new `ITunesOwner`. 123 | pub fn build(&self) -> ITunesOwner { 124 | self.build_impl().unwrap() 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | #[cfg(feature = "builders")] 134 | fn test_builder() { 135 | assert_eq!( 136 | ITunesOwnerBuilder::default() 137 | .name("John Doe".to_string()) 138 | .build(), 139 | ITunesOwner { 140 | name: Some("John Doe".to_string()), 141 | email: None, 142 | } 143 | ); 144 | assert_eq!( 145 | ITunesOwnerBuilder::default() 146 | .name("John Doe".to_string()) 147 | .email("johndoe@example.com".to_string()) 148 | .build(), 149 | ITunesOwner { 150 | name: Some("John Doe".to_string()), 151 | email: Some("johndoe@example.com".to_string()), 152 | } 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/extension/itunes/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | 10 | use crate::extension::Extension; 11 | 12 | mod itunes_category; 13 | mod itunes_channel_extension; 14 | mod itunes_item_extension; 15 | mod itunes_owner; 16 | 17 | pub use self::itunes_category::*; 18 | pub use self::itunes_channel_extension::*; 19 | pub use self::itunes_item_extension::*; 20 | pub use self::itunes_owner::*; 21 | 22 | /// The iTunes XML namespace. 23 | pub const NAMESPACE: &str = "http://www.itunes.com/dtds/podcast-1.0.dtd"; 24 | 25 | /// Formally XML namespace is case sensitive and this should be just an equality check. 26 | /// But many podcast publishers ignore this and use different case variations of the namespace. 27 | /// Hence this check is relaxed and ignores a case. 28 | #[inline] 29 | pub(crate) fn is_itunes_namespace(ns: &str) -> bool { 30 | ns.eq_ignore_ascii_case(NAMESPACE) 31 | } 32 | 33 | fn parse_image(map: &mut BTreeMap>) -> Option { 34 | let mut element = match map.remove("image").map(|mut v| v.remove(0)) { 35 | Some(element) => element, 36 | None => return None, 37 | }; 38 | 39 | element.attrs.remove("href") 40 | } 41 | 42 | fn parse_categories(map: &mut BTreeMap>) -> Vec { 43 | let mut elements = match map.remove("category") { 44 | Some(elements) => elements, 45 | None => return Vec::new(), 46 | }; 47 | 48 | let mut categories = Vec::with_capacity(elements.len()); 49 | 50 | for elem in &mut elements { 51 | let text = elem.attrs.remove("text").unwrap_or_default(); 52 | 53 | let child = { 54 | if let Some(mut child) = elem.children.remove("category").map(|mut v| v.remove(0)) { 55 | let text = child.attrs.remove("text").unwrap_or_default(); 56 | let mut category = ITunesCategory::default(); 57 | category.set_text(text); 58 | Some(Box::new(category)) 59 | } else { 60 | None 61 | } 62 | }; 63 | 64 | let mut category = ITunesCategory::default(); 65 | category.set_text(text); 66 | category.set_subcategory(child); 67 | categories.push(category); 68 | } 69 | 70 | categories 71 | } 72 | 73 | fn parse_owner(map: &mut BTreeMap>) -> Option { 74 | if let Some(mut element) = map.remove("owner").map(|mut v| v.remove(0)) { 75 | let name = element 76 | .children 77 | .remove("name") 78 | .and_then(|mut v| v.remove(0).value); 79 | 80 | let email = element 81 | .children 82 | .remove("email") 83 | .and_then(|mut v| v.remove(0).value); 84 | 85 | let mut owner = ITunesOwner::default(); 86 | owner.set_name(name); 87 | owner.set_email(email); 88 | Some(owner) 89 | } else { 90 | None 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/extension/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::Write; 10 | use std::str; 11 | 12 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 13 | use quick_xml::Error as XmlError; 14 | use quick_xml::Writer; 15 | 16 | use crate::toxml::ToXml; 17 | 18 | /// Types and methods for [Atom](https://www.rssboard.org/rss-profile#namespace-elements-atom) extensions. 19 | #[cfg(feature = "atom")] 20 | pub mod atom; 21 | 22 | /// Types and methods for 23 | /// [iTunes](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) extensions. 24 | pub mod itunes; 25 | 26 | /// Types and methods for [Dublin Core](http://dublincore.org/documents/dces/) extensions. 27 | pub mod dublincore; 28 | 29 | /// Types and methods for [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) extensions. 30 | pub mod syndication; 31 | 32 | pub(crate) mod util; 33 | 34 | /// A map of extension namespace prefixes to local names to elements. 35 | pub type ExtensionMap = BTreeMap>>; 36 | 37 | /// A namespaced extension such as iTunes or Dublin Core. 38 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 39 | #[derive(Debug, Default, Clone, PartialEq)] 40 | #[cfg_attr(feature = "builders", derive(Builder))] 41 | #[cfg_attr( 42 | feature = "builders", 43 | builder( 44 | setter(into), 45 | default, 46 | build_fn(name = "build_impl", private, error = "never::Never") 47 | ) 48 | )] 49 | pub struct Extension { 50 | /// The qualified name of the extension element. 51 | pub name: String, 52 | /// The content of the extension element. 53 | pub value: Option, 54 | /// The attributes for the extension element. 55 | #[cfg_attr(feature = "builders", builder(setter(each = "attr")))] 56 | pub attrs: BTreeMap, 57 | /// The children of the extension element. This is a map of local names to child 58 | /// elements. 59 | #[cfg_attr(feature = "builders", builder(setter(each = "child")))] 60 | pub children: BTreeMap>, 61 | } 62 | 63 | impl Extension { 64 | /// Return the qualified name of this extension. 65 | pub fn name(&self) -> &str { 66 | self.name.as_str() 67 | } 68 | 69 | /// Set the qualified name of this extension. 70 | pub fn set_name(&mut self, name: V) 71 | where 72 | V: Into, 73 | { 74 | self.name = name.into(); 75 | } 76 | 77 | /// Return the text content of this extension. 78 | pub fn value(&self) -> Option<&str> { 79 | self.value.as_deref() 80 | } 81 | 82 | /// Set the text content of this extension. 83 | pub fn set_value(&mut self, value: V) 84 | where 85 | V: Into>, 86 | { 87 | self.value = value.into(); 88 | } 89 | 90 | /// Return the attributes for the extension element. 91 | pub fn attrs(&self) -> &BTreeMap { 92 | &self.attrs 93 | } 94 | 95 | /// Return the children of the extension element. 96 | /// 97 | /// This is a map of local names to child elements. 98 | pub fn children(&self) -> &BTreeMap> { 99 | &self.children 100 | } 101 | } 102 | 103 | impl ToXml for Extension { 104 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 105 | let mut element = BytesStart::new(&self.name); 106 | element.extend_attributes(self.attrs.iter().map(|a| (a.0.as_str(), a.1.as_str()))); 107 | writer.write_event(Event::Start(element))?; 108 | 109 | if let Some(ref value) = self.value { 110 | writer.write_event(Event::Text(BytesText::new(value)))?; 111 | } 112 | 113 | for extension in self.children.values().flatten() { 114 | extension.to_xml(writer)?; 115 | } 116 | 117 | writer.write_event(Event::End(BytesEnd::new(&self.name)))?; 118 | Ok(()) 119 | } 120 | } 121 | 122 | #[cfg(feature = "builders")] 123 | impl ExtensionBuilder { 124 | /// Builds a new `Extension`. 125 | pub fn build(&self) -> Extension { 126 | self.build_impl().unwrap() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/extension/syndication.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::fmt; 10 | use std::io::Write; 11 | use std::str::FromStr; 12 | 13 | use quick_xml::Error as XmlError; 14 | use quick_xml::Writer; 15 | 16 | use crate::extension::Extension; 17 | use crate::toxml::WriterExt; 18 | 19 | /// The Syndication XML namespace. 20 | pub const NAMESPACE: &str = "http://purl.org/rss/1.0/modules/syndication/"; 21 | 22 | /// The unit of time between updates/refreshes 23 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 24 | #[derive(Debug, Clone, PartialEq)] 25 | pub enum UpdatePeriod { 26 | /// refresh hourly 27 | Hourly, 28 | /// refresh daily 29 | Daily, 30 | /// refresh weekly 31 | Weekly, 32 | /// refresh monthly 33 | Monthly, 34 | /// refresh yearly 35 | Yearly, 36 | } 37 | 38 | impl FromStr for UpdatePeriod { 39 | type Err = (); 40 | 41 | fn from_str(s: &str) -> Result { 42 | match s { 43 | "hourly" => Ok(UpdatePeriod::Hourly), 44 | "daily" => Ok(UpdatePeriod::Daily), 45 | "weekly" => Ok(UpdatePeriod::Weekly), 46 | "monthly" => Ok(UpdatePeriod::Monthly), 47 | "yearly" => Ok(UpdatePeriod::Yearly), 48 | _ => Err(()), 49 | } 50 | } 51 | } 52 | 53 | impl fmt::Display for UpdatePeriod { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | match *self { 56 | UpdatePeriod::Hourly => write!(f, "hourly"), 57 | UpdatePeriod::Daily => write!(f, "daily"), 58 | UpdatePeriod::Weekly => write!(f, "weekly"), 59 | UpdatePeriod::Monthly => write!(f, "monthly"), 60 | UpdatePeriod::Yearly => write!(f, "yearly"), 61 | } 62 | } 63 | } 64 | 65 | /// An RSS syndication element extension. 66 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 67 | #[derive(Debug, Clone, PartialEq)] 68 | #[cfg_attr(feature = "builders", derive(Builder))] 69 | #[cfg_attr( 70 | feature = "builders", 71 | builder( 72 | setter(into), 73 | default, 74 | build_fn(name = "build_impl", private, error = "never::Never") 75 | ) 76 | )] 77 | pub struct SyndicationExtension { 78 | /// The refresh period for this channel 79 | pub period: UpdatePeriod, 80 | /// Number of periods between refreshes 81 | pub frequency: u32, 82 | /// Timestamp from which the refresh periods are calculated 83 | pub base: String, 84 | } 85 | 86 | impl SyndicationExtension { 87 | /// Retrieve the base timestamp from which the refresh periods are calculated 88 | pub fn base(&self) -> &str { 89 | &self.base 90 | } 91 | 92 | /// Set the base from which the refresh periods are calculated 93 | pub fn set_base(&mut self, base: &str) { 94 | base.clone_into(&mut self.base); 95 | } 96 | 97 | /// Retrieve the number of periods between refreshes 98 | pub fn frequency(&self) -> u32 { 99 | self.frequency 100 | } 101 | 102 | /// Set the number of periods between refreshes 103 | pub fn set_frequency(&mut self, frequency: u32) { 104 | self.frequency = frequency; 105 | } 106 | 107 | /// Retrieve the refresh period for this channel 108 | pub fn period(&self) -> &UpdatePeriod { 109 | &self.period 110 | } 111 | 112 | /// Set the refresh period for this channel 113 | pub fn set_period(&mut self, period: UpdatePeriod) { 114 | self.period = period; 115 | } 116 | 117 | /// Serializes this extension to the nominated writer 118 | pub fn to_xml( 119 | &self, 120 | namespaces: &BTreeMap, 121 | writer: &mut Writer, 122 | ) -> Result<(), XmlError> { 123 | for (prefix, namespace) in namespaces { 124 | if NAMESPACE == namespace { 125 | writer.write_text_element( 126 | format!("{}:updatePeriod", prefix), 127 | &self.period.to_string(), 128 | )?; 129 | writer.write_text_element( 130 | format!("{}:updateFrequency", prefix), 131 | &format!("{}", self.frequency), 132 | )?; 133 | writer.write_text_element(format!("{}:updateBase", prefix), &self.base)?; 134 | } 135 | } 136 | Ok(()) 137 | } 138 | } 139 | 140 | impl Default for SyndicationExtension { 141 | fn default() -> Self { 142 | SyndicationExtension { 143 | period: UpdatePeriod::Daily, 144 | frequency: 1, 145 | base: String::from("1970-01-01T00:00+00:00"), 146 | } 147 | } 148 | } 149 | 150 | /// Retrieves the extensions for the nominated field and runs the callback if there is at least 1 extension value 151 | fn with_first_ext_value<'a, F>(map: &'a BTreeMap>, field: &str, f: F) 152 | where 153 | F: FnOnce(&'a str), 154 | { 155 | if let Some(extensions) = map.get(field) { 156 | if !extensions.is_empty() { 157 | if let Some(v) = extensions[0].value.as_ref() { 158 | f(v); 159 | } 160 | } 161 | } 162 | } 163 | 164 | impl SyndicationExtension { 165 | /// Creates a `SyndicationExtension` using the specified `BTreeMap`. 166 | pub fn from_map(map: BTreeMap>) -> Self { 167 | let mut syn = SyndicationExtension::default(); 168 | 169 | with_first_ext_value(&map, "updatePeriod", |value| { 170 | if let Ok(update_period) = value.parse() { 171 | syn.period = update_period 172 | } 173 | }); 174 | with_first_ext_value(&map, "updateFrequency", |value| { 175 | if let Ok(frequency) = value.parse() { 176 | syn.frequency = frequency 177 | } 178 | }); 179 | with_first_ext_value(&map, "updateBase", |value| value.clone_into(&mut syn.base)); 180 | 181 | syn 182 | } 183 | } 184 | 185 | #[cfg(feature = "builders")] 186 | impl SyndicationExtensionBuilder { 187 | /// Builds a new `SyndicationExtension`. 188 | pub fn build(&self) -> SyndicationExtension { 189 | self.build_impl().unwrap() 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | 197 | #[test] 198 | #[cfg(feature = "builders")] 199 | fn test_builder() { 200 | assert_eq!( 201 | SyndicationExtensionBuilder::default() 202 | .period(UpdatePeriod::Weekly) 203 | .frequency(2_u32) 204 | .base("2021-01-01T00:00+00:00") 205 | .build(), 206 | SyndicationExtension { 207 | period: UpdatePeriod::Weekly, 208 | frequency: 2, 209 | base: "2021-01-01T00:00+00:00".to_string(), 210 | } 211 | ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/extension/util.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::borrow::Cow; 9 | use std::collections::BTreeMap; 10 | use std::io::BufRead; 11 | use std::str; 12 | 13 | use quick_xml::events::attributes::Attributes; 14 | use quick_xml::events::Event; 15 | use quick_xml::Reader; 16 | 17 | use crate::error::Error; 18 | use crate::extension::{Extension, ExtensionMap}; 19 | use crate::util::{attr_value, decode}; 20 | 21 | pub(crate) fn read_namespace_declarations<'m, R>( 22 | reader: &mut Reader, 23 | mut atts: Attributes, 24 | base: &'m BTreeMap, 25 | ) -> Result>, Error> 26 | where 27 | R: BufRead, 28 | { 29 | let mut namespaces = Cow::Borrowed(base); 30 | for attr in atts.with_checks(false).flatten() { 31 | let key = decode(attr.key.as_ref(), reader)?; 32 | if let Some(ns) = key.strip_prefix("xmlns:") { 33 | namespaces 34 | .to_mut() 35 | .insert(ns.to_string(), attr_value(&attr, reader)?.to_string()); 36 | } 37 | } 38 | Ok(namespaces) 39 | } 40 | 41 | pub(crate) fn extension_name(element_name: &str) -> Option<(&str, &str)> { 42 | let mut split = element_name.splitn(2, ':'); 43 | let ns = split.next().filter(|ns| !ns.is_empty())?; 44 | let name = split.next()?; 45 | Some((ns, name)) 46 | } 47 | 48 | pub(crate) fn extension_entry<'e>( 49 | extensions: &'e mut ExtensionMap, 50 | ns: &str, 51 | name: &str, 52 | ) -> &'e mut Vec { 53 | let map = extensions.entry(ns.to_string()).or_default(); 54 | 55 | map.entry(name.to_string()).or_default() 56 | } 57 | 58 | pub(crate) fn parse_extension_element( 59 | reader: &mut Reader, 60 | mut atts: Attributes, 61 | ) -> Result { 62 | let mut extension = Extension::default(); 63 | let mut buf = Vec::new(); 64 | 65 | for attr in atts.with_checks(false).flatten() { 66 | let key = decode(attr.key.as_ref(), reader)?.to_string(); 67 | let value = attr_value(&attr, reader)?.to_string(); 68 | extension.attrs.insert(key.to_string(), value); 69 | } 70 | 71 | let mut text = String::new(); 72 | loop { 73 | match reader.read_event_into(&mut buf)? { 74 | Event::Start(element) => { 75 | let ext = parse_extension_element(reader, element.attributes())?; 76 | let element_local_name = element.local_name(); 77 | let name = decode(element_local_name.as_ref(), reader)?; 78 | 79 | let items = extension 80 | .children 81 | .entry(name.to_string()) 82 | .or_insert_with(Vec::new); 83 | 84 | items.push(ext); 85 | } 86 | Event::CData(element) => { 87 | text.push_str(decode(&element, reader)?.as_ref()); 88 | } 89 | Event::Text(element) => { 90 | text.push_str(element.unescape()?.as_ref()); 91 | } 92 | Event::End(element) => { 93 | extension.name = decode(element.name().as_ref(), reader)?.into(); 94 | break; 95 | } 96 | Event::Eof => return Err(Error::Eof), 97 | _ => {} 98 | } 99 | 100 | buf.clear(); 101 | } 102 | extension.value = Some(text.trim()) 103 | .filter(|t| !t.is_empty()) 104 | .map(ToString::to_string); 105 | 106 | Ok(extension) 107 | } 108 | 109 | pub fn get_extension_values(v: Vec) -> Vec { 110 | v.into_iter() 111 | .filter_map(|ext| ext.value) 112 | .collect::>() 113 | } 114 | 115 | pub fn remove_extension_value( 116 | map: &mut BTreeMap>, 117 | key: &str, 118 | ) -> Option { 119 | map.remove(key) 120 | .map(|mut v| v.remove(0)) 121 | .and_then(|ext| ext.value) 122 | } 123 | -------------------------------------------------------------------------------- /src/guid.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::attributes::Attributes; 11 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Reader; 14 | use quick_xml::Writer; 15 | 16 | use crate::error::Error; 17 | use crate::toxml::ToXml; 18 | use crate::util::{decode, element_text}; 19 | 20 | /// Represents the GUID of an RSS item. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug, Clone, PartialEq)] 23 | #[cfg_attr(feature = "builders", derive(Builder))] 24 | #[cfg_attr( 25 | feature = "builders", 26 | builder( 27 | setter(into), 28 | default, 29 | build_fn(name = "build_impl", private, error = "never::Never") 30 | ) 31 | )] 32 | pub struct Guid { 33 | /// The value of the GUID. 34 | pub value: String, 35 | /// Indicates if the GUID is a permalink. 36 | pub permalink: bool, 37 | } 38 | 39 | impl Guid { 40 | /// Return whether this GUID is a permalink. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ``` 45 | /// use rss::Guid; 46 | /// 47 | /// let mut guid = Guid::default(); 48 | /// guid.set_permalink(true); 49 | /// assert!(guid.is_permalink()); 50 | /// ``` 51 | pub fn is_permalink(&self) -> bool { 52 | self.permalink 53 | } 54 | 55 | /// Set whether this GUID is a permalink. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ``` 60 | /// use rss::Guid; 61 | /// 62 | /// let mut guid = Guid::default(); 63 | /// guid.set_permalink(true); 64 | /// ``` 65 | pub fn set_permalink(&mut self, permalink: V) 66 | where 67 | V: Into, 68 | { 69 | self.permalink = permalink.into() 70 | } 71 | 72 | /// Return the value of this GUID. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ``` 77 | /// use rss::Guid; 78 | /// 79 | /// let mut guid = Guid::default(); 80 | /// guid.set_value("00000000-0000-0000-0000-00000000000"); 81 | /// assert_eq!(guid.value(), "00000000-0000-0000-0000-00000000000"); 82 | /// ``` 83 | pub fn value(&self) -> &str { 84 | self.value.as_str() 85 | } 86 | 87 | /// Set the value of this GUID. 88 | /// 89 | /// # Examples 90 | /// 91 | /// ``` 92 | /// use rss::Guid; 93 | /// 94 | /// let mut guid = Guid::default(); 95 | /// guid.set_value("00000000-0000-0000-0000-00000000000"); 96 | /// ``` 97 | pub fn set_value(&mut self, value: V) 98 | where 99 | V: Into, 100 | { 101 | self.value = value.into(); 102 | } 103 | } 104 | 105 | impl Default for Guid { 106 | #[inline] 107 | fn default() -> Self { 108 | Guid { 109 | value: Default::default(), 110 | permalink: true, 111 | } 112 | } 113 | } 114 | 115 | impl Guid { 116 | /// Builds a Guid from source XML 117 | pub fn from_xml( 118 | reader: &mut Reader, 119 | mut atts: Attributes, 120 | ) -> Result { 121 | let mut guid = Guid::default(); 122 | 123 | for attr in atts.with_checks(false).flatten() { 124 | if decode(attr.key.as_ref(), reader)?.as_ref() == "isPermaLink" { 125 | guid.permalink = &*attr.value != b"false"; 126 | break; 127 | } 128 | } 129 | 130 | guid.value = element_text(reader)?.unwrap_or_default(); 131 | Ok(guid) 132 | } 133 | } 134 | 135 | impl ToXml for Guid { 136 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 137 | let name = "guid"; 138 | let mut element = BytesStart::new(name); 139 | if !self.permalink { 140 | element.push_attribute(("isPermaLink", "false")); 141 | } 142 | writer.write_event(Event::Start(element))?; 143 | writer.write_event(Event::Text(BytesText::new(&self.value)))?; 144 | writer.write_event(Event::End(BytesEnd::new(name)))?; 145 | Ok(()) 146 | } 147 | } 148 | 149 | #[cfg(feature = "builders")] 150 | impl GuidBuilder { 151 | /// Builds a new `Guid`. 152 | pub fn build(&self) -> Guid { 153 | self.build_impl().unwrap() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::attributes::Attributes; 11 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Reader; 14 | use quick_xml::Writer; 15 | 16 | use crate::error::Error; 17 | use crate::toxml::{ToXml, WriterExt}; 18 | use crate::util::{decode, element_text, skip}; 19 | 20 | /// Represents an image in an RSS feed. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug, Default, Clone, PartialEq)] 23 | #[cfg_attr(feature = "builders", derive(Builder))] 24 | #[cfg_attr( 25 | feature = "builders", 26 | builder( 27 | setter(into), 28 | default, 29 | build_fn(name = "build_impl", private, error = "never::Never") 30 | ) 31 | )] 32 | pub struct Image { 33 | /// The URL of the image. 34 | pub url: String, 35 | /// A description of the image. This is used in the HTML `alt` attribute. 36 | pub title: String, 37 | /// The URL that the image links to. 38 | pub link: String, 39 | /// The width of the image. 40 | pub width: Option, 41 | /// The height of the image. 42 | pub height: Option, 43 | /// The text for the HTML `title` attribute of the link formed around the image. 44 | pub description: Option, 45 | } 46 | 47 | impl Image { 48 | /// Return the URL of this image. 49 | /// 50 | /// # Examples 51 | /// 52 | /// ``` 53 | /// use rss::Image; 54 | /// 55 | /// let mut image = Image::default(); 56 | /// image.set_url("http://example.com/image.png"); 57 | /// assert_eq!(image.url(), "http://example.com/image.png"); 58 | /// ``` 59 | pub fn url(&self) -> &str { 60 | self.url.as_str() 61 | } 62 | 63 | /// Set the URL of this image. 64 | /// 65 | /// # Examples 66 | /// 67 | /// ``` 68 | /// use rss::Image; 69 | /// 70 | /// let mut image = Image::default(); 71 | /// image.set_url("http://example.com/image.png"); 72 | /// ``` 73 | pub fn set_url(&mut self, url: V) 74 | where 75 | V: Into, 76 | { 77 | self.url = url.into(); 78 | } 79 | 80 | /// Return the description of this image. 81 | /// 82 | /// This is used in the HTML `alt` attribute. 83 | /// 84 | /// # Examples 85 | /// 86 | /// ``` 87 | /// use rss::Image; 88 | /// 89 | /// let mut image = Image::default(); 90 | /// image.set_title("Example image"); 91 | /// assert_eq!(image.title(), "Example image"); 92 | /// ``` 93 | pub fn title(&self) -> &str { 94 | self.title.as_str() 95 | } 96 | 97 | /// Set the description of this image. 98 | /// 99 | /// This is used in the HTML `alt` attribute. 100 | /// 101 | /// # Examples 102 | /// 103 | /// ``` 104 | /// use rss::Image; 105 | /// 106 | /// let mut image = Image::default(); 107 | /// image.set_title("Example image"); 108 | /// ``` 109 | pub fn set_title(&mut self, title: V) 110 | where 111 | V: Into, 112 | { 113 | self.title = title.into(); 114 | } 115 | 116 | /// Return the URL that this image links to. 117 | /// 118 | /// # Examples 119 | /// 120 | /// ``` 121 | /// use rss::Image; 122 | /// 123 | /// let mut image = Image::default(); 124 | /// image.set_link("http://example.com"); 125 | /// assert_eq!(image.link(), "http://example.com"); 126 | pub fn link(&self) -> &str { 127 | self.link.as_str() 128 | } 129 | 130 | /// Set the URL that this image links to. 131 | /// 132 | /// # Examples 133 | /// 134 | /// ``` 135 | /// use rss::Image; 136 | /// 137 | /// let mut image = Image::default(); 138 | /// image.set_link("http://example.com"); 139 | pub fn set_link(&mut self, link: V) 140 | where 141 | V: Into, 142 | { 143 | self.link = link.into(); 144 | } 145 | 146 | /// Return the width of this image. 147 | /// 148 | /// If the width is `None` the default value should be considered to be `80`. 149 | /// 150 | /// # Examples 151 | /// 152 | /// ``` 153 | /// use rss::Image; 154 | /// 155 | /// let mut image = Image::default(); 156 | /// image.set_width("80".to_string()); 157 | /// assert_eq!(image.width(), Some("80")); 158 | pub fn width(&self) -> Option<&str> { 159 | self.width.as_deref() 160 | } 161 | 162 | /// Set the width of this image. 163 | /// 164 | /// # Examples 165 | /// 166 | /// ``` 167 | /// use rss::Image; 168 | /// 169 | /// let mut image = Image::default(); 170 | /// image.set_width("80".to_string()); 171 | pub fn set_width(&mut self, width: V) 172 | where 173 | V: Into>, 174 | { 175 | self.width = width.into(); 176 | } 177 | 178 | /// Return the height of this image. 179 | /// 180 | /// If the height is `None` the default value should be considered to be `31`. 181 | /// 182 | /// # Examples 183 | /// 184 | /// ``` 185 | /// use rss::Image; 186 | /// 187 | /// let mut image = Image::default(); 188 | /// image.set_height("31".to_string()); 189 | /// assert_eq!(image.height(), Some("31")); 190 | /// ``` 191 | pub fn height(&self) -> Option<&str> { 192 | self.height.as_deref() 193 | } 194 | 195 | /// Set the height of this image. 196 | /// 197 | /// If the height is `None` the default value should be considered to be `31`. 198 | /// 199 | /// # Examples 200 | /// 201 | /// ``` 202 | /// use rss::Image; 203 | /// 204 | /// let mut image = Image::default(); 205 | /// image.set_height("31".to_string()); 206 | /// ``` 207 | pub fn set_height(&mut self, height: V) 208 | where 209 | V: Into>, 210 | { 211 | self.height = height.into(); 212 | } 213 | 214 | /// Return the title for the link formed around this image. 215 | /// 216 | /// # Examples 217 | /// 218 | /// ``` 219 | /// use rss::Image; 220 | /// 221 | /// let mut image = Image::default(); 222 | /// image.set_description("Example Title".to_string()); 223 | /// assert_eq!(image.description(), Some("Example Title")); 224 | /// ``` 225 | pub fn description(&self) -> Option<&str> { 226 | self.description.as_deref() 227 | } 228 | 229 | /// Set the title for the link formed around this image. 230 | /// 231 | /// # Examples 232 | /// 233 | /// ``` 234 | /// use rss::Image; 235 | /// 236 | /// let mut image = Image::default(); 237 | /// image.set_description("Example Title".to_string()); 238 | /// ``` 239 | pub fn set_description(&mut self, description: V) 240 | where 241 | V: Into>, 242 | { 243 | self.description = description.into(); 244 | } 245 | } 246 | 247 | impl Image { 248 | /// Builds an Image from source XML 249 | pub fn from_xml(reader: &mut Reader, _: Attributes) -> Result { 250 | let mut image = Image::default(); 251 | let mut buf = Vec::new(); 252 | 253 | loop { 254 | match reader.read_event_into(&mut buf)? { 255 | Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { 256 | "url" => image.url = element_text(reader)?.unwrap_or_default(), 257 | "title" => image.title = element_text(reader)?.unwrap_or_default(), 258 | "link" => image.link = element_text(reader)?.unwrap_or_default(), 259 | "width" => image.width = element_text(reader)?, 260 | "height" => image.height = element_text(reader)?, 261 | "description" => image.description = element_text(reader)?, 262 | _ => skip(element.name(), reader)?, 263 | }, 264 | Event::End(_) => break, 265 | Event::Eof => return Err(Error::Eof), 266 | _ => {} 267 | } 268 | 269 | buf.clear(); 270 | } 271 | 272 | Ok(image) 273 | } 274 | } 275 | 276 | impl ToXml for Image { 277 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 278 | let name = "image"; 279 | 280 | writer.write_event(Event::Start(BytesStart::new(name)))?; 281 | 282 | writer.write_text_element("url", &self.url)?; 283 | writer.write_text_element("title", &self.title)?; 284 | writer.write_text_element("link", &self.link)?; 285 | 286 | if let Some(width) = self.width.as_ref() { 287 | writer.write_text_element("width", width)?; 288 | } 289 | 290 | if let Some(height) = self.height.as_ref() { 291 | writer.write_text_element("height", height)?; 292 | } 293 | 294 | if let Some(description) = self.description.as_ref() { 295 | writer.write_text_element("description", description)?; 296 | } 297 | 298 | writer.write_event(Event::End(BytesEnd::new(name)))?; 299 | Ok(()) 300 | } 301 | } 302 | 303 | #[cfg(feature = "builders")] 304 | impl ImageBuilder { 305 | /// Builds a new `Image`. 306 | pub fn build(&self) -> Image { 307 | self.build_impl().unwrap() 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | #![warn(missing_docs)] 9 | #![doc(html_root_url = "https://docs.rs/rss/")] 10 | 11 | //! Library for serializing the RSS web content syndication format. 12 | //! 13 | //! # Reading 14 | //! 15 | //! A channel can be read from any object that implements the `BufRead` trait. 16 | //! 17 | //! ## From a file 18 | //! 19 | //! ```rust,no_run 20 | //! use std::fs::File; 21 | //! use std::io::BufReader; 22 | //! use rss::Channel; 23 | //! 24 | //! let file = File::open("example.xml").unwrap(); 25 | //! let channel = Channel::read_from(BufReader::new(file)).unwrap(); 26 | //! ``` 27 | //! 28 | //! ### From a buffer 29 | //! 30 | //! **Note**: This example requires [reqwest](https://crates.io/crates/reqwest) crate. 31 | //! 32 | //! ```rust,ignore 33 | //! use std::error::Error; 34 | //! use rss::Channel; 35 | //! 36 | //! async fn example_feed() -> Result> { 37 | //! let content = reqwest::get("http://example.com/feed.xml") 38 | //! .await? 39 | //! .bytes() 40 | //! .await?; 41 | //! let channel = Channel::read_from(&content[..])?; 42 | //! Ok(channel) 43 | //! } 44 | //! ``` 45 | //! 46 | //! # Writing 47 | //! 48 | //! A channel can be written to any object that implements the `Write` trait or converted to an 49 | //! XML string using the `ToString` trait. 50 | //! 51 | //! ```rust 52 | //! use rss::Channel; 53 | //! 54 | //! let channel = Channel::default(); 55 | //! channel.write_to(::std::io::sink()).unwrap(); // write to the channel to a writer 56 | //! let string = channel.to_string(); // convert the channel to a string 57 | //! ``` 58 | //! 59 | //! # Creation 60 | //! 61 | //! Builder methods are provided to assist in the creation of channels. 62 | //! 63 | //! **Note**: This requires the `builders` feature, which is enabled by default. 64 | //! 65 | //! ``` 66 | //! use rss::ChannelBuilder; 67 | //! 68 | //! let channel = ChannelBuilder::default() 69 | //! .title("Channel Title") 70 | //! .link("http://example.com") 71 | //! .description("An RSS feed.") 72 | //! .build(); 73 | //! ``` 74 | //! 75 | //! ## Validation 76 | //! 77 | //! Validation methods are provided to validate the contents of a channel against the 78 | //! RSS specification. 79 | //! 80 | //! **Note**: This requires enabling the `validation` feature. 81 | //! 82 | //! ```rust,ignore 83 | //! use rss::Channel; 84 | //! use rss::validation::Validate; 85 | //! 86 | //! let channel = Channel::default(); 87 | //! channel.validate().unwrap(); 88 | //! ``` 89 | 90 | #[cfg(feature = "builders")] 91 | #[macro_use] 92 | extern crate derive_builder; 93 | 94 | extern crate quick_xml; 95 | 96 | #[cfg(feature = "serde")] 97 | #[cfg(feature = "validation")] 98 | extern crate chrono; 99 | #[cfg(feature = "validation")] 100 | extern crate mime; 101 | #[cfg(feature = "serde")] 102 | #[macro_use] 103 | extern crate serde; 104 | #[cfg(feature = "validation")] 105 | extern crate url; 106 | 107 | mod category; 108 | mod channel; 109 | mod cloud; 110 | mod enclosure; 111 | mod guid; 112 | mod image; 113 | mod item; 114 | mod source; 115 | mod textinput; 116 | 117 | mod error; 118 | mod toxml; 119 | mod util; 120 | 121 | /// Types and methods for namespaced extensions. 122 | pub mod extension; 123 | 124 | /// Methods for validating RSS feeds. 125 | #[cfg(feature = "validation")] 126 | pub mod validation; 127 | 128 | pub use crate::category::Category; 129 | #[cfg(feature = "builders")] 130 | pub use crate::category::CategoryBuilder; 131 | pub use crate::channel::Channel; 132 | #[cfg(feature = "builders")] 133 | pub use crate::channel::ChannelBuilder; 134 | pub use crate::cloud::Cloud; 135 | #[cfg(feature = "builders")] 136 | pub use crate::cloud::CloudBuilder; 137 | pub use crate::enclosure::Enclosure; 138 | #[cfg(feature = "builders")] 139 | pub use crate::enclosure::EnclosureBuilder; 140 | pub use crate::guid::Guid; 141 | #[cfg(feature = "builders")] 142 | pub use crate::guid::GuidBuilder; 143 | pub use crate::image::Image; 144 | #[cfg(feature = "builders")] 145 | pub use crate::image::ImageBuilder; 146 | pub use crate::item::Item; 147 | #[cfg(feature = "builders")] 148 | pub use crate::item::ItemBuilder; 149 | pub use crate::source::Source; 150 | #[cfg(feature = "builders")] 151 | pub use crate::source::SourceBuilder; 152 | pub use crate::textinput::TextInput; 153 | #[cfg(feature = "builders")] 154 | pub use crate::textinput::TextInputBuilder; 155 | 156 | pub use crate::error::Error; 157 | -------------------------------------------------------------------------------- /src/source.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::attributes::Attributes; 11 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Reader; 14 | use quick_xml::Writer; 15 | 16 | use crate::error::Error; 17 | use crate::toxml::ToXml; 18 | use crate::util::{attr_value, decode, element_text}; 19 | 20 | /// Represents the source of an RSS item. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug, Default, Clone, PartialEq)] 23 | #[cfg_attr(feature = "builders", derive(Builder))] 24 | #[cfg_attr( 25 | feature = "builders", 26 | builder( 27 | setter(into), 28 | default, 29 | build_fn(name = "build_impl", private, error = "never::Never") 30 | ) 31 | )] 32 | pub struct Source { 33 | /// The URL of the source. 34 | pub url: String, 35 | /// The title of the source. 36 | pub title: Option, 37 | } 38 | 39 | impl Source { 40 | /// Return the URL of this source. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ``` 45 | /// use rss::Source; 46 | /// 47 | /// let mut source = Source::default(); 48 | /// source.set_url("http://example.com"); 49 | /// assert_eq!(source.url(), "http://example.com"); 50 | /// ``` 51 | pub fn url(&self) -> &str { 52 | self.url.as_str() 53 | } 54 | 55 | /// Set the URL of this source. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ``` 60 | /// use rss::Source; 61 | /// 62 | /// let mut source = Source::default(); 63 | /// source.set_url("http://example.com"); 64 | /// ``` 65 | pub fn set_url(&mut self, url: V) 66 | where 67 | V: Into, 68 | { 69 | self.url = url.into(); 70 | } 71 | 72 | /// Return the title of this source. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ``` 77 | /// use rss::Source; 78 | /// 79 | /// let mut source = Source::default(); 80 | /// source.set_title("Source Title".to_string()); 81 | /// assert_eq!(source.title(), Some("Source Title")); 82 | /// ``` 83 | pub fn title(&self) -> Option<&str> { 84 | self.title.as_deref() 85 | } 86 | 87 | /// Set the title of this source. 88 | /// 89 | /// # Examples 90 | /// 91 | /// ``` 92 | /// use rss::Source; 93 | /// 94 | /// let mut source = Source::default(); 95 | /// source.set_title("Source Title".to_string()); 96 | /// ``` 97 | pub fn set_title(&mut self, title: V) 98 | where 99 | V: Into>, 100 | { 101 | self.title = title.into(); 102 | } 103 | } 104 | 105 | impl Source { 106 | /// Builds a Source from source XML 107 | pub fn from_xml( 108 | reader: &mut Reader, 109 | mut atts: Attributes, 110 | ) -> Result { 111 | let mut source = Source::default(); 112 | 113 | for attr in atts.with_checks(false).flatten() { 114 | if decode(attr.key.as_ref(), reader)?.as_ref() == "url" { 115 | source.url = attr_value(&attr, reader)?.to_string(); 116 | break; 117 | } 118 | } 119 | 120 | source.title = element_text(reader)?; 121 | Ok(source) 122 | } 123 | } 124 | 125 | impl ToXml for Source { 126 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 127 | let name = "source"; 128 | let mut element = BytesStart::new(name); 129 | element.push_attribute(("url", &*self.url)); 130 | 131 | writer.write_event(Event::Start(element))?; 132 | 133 | if let Some(ref text) = self.title { 134 | writer.write_event(Event::Text(BytesText::new(text)))?; 135 | } 136 | 137 | writer.write_event(Event::End(BytesEnd::new(name)))?; 138 | Ok(()) 139 | } 140 | } 141 | 142 | #[cfg(feature = "builders")] 143 | impl SourceBuilder { 144 | /// Builds a new `Source`. 145 | pub fn build(&self) -> Source { 146 | self.build_impl().unwrap() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/textinput.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::io::{BufRead, Write}; 9 | 10 | use quick_xml::events::attributes::Attributes; 11 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Reader; 14 | use quick_xml::Writer; 15 | 16 | use crate::error::Error; 17 | use crate::toxml::{ToXml, WriterExt}; 18 | use crate::util::{decode, element_text, skip}; 19 | 20 | /// Represents a text input for an RSS channel. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug, Default, Clone, PartialEq)] 23 | #[cfg_attr(feature = "builders", derive(Builder))] 24 | #[cfg_attr( 25 | feature = "builders", 26 | builder( 27 | setter(into), 28 | default, 29 | build_fn(name = "build_impl", private, error = "never::Never") 30 | ) 31 | )] 32 | pub struct TextInput { 33 | /// The label of the Submit button for the text input. 34 | pub title: String, 35 | /// A description of the text input. 36 | pub description: String, 37 | /// The name of the text object. 38 | pub name: String, 39 | /// The URL of the CGI script that processes the text input request. 40 | pub link: String, 41 | } 42 | 43 | impl TextInput { 44 | /// Return the title for this text field. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ``` 49 | /// use rss::TextInput; 50 | /// 51 | /// let mut text_input = TextInput::default(); 52 | /// text_input.set_title("Input Title"); 53 | /// assert_eq!(text_input.title(), "Input Title"); 54 | /// ``` 55 | pub fn title(&self) -> &str { 56 | self.title.as_str() 57 | } 58 | 59 | /// Set the title for this text field. 60 | /// 61 | /// # Examples 62 | /// 63 | /// ``` 64 | /// use rss::TextInput; 65 | /// 66 | /// let mut text_input = TextInput::default(); 67 | /// text_input.set_title("Input Title"); 68 | /// ``` 69 | pub fn set_title(&mut self, title: V) 70 | where 71 | V: Into, 72 | { 73 | self.title = title.into(); 74 | } 75 | 76 | /// Return the description of this text field. 77 | /// 78 | /// # Examples 79 | /// 80 | /// ``` 81 | /// use rss::TextInput; 82 | /// 83 | /// let mut text_input = TextInput::default(); 84 | /// text_input.set_description("Input description"); 85 | /// assert_eq!(text_input.description(), "Input description"); 86 | /// ``` 87 | pub fn description(&self) -> &str { 88 | self.description.as_str() 89 | } 90 | 91 | /// Set the description of this text field. 92 | /// 93 | /// # Examples 94 | /// 95 | /// ``` 96 | /// use rss::TextInput; 97 | /// 98 | /// let mut text_input = TextInput::default(); 99 | /// text_input.set_description("Input description"); 100 | /// ``` 101 | pub fn set_description(&mut self, description: V) 102 | where 103 | V: Into, 104 | { 105 | self.description = description.into(); 106 | } 107 | 108 | /// Return the name of the text object in this input. 109 | /// 110 | /// # Examples 111 | /// 112 | /// ``` 113 | /// use rss::TextInput; 114 | /// 115 | /// let mut text_input = TextInput::default(); 116 | /// text_input.set_name("Input name"); 117 | /// assert_eq!(text_input.name(), "Input name"); 118 | /// ``` 119 | pub fn name(&self) -> &str { 120 | self.name.as_str() 121 | } 122 | 123 | /// Set the name of the text object in this input. 124 | /// 125 | /// # Examples 126 | /// 127 | /// ``` 128 | /// use rss::TextInput; 129 | /// 130 | /// let mut text_input = TextInput::default(); 131 | /// text_input.set_name("Input name");; 132 | /// ``` 133 | pub fn set_name(&mut self, name: V) 134 | where 135 | V: Into, 136 | { 137 | self.name = name.into(); 138 | } 139 | 140 | /// Return the URL of the GCI script that processes the text input request. 141 | /// 142 | /// # Examples 143 | /// 144 | /// ``` 145 | /// use rss::TextInput; 146 | /// 147 | /// let mut text_input = TextInput::default(); 148 | /// text_input.set_link("http://example.com/submit"); 149 | /// assert_eq!(text_input.link(), "http://example.com/submit"); 150 | /// ``` 151 | pub fn link(&self) -> &str { 152 | self.link.as_str() 153 | } 154 | 155 | /// Set the URL of the GCI script that processes the text input request. 156 | /// 157 | /// # Examples 158 | /// 159 | /// ``` 160 | /// use rss::TextInput; 161 | /// 162 | /// let mut text_input = TextInput::default(); 163 | /// text_input.set_link("http://example.com/submit"); 164 | /// ``` 165 | pub fn set_link(&mut self, link: V) 166 | where 167 | V: Into, 168 | { 169 | self.link = link.into(); 170 | } 171 | } 172 | 173 | impl TextInput { 174 | /// Builds a TextInput from source XML 175 | pub fn from_xml(reader: &mut Reader, _: Attributes) -> Result { 176 | let mut text_input = TextInput::default(); 177 | let mut buf = Vec::new(); 178 | 179 | loop { 180 | match reader.read_event_into(&mut buf)? { 181 | Event::Start(element) => match decode(element.name().as_ref(), reader)?.as_ref() { 182 | "title" => text_input.title = element_text(reader)?.unwrap_or_default(), 183 | "description" => { 184 | text_input.description = element_text(reader)?.unwrap_or_default() 185 | } 186 | "name" => text_input.name = element_text(reader)?.unwrap_or_default(), 187 | "link" => text_input.link = element_text(reader)?.unwrap_or_default(), 188 | _ => skip(element.name(), reader)?, 189 | }, 190 | Event::End(_) => break, 191 | Event::Eof => return Err(Error::Eof), 192 | _ => {} 193 | } 194 | 195 | buf.clear(); 196 | } 197 | 198 | Ok(text_input) 199 | } 200 | } 201 | 202 | impl ToXml for TextInput { 203 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 204 | let name = "textInput"; 205 | 206 | writer.write_event(Event::Start(BytesStart::new(name)))?; 207 | 208 | writer.write_text_element("title", &self.title)?; 209 | writer.write_text_element("description", &self.description)?; 210 | writer.write_text_element("name", &self.name)?; 211 | writer.write_text_element("link", &self.link)?; 212 | 213 | writer.write_event(Event::End(BytesEnd::new(name)))?; 214 | Ok(()) 215 | } 216 | } 217 | 218 | #[cfg(feature = "builders")] 219 | impl TextInputBuilder { 220 | /// Builds a new `TextInput`. 221 | pub fn build(&self) -> TextInput { 222 | self.build_impl().unwrap() 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/toxml.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::BTreeMap; 9 | use std::io::Write; 10 | 11 | use quick_xml::events::{BytesCData, BytesEnd, BytesStart, BytesText, Event}; 12 | use quick_xml::Error as XmlError; 13 | use quick_xml::Writer; 14 | 15 | pub trait ToXml { 16 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError>; 17 | 18 | fn used_namespaces(&self) -> BTreeMap { 19 | BTreeMap::new() 20 | } 21 | } 22 | 23 | impl<'a, T: ToXml> ToXml for &'a T { 24 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 25 | (*self).to_xml(writer) 26 | } 27 | } 28 | 29 | pub trait WriterExt { 30 | fn write_text_element(&mut self, name: N, text: T) -> Result<(), XmlError> 31 | where 32 | N: AsRef, 33 | T: AsRef; 34 | 35 | fn write_text_elements(&mut self, name: N, values: I) -> Result<(), XmlError> 36 | where 37 | N: AsRef, 38 | T: AsRef, 39 | I: IntoIterator; 40 | 41 | fn write_cdata_element(&mut self, name: N, text: T) -> Result<(), XmlError> 42 | where 43 | N: AsRef, 44 | T: AsRef; 45 | 46 | fn write_object(&mut self, object: T) -> Result<(), XmlError> 47 | where 48 | T: ToXml; 49 | 50 | fn write_objects(&mut self, objects: I) -> Result<(), XmlError> 51 | where 52 | T: ToXml, 53 | I: IntoIterator; 54 | } 55 | 56 | impl WriterExt for Writer { 57 | fn write_text_element(&mut self, name: N, text: T) -> Result<(), XmlError> 58 | where 59 | N: AsRef, 60 | T: AsRef, 61 | { 62 | let name = name.as_ref(); 63 | self.write_event(Event::Start(BytesStart::new(name)))?; 64 | self.write_event(Event::Text(BytesText::new(text.as_ref())))?; 65 | self.write_event(Event::End(BytesEnd::new(name)))?; 66 | Ok(()) 67 | } 68 | 69 | fn write_text_elements(&mut self, name: N, values: I) -> Result<(), XmlError> 70 | where 71 | N: AsRef, 72 | T: AsRef, 73 | I: IntoIterator, 74 | { 75 | for value in values { 76 | self.write_text_element(&name, value)?; 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | fn write_cdata_element(&mut self, name: N, text: T) -> Result<(), XmlError> 83 | where 84 | N: AsRef, 85 | T: AsRef, 86 | { 87 | let name = name.as_ref(); 88 | self.write_event(Event::Start(BytesStart::new(name)))?; 89 | BytesCData::escaped(text.as_ref()) 90 | .try_for_each(|event| self.write_event(Event::CData(event)))?; 91 | self.write_event(Event::End(BytesEnd::new(name)))?; 92 | Ok(()) 93 | } 94 | 95 | #[inline] 96 | fn write_object(&mut self, object: T) -> Result<(), XmlError> 97 | where 98 | T: ToXml, 99 | { 100 | object.to_xml(self) 101 | } 102 | 103 | fn write_objects(&mut self, objects: I) -> Result<(), XmlError> 104 | where 105 | T: ToXml, 106 | I: IntoIterator, 107 | { 108 | for object in objects { 109 | object.to_xml(self)?; 110 | } 111 | 112 | Ok(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::borrow::Cow; 9 | use std::io::BufRead; 10 | 11 | use quick_xml::events::attributes::Attribute; 12 | use quick_xml::events::Event; 13 | use quick_xml::name::QName; 14 | use quick_xml::Reader; 15 | 16 | use crate::error::Error; 17 | 18 | pub(crate) fn decode<'s, B: BufRead>( 19 | bytes: &'s [u8], 20 | reader: &Reader, 21 | ) -> Result, Error> { 22 | let text = reader.decoder().decode(bytes)?; 23 | Ok(text) 24 | } 25 | 26 | pub(crate) fn attr_value<'s, B: BufRead>( 27 | attr: &'s Attribute<'s>, 28 | reader: &Reader, 29 | ) -> Result, Error> { 30 | let value = attr.decode_and_unescape_value(reader.decoder())?; 31 | Ok(value) 32 | } 33 | 34 | pub(crate) fn skip(end: QName<'_>, reader: &mut Reader) -> Result<(), Error> { 35 | reader.read_to_end_into(end, &mut Vec::new())?; 36 | Ok(()) 37 | } 38 | 39 | pub fn element_text(reader: &mut Reader) -> Result, Error> { 40 | let mut content = String::new(); 41 | let mut buf = Vec::new(); 42 | 43 | loop { 44 | match reader.read_event_into(&mut buf)? { 45 | Event::Start(element) => { 46 | skip(element.name(), reader)?; 47 | } 48 | Event::Text(element) => { 49 | let decoded = element.unescape()?; 50 | content.push_str(decoded.as_ref()); 51 | } 52 | Event::CData(element) => { 53 | content.push_str(decode(&element, reader)?.as_ref()); 54 | } 55 | Event::End(_) | Event::Eof => break, 56 | _ => {} 57 | } 58 | buf.clear(); 59 | } 60 | 61 | Ok(Some(content.trim().to_owned()).filter(|c| !c.is_empty())) 62 | } 63 | -------------------------------------------------------------------------------- /src/validation.rs: -------------------------------------------------------------------------------- 1 | // This file is part of rss. 2 | // 3 | // Copyright © 2015-2021 The rust-syndication Developers 4 | // 5 | // This program is free software; you can redistribute it and/or modify 6 | // it under the terms of the MIT License and/or Apache 2.0 License. 7 | 8 | use std::collections::HashSet; 9 | use std::error::Error as StdError; 10 | use std::fmt; 11 | use std::num::ParseIntError; 12 | 13 | use chrono::DateTime; 14 | use chrono::ParseError as DateParseError; 15 | use mime::FromStrError as MimeParseError; 16 | use mime::Mime; 17 | use url::ParseError as UrlParseError; 18 | use url::Url; 19 | 20 | use crate::{Category, Channel, Cloud, Enclosure, Image, Item, Source, TextInput}; 21 | 22 | #[derive(Debug)] 23 | /// Errors that occur during validation. 24 | pub enum ValidationError { 25 | /// An error while parsing a string to a date. 26 | DateParsing(DateParseError), 27 | /// An error while parsing a string to an integer. 28 | IntParsing(ParseIntError), 29 | /// An error while parsing a string to a URL. 30 | UrlParsing(UrlParseError), 31 | /// An error while parsing a string to a MIME type. 32 | MimeParsing(MimeParseError), 33 | /// A different validation error. 34 | Validation(String), 35 | } 36 | 37 | impl StdError for ValidationError { 38 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 39 | match *self { 40 | ValidationError::DateParsing(ref err) => Some(err), 41 | ValidationError::IntParsing(ref err) => Some(err), 42 | ValidationError::UrlParsing(ref err) => Some(err), 43 | _ => None, 44 | } 45 | } 46 | } 47 | 48 | impl fmt::Display for ValidationError { 49 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 50 | match *self { 51 | ValidationError::DateParsing(ref err) => err.fmt(f), 52 | ValidationError::IntParsing(ref err) => err.fmt(f), 53 | ValidationError::UrlParsing(ref err) => err.fmt(f), 54 | ValidationError::MimeParsing(_) => write!(f, "Unable to parse MIME type"), 55 | ValidationError::Validation(ref s) => write!(f, "{}", s), 56 | } 57 | } 58 | } 59 | 60 | impl From for ValidationError { 61 | fn from(err: DateParseError) -> Self { 62 | ValidationError::DateParsing(err) 63 | } 64 | } 65 | 66 | impl From for ValidationError { 67 | fn from(err: ParseIntError) -> Self { 68 | ValidationError::IntParsing(err) 69 | } 70 | } 71 | 72 | impl From for ValidationError { 73 | fn from(err: UrlParseError) -> Self { 74 | ValidationError::UrlParsing(err) 75 | } 76 | } 77 | 78 | impl From for ValidationError { 79 | fn from(err: MimeParseError) -> Self { 80 | ValidationError::MimeParsing(err) 81 | } 82 | } 83 | 84 | /// A trait to support data validation. 85 | pub trait Validate { 86 | /// Validate the data against the RSS specification. 87 | fn validate(&self) -> Result<(), ValidationError>; 88 | } 89 | 90 | macro_rules! validate { 91 | ($e: expr, $msg: expr) => {{ 92 | if !($e) { 93 | return Err(ValidationError::Validation(String::from($msg))); 94 | } 95 | }}; 96 | } 97 | 98 | impl Validate for Channel { 99 | fn validate(&self) -> Result<(), ValidationError> { 100 | Url::parse(self.link())?; 101 | 102 | for category in self.categories() { 103 | category.validate()?; 104 | } 105 | 106 | if let Some(cloud) = self.cloud() { 107 | cloud.validate()?; 108 | } 109 | 110 | if let Some(docs) = self.docs() { 111 | Url::parse(docs)?; 112 | } 113 | 114 | if let Some(image) = self.image() { 115 | image.validate()?; 116 | } 117 | 118 | for item in self.items() { 119 | item.validate()?; 120 | } 121 | 122 | if let Some(last_build_date) = self.last_build_date() { 123 | DateTime::parse_from_rfc2822(last_build_date)?; 124 | } 125 | 126 | if let Some(pub_date) = self.pub_date() { 127 | DateTime::parse_from_rfc2822(pub_date)?; 128 | } 129 | 130 | for hour in self.skip_hours() { 131 | let hour = hour.parse::()?; 132 | validate!( 133 | (0..=23).contains(&hour), 134 | "Channel skip hour is not between 0 and 23" 135 | ); 136 | } 137 | 138 | let valid_days = { 139 | let mut set = HashSet::with_capacity(7); 140 | set.insert("Monday"); 141 | set.insert("Tuesday"); 142 | set.insert("Wednesday"); 143 | set.insert("Thursday"); 144 | set.insert("Friday"); 145 | set.insert("Saturday"); 146 | set.insert("Sunday"); 147 | set 148 | }; 149 | 150 | for day in self.skip_days() { 151 | validate!( 152 | valid_days.contains(day.as_str()), 153 | format!("Unknown skip day: {}", day) 154 | ); 155 | } 156 | 157 | if let Some(text_input) = self.text_input() { 158 | text_input.validate()?; 159 | } 160 | 161 | if let Some(ttl) = self.ttl() { 162 | let ttl = ttl.parse::()?; 163 | validate!(ttl > 0, "Channel TTL is not greater than 0"); 164 | } 165 | 166 | Ok(()) 167 | } 168 | } 169 | 170 | impl Validate for Category { 171 | fn validate(&self) -> Result<(), ValidationError> { 172 | if let Some(domain) = self.domain() { 173 | Url::parse(domain)?; 174 | } 175 | Ok(()) 176 | } 177 | } 178 | 179 | impl Validate for Cloud { 180 | fn validate(&self) -> Result<(), ValidationError> { 181 | let port = self.port().parse::()?; 182 | validate!(port > 0, "Cloud port must be greater than 0"); 183 | Url::parse(self.domain())?; 184 | validate!( 185 | vec!["xml-rpc", "soap", "http-post"].contains(&self.protocol()), 186 | format!("Unknown cloud protocol: {}", self.protocol()) 187 | ); 188 | Ok(()) 189 | } 190 | } 191 | 192 | impl Validate for Enclosure { 193 | fn validate(&self) -> Result<(), ValidationError> { 194 | Url::parse(self.url())?; 195 | self.mime_type().parse::()?; 196 | let length = self.length().parse::()?; 197 | validate!(length > 0, "Enclosure length is not greater than 0"); 198 | Ok(()) 199 | } 200 | } 201 | 202 | impl Validate for TextInput { 203 | fn validate(&self) -> Result<(), ValidationError> { 204 | Url::parse(self.link())?; 205 | Ok(()) 206 | } 207 | } 208 | 209 | impl Validate for Image { 210 | fn validate(&self) -> Result<(), ValidationError> { 211 | Url::parse(self.link())?; 212 | Url::parse(self.url())?; 213 | 214 | if let Some(width) = self.width() { 215 | let width = width.parse::()?; 216 | validate!( 217 | (0..=144).contains(&width), 218 | "Image width is not between 0 and 144" 219 | ); 220 | } 221 | 222 | if let Some(height) = self.height() { 223 | let height = height.parse::()?; 224 | validate!( 225 | (0..=144).contains(&height), 226 | "Image height is not between 0 and 144" 227 | ); 228 | } 229 | 230 | Ok(()) 231 | } 232 | } 233 | 234 | impl Validate for Item { 235 | fn validate(&self) -> Result<(), ValidationError> { 236 | if let Some(link) = self.link() { 237 | Url::parse(link)?; 238 | } 239 | 240 | if let Some(comments) = self.comments() { 241 | Url::parse(comments)?; 242 | } 243 | 244 | if let Some(enclosure) = self.enclosure() { 245 | enclosure.validate()?; 246 | } 247 | 248 | if let Some(pub_date) = self.pub_date() { 249 | DateTime::parse_from_rfc2822(pub_date)?; 250 | } 251 | 252 | if let Some(source) = self.source() { 253 | source.validate()?; 254 | } 255 | 256 | Ok(()) 257 | } 258 | } 259 | 260 | impl Validate for Source { 261 | fn validate(&self) -> Result<(), ValidationError> { 262 | Url::parse(self.url())?; 263 | Ok(()) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /tests/data/category.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Category 2 6 | 7 | 8 | Category 2 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/data/channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <![CDATA[Title]]> 5 | http://example.com/ 6 | Description 7 | en-US 8 | editor@example.com 9 | webmaster@example.com 10 | Sat, 27 Aug 2016 00:00:00 GMT 11 | Sat, 27 Aug 2016 09:00:00 GMT 12 | Generator 13 | http://blogs.law.harvard.edu/tech/rss 14 | 60 15 | 16 | 6 17 | 8 18 | 19 | 20 | 21 | Tuesday 22 | Thursday 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/cloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/data/content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | link.]]> 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/data/dublincore.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Contributor 1 5 | Contributor 2 6 | Coverage 7 | Creator 8 | 2016-08-27 9 | Description 10 | text/plain 11 | Identifier 12 | en-US 13 | Publisher 14 | Relation 15 | Company 16 | Source 17 | Subject 18 | Title 19 | Type 20 | 21 | Contributor 1 22 | Contributor 2 23 | Coverage 24 | Creator 25 | 2016-08-27 26 | Description 27 | text/plain 28 | Identifier 29 | en-US 30 | Publisher 31 | Relation 32 | Company 33 | Source 34 | Subject 35 | Title 36 | Type 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/data/dublincore_altprefix.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Contributor 1 5 | Contributor 2 6 | Coverage 7 | Creator 8 | 2016-08-27 9 | Description 10 | text/plain 11 | Identifier 12 | en-US 13 | Publisher 14 | Relation 15 | Company 16 | Source 17 | Subject 18 | Title 19 | Type 20 | 21 | Contributor 1 22 | Contributor 2 23 | Coverage 24 | Creator 25 | 2016-08-27 26 | Description 27 | text/plain 28 | Identifier 29 | en-US 30 | Publisher 31 | Relation 32 | Company 33 | Source 34 | Subject 35 | Title 36 | Type 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/data/enclosure.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/data/extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Contributor 1 7 | Contributor 2 8 | 9 | Child 1 10 | Child 2 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/guid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | abc 6 | 7 | 8 | def?g=h&i=j 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/data/image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | http://example.org/url 7 | http://example.org/link 8 | 100 9 | 200 10 | Description 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/data/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <![CDATA[Title]]> 6 | http://example.com/ 7 | Description 8 | author@example.com 9 | Comments 10 | Sat, 27 Aug 2016 00:00:00 GMT 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/data/itunes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Author 5 | yes 6 | 7 | 8 | 9 | 10 | 11 | no 12 | yes 13 | http://example.com/feed/ 14 | 15 | Name 16 | example@example.com 17 | 18 | Subtitle 19 | Summary 20 | key1,key2,key3 21 | episodic 22 | 23 | Author 24 | yes 25 | 26 | 01:22:33 27 | yes 28 | no 29 | 1 30 | Subtitle 31 | Summary 32 | key1,key2,key3 33 | 2 34 | 3 35 | trailer 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/data/mixed_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <a href="http://example.com/">link</a> 6 | Title 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/data/rss090.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Mozilla Dot Org 8 | http://www.mozilla.org 9 | the Mozilla Organization 10 | web site 11 | 12 | 13 | 14 | Mozilla 15 | http://www.mozilla.org/images/moz.gif 16 | http://www.mozilla.org 17 | 18 | 19 | 20 | New Status Updates 21 | http://www.mozilla.org/status/ 22 | 23 | 24 | 25 | Bugzilla Reorganized 26 | http://www.mozilla.org/bugs/ 27 | 28 | 29 | 30 | Mozilla Party, 2.0! 31 | http://www.mozilla.org/party/1999/ 32 | 33 | 34 | 35 | Unix Platform Parity 36 | http://www.mozilla.org/build/unix.html 37 | 38 | 39 | 40 | NPL 1.0M published 41 | http://www.mozilla.org/NPL/NPL-1.0M.html 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/data/rss091.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WriteTheWeb 5 | http://writetheweb.com 6 | News for web users that write back 7 | en-us 8 | Copyright 2000, WriteTheWeb team. 9 | editor@writetheweb.com 10 | webmaster@writetheweb.com 11 | 12 | WriteTheWeb 13 | http://writetheweb.com/images/mynetscape88.gif 14 | http://writetheweb.com 15 | 88 16 | 31 17 | News for web users that write back 18 | 19 | 20 | Giving the world a pluggable Gnutella 21 | http://writetheweb.com/read.php?item=24 22 | WorldOS is a framework on which to build programs that work like Freenet or Gnutella -allowing distributed applications using peer-to-peer routing. 23 | 24 | 25 | Syndication discussions hot up 26 | http://writetheweb.com/read.php?item=23 27 | After a period of dormancy, the Syndication mailing list has become active again, with contributions from leaders in traditional media and Web syndication. 28 | 29 | 30 | Personal web server integrates file sharing and messaging 31 | http://writetheweb.com/read.php?item=22 32 | The Magi Project is an innovative project to create a combined personal web server and messaging system that enables the sharing and synchronization of information across desktop, laptop and palmtop devices. 33 | 34 | 35 | Syndication and Metadata 36 | http://writetheweb.com/read.php?item=21 37 | RSS is probably the best known metadata format around. RDF is probably one of the least understood. In this essay, published on my O'Reilly Network weblog, I argue that the next generation of RSS should be based on RDF. 38 | 39 | 40 | UK bloggers get organised 41 | http://writetheweb.com/read.php?item=20 42 | Looks like the weblogs scene is gathering pace beyond the shores of the US. There's now a UK-specific page on weblogs.com, and a mailing list at egroups. 43 | 44 | 45 | Yournamehere.com more important than anything 46 | http://writetheweb.com/read.php?item=19 47 | Whatever you're publishing on the web, your site name is the most valuable asset you have, according to Carl Steadman. 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/data/rss092.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dave Winer: Grateful Dead 6 | http://www.scripting.com/blog/categories/gratefulDead.html 7 | A high-fidelity Grateful Dead song every day. This is where we're experimenting with enclosures on RSS news items that download when you're not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. 8 | Fri, 13 Apr 2001 19:23:02 GMT 9 | http://backend.userland.com/rss092 10 | dave@userland.com (Dave Winer) 11 | dave@userland.com (Dave Winer) 12 | 13 | 14 | It's been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it's #16 on the hotlist of upstreaming Radio users, there's no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight's song is a live version of Weather Report Suite from Dick's Picks Volume 7. It's wistful music. Of course a beautiful song, oft-quoted here on Scripting News. <i>A little change, the wind and rain.</i> 15 | 16 | 17 | 18 | 19 | Kevin Drennan started a <a href="http://deadend.editthispage.com/">Grateful Dead Weblog</a>. Hey it's cool, he even has a <a href="http://deadend.editthispage.com/directory/61">directory</a>. <i>A Frontier 7 feature.</i> 20 | Scripting News 21 | 22 | 23 | <a href="http://arts.ucsc.edu/GDead/AGDL/other1.html">The Other One</a>, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time. 24 | 25 | 26 | 27 | This is a test of a change I just made. Still diggin.. 28 | 29 | 30 | The HTML rendering almost <a href="http://validator.w3.org/check/referer">validates</a>. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don't know. If you're blind send me an email if u cn rd ths. 31 | 32 | 33 | <a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin's_Tower.txt">Franklin's Tower</a>, a live version from One From The Vault. 34 | 35 | 36 | 37 | Moshe Weitzman says Shakedown Street is what I'm lookin for for tonight. I'm listening right now. It's one of my favorites. "Don't tell me this town ain't got no heart." Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? "Spanish lady come to me.." 38 | Scripting News 39 | 40 | 41 | <a href="http://www.scripting.com/mp3s/youWinAgain.mp3">The news is out</a>, all over town..<p> 42 | You've been seen, out runnin round. <p> 43 | The lyrics are <a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt">here</a>, short and sweet. <p> 44 | <i>You win again!</i> 45 | 46 | 47 | 48 | 49 | <a href="http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm">Weather Report Suite</a>: "Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain." 50 | 51 | 52 | 53 | <a href="http://arts.ucsc.edu/gdead/agdl/darkstar.html">Dark Star</a> crashes, pouring its light into ashes. 54 | 55 | 56 | 57 | DaveNet: <a href="http://davenet.userland.com/2001/01/21/theUsBlues">The U.S. Blues</a>. 58 | 59 | 60 | Still listening to the US Blues. <i>"Wave that flag, wave it wide and high.."</i> Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let's take it back. Hey I'm still a hippie. <i>"You could call this song The United States Blues."</i> 61 | 62 | 63 | <a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i> 64 | 65 | 66 | 67 | Grateful Dead: "Tennessee, Tennessee, ain't no place I'd rather be." 68 | 69 | 70 | 71 | Ed Cone: "Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band's finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha." 72 | 73 | 74 | 75 | <a href="http://arts.ucsc.edu/GDead/AGDL/fotd.html">Tonight's Song</a>: "If I get home before daylight I just might get some sleep tonight." 76 | 77 | 78 | 79 | <a href="http://arts.ucsc.edu/GDead/AGDL/uncle.html">Tonight's song</a>: "Come hear Uncle John's Band by the river side. Got some things to talk about here beside the rising tide." 80 | 81 | 82 | 83 | <a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt">Me and My Uncle</a>: "I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road." 84 | 85 | 86 | 87 | 88 | Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain't worth a dime, if you don't lay em down. 89 | 90 | 91 | 92 | Two-Way-Web: <a href="http://www.thetwowayweb.com/payloadsForRss">Payloads for RSS</a>. "When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn't want to hear about it." 93 | 94 | 95 | A touch of gray, kinda suits you anyway.. 96 | 97 | 98 | 99 | <a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i> 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /tests/data/rss1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | XML.com 10 | http://xml.com/pub 11 | 12 | XML.com features a rich mix of information and services 13 | for the XML community. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | XML.com 31 | http://www.xml.com 32 | http://xml.com/universal/images/xml_tiny.gif 33 | 34 | 35 | 36 | Processing Inclusions with XSLT 37 | http://xml.com/pub/2000/08/09/xslt/xslt.html 38 | 39 | Processing document inclusions with general XML tools can be 40 | problematic. This article proposes a way of preserving inclusion 41 | information through SAX-based processing. 42 | 43 | 44 | 45 | 46 | Putting RDF to Work 47 | http://xml.com/pub/2000/08/09/rdfdb/index.html 48 | 49 | Tool and API support for the Resource Description Framework 50 | is slowly coming of age. Edd Dumbill takes a look at RDFDB, 51 | one of the most exciting new RDF toolkits. 52 | 53 | 54 | 55 | 56 | Search XML.com 57 | Search XML.com's XML collection 58 | s 59 | http://search.xml.com 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/data/rss2_with_atom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Liftoff News 5 | http://liftoff.msfc.nasa.gov/ 6 | Liftoff to Space Exploration. 7 | en-us 8 | Tue, 10 Jun 2003 04:00:00 GMT 9 | Tue, 10 Jun 2003 09:41:01 GMT 10 | http://blogs.law.harvard.edu/tech/rss 11 | Weblog Editor 2.0 12 | editor@example.com 13 | webmaster@example.com 14 | 15 | 16 | Star City 17 | http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp 18 | 19 | How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>. 20 | Tue, 03 Jun 2003 09:39:21 GMT 21 | http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 22 | 23 | 24 | 25 | Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st. 26 | Fri, 30 May 2003 11:06:42 GMT 27 | http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 28 | 29 | 30 | The Engine That Does More 31 | http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp 32 | 33 | Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that. 34 | Tue, 27 May 2003 08:37:32 GMT 35 | http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/data/rss2sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Liftoff News 5 | http://liftoff.msfc.nasa.gov/ 6 | Liftoff to Space Exploration. 7 | en-us 8 | Tue, 10 Jun 2003 04:00:00 GMT 9 | Tue, 10 Jun 2003 09:41:01 GMT 10 | http://blogs.law.harvard.edu/tech/rss 11 | Weblog Editor 2.0 12 | editor@example.com 13 | webmaster@example.com 14 | 15 | Star City 16 | http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp 17 | How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>. 18 | Tue, 03 Jun 2003 09:39:21 GMT 19 | http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 20 | 21 | 22 | Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st. 23 | Fri, 30 May 2003 11:06:42 GMT 24 | http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 25 | 26 | 27 | The Engine That Does More 28 | http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp 29 | Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that. 30 | Tue, 27 May 2003 08:37:32 GMT 31 | http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 32 | 33 | 34 | Astronauts' Dirty Laundry 35 | http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp 36 | Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options. 37 | Tue, 20 May 2003 08:56:02 GMT 38 | http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/data/source.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Feed 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/data/syndication.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | Meerkat 11 | http://meerkat.oreillynet.com 12 | Meerkat: An Open Wire Service 13 | hourly 14 | 2 15 | 2000-01-01T12:00+00:00 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Meerkat Powered! 31 | http://meerkat.oreillynet.com/icons/meerkat-powered.jpg 32 | http://meerkat.oreillynet.com 33 | 34 | 35 | 36 | XML: A Disruptive Technology 37 | http://c.moreover.com/click/here.pl?r123 38 | 39 | XML is placing increasingly heavy loads on the existing technical 40 | infrastructure of the Internet. 41 | 42 | 43 | 44 | 45 | Search Meerkat 46 | Search Meerkat's RSS Database... 47 | s 48 | http://meerkat.oreillynet.com/ 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/data/textinput.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | Name 7 | http://example.com/ 8 | Description 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/data/verify_write_format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Title 5 | http://example.com/ 6 | Description 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/write.rs: -------------------------------------------------------------------------------- 1 | extern crate rss; 2 | 3 | use rss::{ 4 | extension, extension::itunes::ITunesChannelExtensionBuilder, CategoryBuilder, Channel, 5 | ChannelBuilder, CloudBuilder, EnclosureBuilder, GuidBuilder, ImageBuilder, Item, ItemBuilder, 6 | SourceBuilder, TextInputBuilder, 7 | }; 8 | use std::collections::BTreeMap; 9 | 10 | macro_rules! test_write { 11 | ($channel: ident) => {{ 12 | let output = $channel.to_string(); 13 | let parsed = output.parse::().expect("failed to parse xml"); 14 | assert_eq!($channel, parsed); 15 | }}; 16 | } 17 | 18 | #[test] 19 | fn write_channel() { 20 | let input = include_str!("data/channel.xml"); 21 | let channel = input.parse::().expect("failed to parse xml"); 22 | test_write!(channel); 23 | } 24 | 25 | #[test] 26 | fn write_item() { 27 | let input = include_str!("data/item.xml"); 28 | let channel = input.parse::().expect("failed to parse xml"); 29 | test_write!(channel); 30 | } 31 | 32 | #[test] 33 | fn write_content() { 34 | let input = include_str!("data/content.xml"); 35 | let channel = input.parse::().expect("failed to parse xml"); 36 | test_write!(channel); 37 | } 38 | 39 | #[test] 40 | fn write_source() { 41 | let input = include_str!("data/source.xml"); 42 | let channel = input.parse::().expect("failed to parse xml"); 43 | test_write!(channel); 44 | } 45 | 46 | #[test] 47 | fn write_guid() { 48 | let input = include_str!("data/guid.xml"); 49 | let channel = input.parse::().expect("failed to parse xml"); 50 | test_write!(channel); 51 | } 52 | 53 | #[test] 54 | fn write_enclosure() { 55 | let input = include_str!("data/enclosure.xml"); 56 | let channel = input.parse::().expect("failed to parse xml"); 57 | test_write!(channel); 58 | } 59 | 60 | #[test] 61 | fn write_category() { 62 | let input = include_str!("data/category.xml"); 63 | let channel = input.parse::().expect("failed to parse xml"); 64 | test_write!(channel); 65 | } 66 | 67 | #[test] 68 | fn write_image() { 69 | let input = include_str!("data/image.xml"); 70 | let channel = input.parse::().expect("failed to parse xml"); 71 | test_write!(channel); 72 | } 73 | 74 | #[test] 75 | fn write_mixed_content() { 76 | let input = include_str!("data/mixed_content.xml"); 77 | let channel = input.parse::().expect("failed to parse xml"); 78 | test_write!(channel); 79 | } 80 | 81 | #[test] 82 | fn write_cloud() { 83 | let input = include_str!("data/cloud.xml"); 84 | let channel = input.parse::().expect("failed to parse xml"); 85 | test_write!(channel); 86 | } 87 | 88 | #[test] 89 | fn write_textinput() { 90 | let input = include_str!("data/textinput.xml"); 91 | let channel = input.parse::().expect("failed to parse xml"); 92 | test_write!(channel); 93 | } 94 | 95 | #[test] 96 | fn write_extension() { 97 | let input = include_str!("data/extension.xml"); 98 | let channel = input.parse::().expect("failed to parse xml"); 99 | test_write!(channel); 100 | } 101 | 102 | #[test] 103 | fn write_itunes() { 104 | let input = include_str!("data/itunes.xml"); 105 | let channel = input.parse::().expect("failed to parse xml"); 106 | test_write!(channel); 107 | } 108 | 109 | #[test] 110 | fn write_itunes_namespace() { 111 | let itunes_extension = ITunesChannelExtensionBuilder::default() 112 | .author(Some("author".to_string())) 113 | .build(); 114 | let channel = rss::ChannelBuilder::default() 115 | .title("Channel Title") 116 | .link("http://example.com") 117 | .description("Channel Description") 118 | .itunes_ext(itunes_extension) 119 | .build(); 120 | 121 | let xml = String::from_utf8(channel.pretty_write_to(Vec::new(), b' ', 4).unwrap()).unwrap(); 122 | assert_eq!( 123 | xml, 124 | r##" 125 | 126 | 127 | Channel Title 128 | http://example.com 129 | Channel Description 130 | author 131 | 132 | "## 133 | ); 134 | } 135 | 136 | #[test] 137 | fn write_dublincore() { 138 | let input = include_str!("data/dublincore.xml"); 139 | let channel = input.parse::().expect("failed to parse xml"); 140 | test_write!(channel); 141 | } 142 | 143 | #[test] 144 | fn write_syndication() { 145 | let input = include_str!("data/syndication.xml"); 146 | let channel = input.parse::().expect("failed to parse xml"); 147 | test_write!(channel); 148 | } 149 | 150 | #[test] 151 | fn verify_write_format() { 152 | let item = ItemBuilder::default() 153 | .itunes_ext(extension::itunes::ITunesItemExtension::default()) 154 | .dublin_core_ext(extension::dublincore::DublinCoreExtension::default()) 155 | .build(); 156 | 157 | let mut namespaces: BTreeMap = BTreeMap::new(); 158 | namespaces.insert("ext".to_string(), "http://example.com/".to_string()); 159 | 160 | let channel = ChannelBuilder::default() 161 | .title("Title") 162 | .link("http://example.com/") 163 | .description("Description") 164 | .items(vec![item]) 165 | .namespaces(namespaces) 166 | .build(); 167 | 168 | let output = include_str!("data/verify_write_format.xml") 169 | .replace("\n", "") 170 | .replace("\r", "") 171 | .replace("\t", ""); 172 | 173 | assert_eq!(channel.to_string(), output); 174 | } 175 | 176 | #[test] 177 | fn test_content_namespace() { 178 | let channel = ChannelBuilder::default() 179 | .item( 180 | ItemBuilder::default() 181 | .content("Lorem ipsum dolor sit amet".to_owned()) 182 | .build(), 183 | ) 184 | .build(); 185 | let xml = channel.to_string(); 186 | 187 | assert!(xml.contains("xmlns:content=")); 188 | assert!(!xml.contains("xmlns:dc=")); 189 | assert!(!xml.contains("xmlns:itunes=")); 190 | } 191 | 192 | #[test] 193 | fn test_namespaces() { 194 | let channel = ChannelBuilder::default() 195 | .items(vec![ItemBuilder::default() 196 | .content("Lorem ipsum dolor sit amet".to_owned()) 197 | .itunes_ext( 198 | extension::itunes::ITunesItemExtensionBuilder::default() 199 | .author("Anonymous".to_owned()) 200 | .build(), 201 | ) 202 | .dublin_core_ext( 203 | extension::dublincore::DublinCoreExtensionBuilder::default() 204 | .languages(vec!["English".to_owned(), "Deutsch".to_owned()]) 205 | .build(), 206 | ) 207 | .build()]) 208 | .build(); 209 | let xml = channel.to_string(); 210 | 211 | assert!(xml.contains("xmlns:content=")); 212 | assert!(xml.contains("xmlns:dc=")); 213 | assert!(xml.contains("xmlns:itunes=")); 214 | } 215 | 216 | #[test] 217 | fn test_escape() { 218 | let mut channel = ChannelBuilder::default() 219 | .image( 220 | ImageBuilder::default() 221 | .url("http://example.com/image.png") 222 | .link("http://example.com/") 223 | .width("120px".to_string()) 224 | .height("80px".to_string()) 225 | .build(), 226 | ) 227 | .categories(vec![CategoryBuilder::default().name("this & that").build()]) 228 | .cloud( 229 | CloudBuilder::default() 230 | .domain("example.com") 231 | .port("80") 232 | .path("/rpc?r=1&p=2&c=3") 233 | .register_procedure("notify") 234 | .protocol("xml-rpc") 235 | .build(), 236 | ) 237 | .items(vec![ItemBuilder::default() 238 | .guid( 239 | GuidBuilder::default() 240 | .value("51ed8fb6-e7db-4b1d-a75a-0d1621e895b4") 241 | .build(), 242 | ) 243 | .description("let's try & break this ]]>, shall we?".to_owned()) 244 | .content("Lorem ipsum dolor sit amet".to_owned()) 245 | .enclosure( 246 | EnclosureBuilder::default() 247 | .url("http://example.com?test=1&another=true") 248 | .build(), 249 | ) 250 | .source( 251 | SourceBuilder::default() 252 | .url("http://example.com?test=2&another=false") 253 | .title("".to_owned()) 254 | .build(), 255 | ) 256 | .build()]) 257 | .text_input( 258 | TextInputBuilder::default() 259 | .description("Search") 260 | .title("Search") 261 | .link("http://example.com/search?") 262 | .name("q") 263 | .build(), 264 | ) 265 | .build(); 266 | 267 | let mut attrs = BTreeMap::new(); 268 | attrs.insert("ext:key1".to_owned(), "value 1&2".to_owned()); 269 | attrs.insert("ext:key2".to_owned(), "value 2&3".to_owned()); 270 | 271 | let mut extension_tag = BTreeMap::new(); 272 | extension_tag.insert( 273 | "tag".to_owned(), 274 | vec![extension::ExtensionBuilder::default() 275 | .name("ext:tag") 276 | .attrs(attrs) 277 | .build()], 278 | ); 279 | 280 | channel.extensions.insert("ext".to_owned(), extension_tag); 281 | channel 282 | .namespaces 283 | .insert("ext".to_owned(), "http://example.com/ext".to_owned()); 284 | 285 | let xml = channel.to_string(); 286 | 287 | assert!(xml.contains("this & that")); 288 | assert!(xml.contains("value 1&2")); 289 | assert!(xml.contains("value 2&3")); 290 | assert!(xml.contains("r=1&p=2&c=3")); 291 | assert!(xml.contains("http://example.com?test=1&another=true")); 292 | assert!(xml.contains("http://example.com?test=2&another=false")); 293 | assert!(xml.contains("<title>")); 294 | assert!(xml.contains("<![CDATA[let's try & break this <item> ]]]]><![CDATA[>, shall we?]]>")); 295 | 296 | let channel = rss::Channel::read_from(xml.as_bytes()).unwrap(); 297 | 298 | assert_eq!(channel.categories[0].name, "this & that"); 299 | assert_eq!(channel.cloud.unwrap().path, "/rpc?r=1&p=2&c=3"); 300 | assert_eq!(channel.extensions["ext"]["tag"][0].name, "ext:tag"); 301 | assert_eq!(channel.extensions["ext"]["tag"][0].value, None); 302 | assert_eq!( 303 | channel.extensions["ext"]["tag"][0].attrs["ext:key1"], 304 | "value 1&2" 305 | ); 306 | assert_eq!( 307 | channel.extensions["ext"]["tag"][0].attrs["ext:key2"], 308 | "value 2&3" 309 | ); 310 | assert_eq!( 311 | channel.items[0].enclosure.as_ref().unwrap().url, 312 | "http://example.com?test=1&another=true" 313 | ); 314 | assert_eq!( 315 | channel.items[0].source.as_ref().unwrap().url, 316 | "http://example.com?test=2&another=false" 317 | ); 318 | assert_eq!( 319 | channel.items[0] 320 | .source 321 | .as_ref() 322 | .unwrap() 323 | .title 324 | .as_ref() 325 | .unwrap(), 326 | "<title>" 327 | ); 328 | assert_eq!( 329 | channel.items[0].description.as_ref().unwrap(), 330 | "let's try & break this <item> ]]>, shall we?" 331 | ); 332 | } 333 | 334 | #[test] 335 | fn test_write_link() { 336 | let channel = Channel { 337 | title: "Channel title".into(), 338 | link: "http://example.com/feed".into(), 339 | items: vec![Item { 340 | link: Some("http://example.com/post1".into()), 341 | ..Default::default() 342 | }], 343 | ..Default::default() 344 | }; 345 | 346 | let buf = channel.pretty_write_to(Vec::new(), b' ', 4).unwrap(); 347 | assert_eq!( 348 | std::str::from_utf8(&buf).unwrap(), 349 | r#"<?xml version="1.0" encoding="utf-8"?> 350 | <rss version="2.0"> 351 | <channel> 352 | <title>Channel title 353 | http://example.com/feed 354 | 355 | 356 | http://example.com/post1 357 | 358 | 359 | "# 360 | ); 361 | } 362 | 363 | #[cfg(feature = "atom")] 364 | #[test] 365 | fn test_atom_write_channel() { 366 | let channel = Channel { 367 | title: "Channel title".into(), 368 | atom_ext: Some(rss::extension::atom::AtomExtension { 369 | links: vec![rss::extension::atom::Link { 370 | rel: "self".into(), 371 | href: "http://example.com/feed".into(), 372 | ..Default::default() 373 | }], 374 | }), 375 | ..Default::default() 376 | }; 377 | 378 | let buf = channel.pretty_write_to(Vec::new(), b' ', 4).unwrap(); 379 | assert_eq!( 380 | std::str::from_utf8(&buf).unwrap(), 381 | r#" 382 | 383 | 384 | Channel title 385 | 386 | 387 | 388 | 389 | "# 390 | ); 391 | } 392 | 393 | #[cfg(feature = "atom")] 394 | #[test] 395 | fn test_atom_write_item() { 396 | let channel = Channel { 397 | title: "Channel title".into(), 398 | items: vec![Item { 399 | link: Some("http://example.com/post1".into()), 400 | atom_ext: Some(rss::extension::atom::AtomExtension { 401 | links: vec![rss::extension::atom::Link { 402 | rel: "related".into(), 403 | href: "http://example.com/post1".into(), 404 | ..Default::default() 405 | }], 406 | }), 407 | ..Default::default() 408 | }], 409 | ..Default::default() 410 | }; 411 | 412 | let buf = channel.pretty_write_to(Vec::new(), b' ', 4).unwrap(); 413 | assert_eq!( 414 | std::str::from_utf8(&buf).unwrap(), 415 | r#" 416 | 417 | 418 | Channel title 419 | 420 | 421 | 422 | http://example.com/post1 423 | 424 | 425 | 426 | "# 427 | ); 428 | } 429 | --------------------------------------------------------------------------------