├── rustfmt.toml ├── .gitignore ├── tests ├── data │ ├── text_empty.xml │ ├── text_plain.xml │ ├── text_cdata.xml │ ├── text_plain_escaped.xml │ ├── category.xml │ ├── content_text_plain.xml │ ├── text_cdata_escaped.xml │ ├── generator.xml │ ├── content_src.xml │ ├── content_text_html.xml │ ├── content_text_cdata.xml │ ├── content_text_other.xml │ ├── content_text_plain_escaped.xml │ ├── text_html.xml │ ├── link.xml │ ├── text_xhtml_escaped.xml │ ├── content_text_cdata_escaped.xml │ ├── text_xhtml.xml │ ├── content_text_xhtml.xml │ ├── person.xml │ ├── content_text_html_common.xml │ ├── extension.xml │ ├── source.xml │ ├── feed.xml │ ├── entry.xml │ └── entry_with_non_standard_dates.xml ├── write.rs ├── content.rs ├── text.rs ├── builders.rs └── read.rs ├── src ├── fromxml.rs ├── error.rs ├── lib.rs ├── toxml.rs ├── extension │ ├── util.rs │ └── mod.rs ├── category.rs ├── generator.rs ├── text.rs ├── person.rs ├── util.rs ├── link.rs ├── content.rs ├── source.rs ├── entry.rs └── feed.rs ├── .github └── workflows │ ├── coverage.yml │ ├── clippy.yml │ └── build.yml ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── CHANGELOG.md └── LICENSE-APACHE /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .vscode/ 5 | .idea/ 6 | -------------------------------------------------------------------------------- /tests/data/text_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | </feed> 4 | -------------------------------------------------------------------------------- /tests/data/text_plain.xml: -------------------------------------------------------------------------------- 1 | <feed xmlns="http://www.w3.org/2005/Atom"> 2 | <title type="text">Feed Title 3 | 4 | -------------------------------------------------------------------------------- /tests/data/text_cdata.xml: -------------------------------------------------------------------------------- 1 | 2 | <![CDATA[<p>Feed Title</p>]]> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/text_plain_escaped.xml: -------------------------------------------------------------------------------- 1 | 2 | <p>Feed Title</p> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/category.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/data/content_text_plain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Entry content 4 | 5 | -------------------------------------------------------------------------------- /tests/data/text_cdata_escaped.xml: -------------------------------------------------------------------------------- 1 | 2 | <![CDATA[<p>Feed Title</p>]]> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/generator.xml: -------------------------------------------------------------------------------- 1 | 2 | Example Generator 3 | -------------------------------------------------------------------------------- /tests/data/content_src.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/data/content_text_html.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | <p>Entry content</p> 4 | 5 | -------------------------------------------------------------------------------- /tests/data/content_text_cdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Entry content

]]>
4 |
5 |
6 | -------------------------------------------------------------------------------- /tests/data/content_text_other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | RW50cnkgY29udGVudA== 4 | 5 | -------------------------------------------------------------------------------- /tests/data/content_text_plain_escaped.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | <p>Entry content</p> 4 | 5 | -------------------------------------------------------------------------------- /tests/data/text_html.xml: -------------------------------------------------------------------------------- 1 | 2 | <p><b>F</b>eed Title<![CDATA[</p>]]><!-- <b>Important</b> comment --> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/link.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/data/text_xhtml_escaped.xml: -------------------------------------------------------------------------------- 1 | 2 | <div xmlns="http://www.w3.org/1999/xhtml"><p>Feed Title</p></div> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/content_text_cdata_escaped.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/data/text_xhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | <div xmlns="http://www.w3.org/1999/xhtml"><p>Feed Title</p><!-- <b>Important</b> comment --><![CDATA[<hr/>]]></div> 3 | 4 | -------------------------------------------------------------------------------- /tests/data/content_text_xhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Entry content with a link inside.

4 |
5 |
-------------------------------------------------------------------------------- /tests/data/person.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | John Doe 4 | johndoe@example.com 5 | http://example.com 6 | Example Name 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/fromxml.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | use quick_xml::events::attributes::Attributes; 4 | use quick_xml::Reader; 5 | 6 | use crate::error::Error; 7 | 8 | pub(crate) trait FromXml: Sized { 9 | fn from_xml(reader: &mut Reader, atts: Attributes<'_>) -> Result; 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/content_text_html_common.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <p>Entry content</p><a href="2021-05-51/article.html">Read more</a> 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/data/extension.xml: -------------------------------------------------------------------------------- 1 | 2 | <strong>Title</strong> 3 | 4 | Child 5 | 6 | 7 | <strong>T]]> 8 | 9 | Child 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.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:latest 14 | options: --security-opt seccomp=unconfined 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: cargo tarpaulin -f -t 5 --out Xml -v -- --test-threads=1 18 | - uses: codecov/codecov-action@v3 19 | -------------------------------------------------------------------------------- /.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@v1 11 | with: 12 | toolchain: stable 13 | - run: cargo clippy --all --all-features 14 | fmt: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: dtolnay/rust-toolchain@v1 19 | with: 20 | toolchain: stable 21 | - run: cargo fmt --all -- --check 22 | -------------------------------------------------------------------------------- /tests/data/source.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Feed Title 5 | Feed subtitle 6 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 7 | 2017-06-03T15:15:44-05:00 8 | http://example.com/icon.png 9 | http://example.com/logo.png 10 | © 2017 John Doe 11 | 12 | John Doe 13 | 14 | 15 | Jane Doe 16 | 17 | 18 | 19 | 20 | John Doe 21 | 22 | 23 | Jane Doe 24 | 25 | Feed Generator 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/data/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | Feed Title 3 | Feed subtitle 4 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 5 | 2017-06-03T15:15:44-05:00 6 | http://example.com/icon.png 7 | http://example.com/logo.png 8 | © 2017 John Doe 9 | 10 | John Doe 11 | 12 | 13 | Jane Doe 14 | 15 | 16 | 17 | 18 | John Doe 19 | 20 | 21 | Jane Doe 22 | 23 | Feed Generator 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/data/entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Entry Title 4 | http://example.com/article/1 5 | 2017-06-03T15:15:44-05:00 6 | 7 | John Doe 8 | 9 | 10 | Jane Doe 11 | 12 | 13 | 14 | 15 | John Doe 16 | 17 | 18 | Jane Doe 19 | 20 | 21 | 22 | 2017-06-01T15:15:44-05:00 23 | Entry summary 24 | © 2017 John Doe 25 | Entry content 26 | 27 | 28 | -------------------------------------------------------------------------------- /.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 | - 1.83.0 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: dtolnay/rust-toolchain@v1 24 | with: 25 | toolchain: ${{ matrix.rust }} 26 | - name: Build 27 | run: | 28 | cargo build --all-targets --no-default-features --verbose 29 | cargo build --all-targets --verbose 30 | - name: Run tests 31 | run: | 32 | cargo test --all-targets --no-default-features --verbose 33 | cargo test --all-targets --verbose 34 | env: 35 | RUST_BACKTRACE: 1 36 | -------------------------------------------------------------------------------- /tests/data/entry_with_non_standard_dates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Entry Title 4 | http://example.com/article/1 5 | Sat, 03 Jun 2017 15:15:44 -0500 6 | 7 | John Doe 8 | 9 | 10 | Jane Doe 11 | 12 | 13 | 14 | 15 | John Doe 16 | 17 | 18 | Jane Doe 19 | 20 | 21 | 22 | 23 | Thu, 01 Jun 2017 15:15:44 -0500 24 | 25 | Entry summary 26 | © 2017 John Doe 27 | Entry content 28 | 29 | 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atom_syndication" 3 | version = "0.12.7" 4 | authors = ["James Hurst "] 5 | edition = "2021" 6 | description = "Library for serializing the Atom web content syndication format" 7 | repository = "https://github.com/rust-syndication/atom" 8 | documentation = "https://docs.rs/atom_syndication/" 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | keywords = ["atom", "feed", "parser", "parsing"] 12 | categories = ["parser-implementations"] 13 | rust-version = "1.83" 14 | 15 | [dependencies] 16 | diligent-date-parser = "0.1.3" 17 | quick-xml = { version = "0.38", features = ["encoding"] } 18 | chrono = { version = "0.4", default-features = false, features = ["alloc"] } 19 | derive_builder = { version = "0.20", optional = true } 20 | never = { version = "0.1", optional = true } 21 | serde = { version = "1.0", optional = true, features = ["derive"] } 22 | 23 | [features] 24 | default = ["builders"] 25 | builders = ["derive_builder", "never"] 26 | with-serde = ["serde", "chrono/serde"] 27 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2017 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 | -------------------------------------------------------------------------------- /tests/write.rs: -------------------------------------------------------------------------------- 1 | extern crate atom_syndication as atom; 2 | 3 | use std::fs::File; 4 | use std::io::BufReader; 5 | 6 | use crate::atom::{Content, Entry, Feed}; 7 | 8 | macro_rules! feed { 9 | ($f:expr) => {{ 10 | let file = File::open($f).unwrap(); 11 | let reader = BufReader::new(file); 12 | Feed::read_from(reader).unwrap() 13 | }}; 14 | } 15 | 16 | #[test] 17 | fn write_feed() { 18 | let feed = feed!("tests/data/feed.xml"); 19 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 20 | } 21 | 22 | #[test] 23 | fn write_entry() { 24 | let feed = feed!("tests/data/entry.xml"); 25 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 26 | } 27 | 28 | #[test] 29 | fn write_category() { 30 | let feed = feed!("tests/data/category.xml"); 31 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 32 | } 33 | 34 | #[test] 35 | fn write_generator() { 36 | let feed = feed!("tests/data/generator.xml"); 37 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 38 | } 39 | 40 | #[test] 41 | fn write_link() { 42 | let feed = feed!("tests/data/link.xml"); 43 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 44 | } 45 | 46 | #[test] 47 | fn write_person() { 48 | let feed = feed!("tests/data/person.xml"); 49 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 50 | } 51 | 52 | #[test] 53 | fn write_source() { 54 | let feed = feed!("tests/data/source.xml"); 55 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 56 | } 57 | 58 | #[test] 59 | fn write_extension() { 60 | let feed = feed!("tests/data/extension.xml"); 61 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 62 | } 63 | 64 | #[test] 65 | fn write_content_roundtrip() { 66 | let mut content = Content::default(); 67 | content.set_base("http://example.com/blog/".to_string()); 68 | content.set_content_type("html".to_string()); 69 | content.set_value("Read more".to_string()); 70 | 71 | let mut entry = Entry::default(); 72 | entry.set_content(content); 73 | 74 | let mut feed = Feed::default(); 75 | feed.set_entries(vec![entry]); 76 | 77 | assert_eq!(feed.to_string().parse::().unwrap(), feed); 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom 2 | 3 | [![Build status](https://github.com/rust-syndication/atom/workflows/Build/badge.svg)](https://github.com/rust-syndication/atom/actions) 4 | [![Crates.io Status](https://img.shields.io/crates/v/atom_syndication.svg)](https://crates.io/crates/atom_syndication) 5 | [![Coverage](https://codecov.io/gh/rust-syndication/atom/branch/master/graph/badge.svg)](https://codecov.io/gh/rust-syndication/atom/) 6 | 7 | Library for serializing the Atom web content syndication format. 8 | 9 | [Documentation](https://docs.rs/atom_syndication/) 10 | 11 | This crate requires *Rustc version 1.83.0 or greater*. 12 | 13 | ## Usage 14 | 15 | Add the dependency to your `Cargo.toml`. 16 | 17 | ```toml 18 | [dependencies] 19 | atom_syndication = "0.12" 20 | ``` 21 | 22 | Or, if you want [Serde](https://github.com/serde-rs/serde) include the feature like this: 23 | 24 | ```toml 25 | [dependencies] 26 | atom_syndication = { version = "0.12", features = ["with-serde"] } 27 | ``` 28 | 29 | The package includes a single crate named `atom_syndication`. 30 | 31 | ```rust 32 | extern crate atom_syndication; 33 | ``` 34 | 35 | ## Reading 36 | 37 | A feed can be read from any object that implements the `BufRead` trait or using the `FromStr` trait. 38 | 39 | ```rust 40 | use std::fs::File; 41 | use std::io::BufReader; 42 | use atom_syndication::Feed; 43 | 44 | let file = File::open("example.xml").unwrap(); 45 | let feed = Feed::read_from(BufReader::new(file)).unwrap(); 46 | 47 | let string = ""; 48 | let feed = string.parse::().unwrap(); 49 | ``` 50 | 51 | ## Writing 52 | 53 | A feed can be written to any object that implements the `Write` trait or converted to an XML string using the `ToString` trait. 54 | 55 | ### Example 56 | 57 | ```rust 58 | use std::fs::File; 59 | use std::io::{BufReader, sink}; 60 | use atom_syndication::Feed; 61 | 62 | let file = File::open("example.xml").unwrap(); 63 | let feed = Feed::read_from(BufReader::new(file)).unwrap(); 64 | 65 | // write to the feed to a writer 66 | feed.write_to(sink()).unwrap(); 67 | 68 | // convert the feed to a string 69 | let string = feed.to_string(); 70 | ``` 71 | 72 | ## Invalid Feeds 73 | 74 | As a best effort to parse invalid feeds `atom_syndication` will default elements declared as "required" by the Atom specification to an empty string. 75 | 76 | ## License 77 | 78 | Licensed under either of 79 | 80 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 81 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 82 | 83 | at your option. 84 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | /// An error that occurred while performing an Atom operation. 6 | #[non_exhaustive] 7 | pub enum Error { 8 | /// Unable to parse XML. 9 | Xml(XmlError), 10 | /// Input did not begin with an opening feed tag. 11 | InvalidStartTag, 12 | /// Unexpected end of input. 13 | Eof, 14 | /// The format of the timestamp is wrong. 15 | WrongDatetime(String), 16 | /// The value of an attribute is wrong. 17 | WrongAttribute { 18 | /// The name of the attribute. 19 | attribute: &'static str, 20 | /// Invalid value. 21 | value: String, 22 | }, 23 | } 24 | 25 | impl StdError for Error { 26 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 27 | match *self { 28 | Error::Xml(ref err) => Some(err), 29 | Error::InvalidStartTag => None, 30 | Error::Eof => None, 31 | Error::WrongDatetime(_) => None, 32 | Error::WrongAttribute { .. } => None, 33 | } 34 | } 35 | } 36 | 37 | impl fmt::Display for Error { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match *self { 40 | Error::Xml(ref err) => fmt::Display::fmt(err, f), 41 | Error::InvalidStartTag => write!(f, "input did not begin with an opening feed tag"), 42 | Error::Eof => write!(f, "unexpected end of input"), 43 | Error::WrongDatetime(ref datetime) => write!( 44 | f, 45 | "timestamps must be formatted by RFC3339, rather than {}", 46 | datetime 47 | ), 48 | Error::WrongAttribute { 49 | attribute, 50 | ref value, 51 | } => write!( 52 | f, 53 | "Unsupported value of attribute {}: '{}'.", 54 | attribute, value 55 | ), 56 | } 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(err: XmlError) -> Error { 62 | Error::Xml(err) 63 | } 64 | } 65 | 66 | #[derive(Debug)] 67 | pub struct XmlError(Box); 68 | 69 | impl XmlError { 70 | pub(crate) fn new(err: impl StdError + Send + Sync + 'static) -> Self { 71 | Self(Box::new(err)) 72 | } 73 | } 74 | 75 | impl StdError for XmlError { 76 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 77 | self.0.source() 78 | } 79 | } 80 | 81 | impl fmt::Display for XmlError { 82 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 | fmt::Display::fmt(&self.0, f) 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod test { 89 | use super::*; 90 | 91 | #[test] 92 | fn error_send_and_sync() { 93 | fn assert_send_sync() {} 94 | assert_send_sync::(); 95 | assert_send_sync::(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs, rust_2018_idioms)] 2 | #![doc(html_root_url = "https://docs.rs/atom_syndication/")] 3 | 4 | //! Library for serializing the Atom web content syndication format. 5 | //! 6 | //! # Reading 7 | //! 8 | //! A feed can be read from any object that implements the `BufRead` trait or using 9 | //! the `FromStr` trait. 10 | //! 11 | //! ```no_run 12 | //! use std::fs::File; 13 | //! use std::io::BufReader; 14 | //! use atom_syndication::Feed; 15 | //! 16 | //! let file = File::open("example.xml").unwrap(); 17 | //! let feed = Feed::read_from(BufReader::new(file)).unwrap(); 18 | //! 19 | //! let string = ""; 20 | //! let feed = string.parse::().unwrap(); 21 | //! ``` 22 | //! 23 | //! # Writing 24 | //! 25 | //! A feed can be written to any object that implements the `Write` trait or converted to an XML 26 | //! string using the `ToString` trait. 27 | //! 28 | //! ## Example 29 | //! 30 | //! ```no_run 31 | //! use std::fs::File; 32 | //! use std::io::{BufReader, sink}; 33 | //! use atom_syndication::Feed; 34 | //! 35 | //! let file = File::open("example.xml").unwrap(); 36 | //! let feed = Feed::read_from(BufReader::new(file)).unwrap(); 37 | //! 38 | //! // write to the feed to a writer 39 | //! feed.write_to(sink()).unwrap(); 40 | //! 41 | //! // convert the feed to a string 42 | //! let string = feed.to_string(); 43 | //! ``` 44 | 45 | #[cfg(feature = "serde")] 46 | #[macro_use] 47 | extern crate serde; 48 | 49 | #[cfg(feature = "builders")] 50 | #[macro_use] 51 | extern crate derive_builder; 52 | 53 | mod category; 54 | mod content; 55 | mod entry; 56 | mod feed; 57 | mod generator; 58 | mod link; 59 | mod person; 60 | mod source; 61 | mod text; 62 | 63 | mod error; 64 | mod fromxml; 65 | mod toxml; 66 | mod util; 67 | 68 | /// Types and functions for namespaced extensions. 69 | pub mod extension; 70 | 71 | pub use crate::category::Category; 72 | #[cfg(feature = "builders")] 73 | pub use crate::category::CategoryBuilder; 74 | pub use crate::content::Content; 75 | #[cfg(feature = "builders")] 76 | pub use crate::content::ContentBuilder; 77 | pub use crate::entry::Entry; 78 | #[cfg(feature = "builders")] 79 | pub use crate::entry::EntryBuilder; 80 | pub use crate::error::Error; 81 | pub use crate::feed::Feed; 82 | #[cfg(feature = "builders")] 83 | pub use crate::feed::FeedBuilder; 84 | pub use crate::feed::WriteConfig; 85 | pub use crate::generator::Generator; 86 | #[cfg(feature = "builders")] 87 | pub use crate::generator::GeneratorBuilder; 88 | pub use crate::link::Link; 89 | #[cfg(feature = "builders")] 90 | pub use crate::link::LinkBuilder; 91 | pub use crate::person::Person; 92 | #[cfg(feature = "builders")] 93 | pub use crate::person::PersonBuilder; 94 | pub use crate::source::Source; 95 | #[cfg(feature = "builders")] 96 | pub use crate::source::SourceBuilder; 97 | #[cfg(feature = "builders")] 98 | pub use crate::text::TextBuilder; 99 | pub use crate::text::{Text, TextType}; 100 | pub use crate::util::FixedDateTime; 101 | -------------------------------------------------------------------------------- /src/toxml.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 4 | use quick_xml::Writer; 5 | 6 | use crate::error::XmlError; 7 | 8 | pub(crate) trait ToXml { 9 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError>; 10 | } 11 | 12 | impl<'a, T: ToXml> ToXml for &'a T { 13 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 14 | (*self).to_xml(writer) 15 | } 16 | } 17 | 18 | pub(crate) trait ToXmlNamed { 19 | fn to_xml_named(&self, writer: &mut Writer, name: &str) -> Result<(), XmlError> 20 | where 21 | W: Write; 22 | } 23 | 24 | impl<'a, T: ToXmlNamed> ToXmlNamed for &'a T { 25 | fn to_xml_named(&self, writer: &mut Writer, name: &str) -> Result<(), XmlError> 26 | where 27 | W: Write, 28 | { 29 | (*self).to_xml_named(writer, name) 30 | } 31 | } 32 | 33 | pub(crate) trait WriterExt { 34 | fn write_text_element(&mut self, name: &str, text: &str) -> Result<(), XmlError>; 35 | 36 | fn write_object(&mut self, object: T) -> Result<(), XmlError> 37 | where 38 | T: ToXml; 39 | 40 | fn write_object_named(&mut self, object: T, name: &str) -> Result<(), XmlError> 41 | where 42 | T: ToXmlNamed; 43 | 44 | fn write_objects(&mut self, objects: I) -> Result<(), XmlError> 45 | where 46 | T: ToXml, 47 | I: IntoIterator; 48 | 49 | fn write_objects_named(&mut self, objects: I, name: &str) -> Result<(), XmlError> 50 | where 51 | T: ToXmlNamed, 52 | I: IntoIterator; 53 | } 54 | 55 | impl WriterExt for Writer { 56 | fn write_text_element(&mut self, name: &str, text: &str) -> Result<(), XmlError> { 57 | self.write_event(Event::Start(BytesStart::new(name))) 58 | .map_err(XmlError::new)?; 59 | self.write_event(Event::Text(BytesText::new(text))) 60 | .map_err(XmlError::new)?; 61 | self.write_event(Event::End(BytesEnd::new(name))) 62 | .map_err(XmlError::new)?; 63 | Ok(()) 64 | } 65 | 66 | fn write_object(&mut self, object: T) -> Result<(), XmlError> 67 | where 68 | T: ToXml, 69 | { 70 | object.to_xml(self) 71 | } 72 | 73 | fn write_object_named(&mut self, object: T, name: &str) -> Result<(), XmlError> 74 | where 75 | T: ToXmlNamed, 76 | { 77 | object.to_xml_named(self, name) 78 | } 79 | 80 | fn write_objects(&mut self, objects: I) -> Result<(), XmlError> 81 | where 82 | T: ToXml, 83 | I: IntoIterator, 84 | { 85 | for object in objects { 86 | object.to_xml(self)?; 87 | } 88 | 89 | Ok(()) 90 | } 91 | 92 | fn write_objects_named(&mut self, objects: I, name: &str) -> Result<(), XmlError> 93 | where 94 | T: ToXmlNamed, 95 | I: IntoIterator, 96 | { 97 | for object in objects { 98 | object.to_xml_named(self, name)?; 99 | } 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/content.rs: -------------------------------------------------------------------------------- 1 | extern crate atom_syndication as atom; 2 | 3 | use std::fs::File; 4 | use std::io::BufReader; 5 | 6 | use crate::atom::Feed; 7 | 8 | macro_rules! feed { 9 | ($f:expr) => {{ 10 | let file = File::open($f).unwrap(); 11 | let reader = BufReader::new(file); 12 | Feed::read_from(reader).unwrap() 13 | }}; 14 | } 15 | 16 | #[test] 17 | fn content_src() { 18 | let feed = feed!("tests/data/content_src.xml"); 19 | let content = feed.entries().first().unwrap().content().unwrap(); 20 | assert_eq!(content.value(), None); 21 | assert_eq!(content.src(), Some("http://example.com/image.png")); 22 | assert_eq!(content.content_type(), Some("image/png")); 23 | } 24 | 25 | #[test] 26 | fn content_text_cdata_escaped() { 27 | let feed = feed!("tests/data/content_text_cdata_escaped.xml"); 28 | let content = feed.entries().first().unwrap().content().unwrap(); 29 | assert_eq!(content.value(), Some("<p>Entry content</p>")); 30 | assert_eq!(content.content_type(), Some("text")); 31 | } 32 | 33 | #[test] 34 | fn content_text_cdata() { 35 | let feed = feed!("tests/data/content_text_cdata.xml"); 36 | let content = feed.entries().first().unwrap().content().unwrap(); 37 | assert_eq!(content.value(), Some("

Entry content

")); 38 | assert_eq!(content.content_type(), Some("text")); 39 | } 40 | 41 | #[test] 42 | fn content_text_html() { 43 | let feed = feed!("tests/data/content_text_html.xml"); 44 | let content = feed.entries().first().unwrap().content().unwrap(); 45 | assert_eq!(content.value(), Some("

Entry content

")); 46 | assert_eq!(content.content_type(), Some("html")); 47 | } 48 | 49 | #[test] 50 | fn content_text_html_common_attributes() { 51 | let feed = feed!("tests/data/content_text_html_common.xml"); 52 | let content = feed.entries().first().unwrap().content().unwrap(); 53 | assert_eq!(content.base(), Some("http://example.com/blog/")); 54 | assert_eq!( 55 | content.value(), 56 | Some("

Entry content

Read more") 57 | ); 58 | assert_eq!(content.content_type(), Some("html")); 59 | } 60 | 61 | #[test] 62 | fn content_text_other() { 63 | let feed = feed!("tests/data/content_text_other.xml"); 64 | let content = feed.entries().first().unwrap().content().unwrap(); 65 | assert_eq!(content.value(), Some("RW50cnkgY29udGVudA==")); 66 | assert_eq!(content.content_type(), Some("application/octet-stream")); 67 | } 68 | 69 | #[test] 70 | fn content_text_plain_escaped() { 71 | let feed = feed!("tests/data/content_text_plain_escaped.xml"); 72 | let content = feed.entries().first().unwrap().content().unwrap(); 73 | assert_eq!(content.value(), Some("

Entry content

")); 74 | assert_eq!(content.content_type(), Some("text")); 75 | } 76 | 77 | #[test] 78 | fn content_text_plain() { 79 | let feed = feed!("tests/data/content_text_plain.xml"); 80 | let content = feed.entries().first().unwrap().content().unwrap(); 81 | assert_eq!(content.value(), Some("Entry content")); 82 | assert_eq!(content.content_type(), Some("text")); 83 | } 84 | 85 | #[test] 86 | fn content_text_xhtml() { 87 | let feed = feed!("tests/data/content_text_xhtml.xml"); 88 | let content = feed.entries().first().unwrap().content().unwrap(); 89 | assert_eq!( 90 | content.value(), 91 | Some( 92 | r#"

Entry content with a link inside.

"# 93 | ) 94 | ); 95 | assert_eq!(content.content_type(), Some("xhtml")); 96 | } 97 | -------------------------------------------------------------------------------- /src/extension/util.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::io::BufRead; 3 | 4 | use quick_xml::{ 5 | escape::resolve_predefined_entity, 6 | events::{attributes::Attributes, Event}, 7 | Reader, 8 | }; 9 | 10 | use crate::error::{Error, XmlError}; 11 | use crate::extension::{Extension, ExtensionMap}; 12 | use crate::util::{attr_value, decode}; 13 | 14 | pub fn extension_name(element_name: &str) -> Option<(&str, &str)> { 15 | let mut split = element_name.splitn(2, ':'); 16 | let ns = split.next().filter(|ns| !ns.is_empty())?; 17 | let name = split.next()?; 18 | Some((ns, name)) 19 | } 20 | 21 | pub fn parse_extension( 22 | reader: &mut Reader, 23 | atts: Attributes<'_>, 24 | ns: &str, 25 | name: &str, 26 | extensions: &mut ExtensionMap, 27 | ) -> Result<(), Error> 28 | where 29 | R: BufRead, 30 | { 31 | let ext = parse_extension_element(reader, atts)?; 32 | 33 | if !extensions.contains_key(ns) { 34 | extensions.insert(ns.to_string(), BTreeMap::new()); 35 | } 36 | 37 | let map = match extensions.get_mut(ns) { 38 | Some(map) => map, 39 | None => unreachable!(), 40 | }; 41 | 42 | if !map.contains_key(name) { 43 | map.insert(name.to_string(), Vec::new()); 44 | } 45 | 46 | let items = match map.get_mut(name) { 47 | Some(items) => items, 48 | None => unreachable!(), 49 | }; 50 | 51 | items.push(ext); 52 | 53 | Ok(()) 54 | } 55 | 56 | fn parse_extension_element( 57 | reader: &mut Reader, 58 | mut atts: Attributes<'_>, 59 | ) -> Result { 60 | let mut extension = Extension::default(); 61 | let mut buf = Vec::new(); 62 | 63 | for attr in atts.with_checks(false).flatten() { 64 | let key = decode(attr.key.local_name().as_ref(), reader)?.to_string(); 65 | let value = attr_value(&attr, reader)?.to_string(); 66 | extension.attrs.insert(key, value); 67 | } 68 | 69 | let mut text = String::new(); 70 | loop { 71 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 72 | Event::Start(element) => { 73 | let ext = parse_extension_element(reader, element.attributes())?; 74 | let element_local_name = element.local_name(); 75 | let name = decode(element_local_name.as_ref(), reader)?; 76 | 77 | if !extension.children.contains_key(&*name) { 78 | extension.children.insert(name.to_string(), Vec::new()); 79 | } 80 | 81 | let items = match extension.children.get_mut(&*name) { 82 | Some(items) => items, 83 | None => unreachable!(), 84 | }; 85 | 86 | items.push(ext); 87 | } 88 | Event::CData(element) => { 89 | text.push_str(decode(&element, reader)?.as_ref()); 90 | } 91 | Event::Text(element) => { 92 | text.push_str(element.decode().map_err(XmlError::new)?.as_ref()); 93 | } 94 | Event::GeneralRef(gref) => { 95 | let entity = gref.decode().map_err(XmlError::new)?; 96 | if let Some(resolved_entity) = resolve_predefined_entity(&entity) { 97 | text.push_str(resolved_entity); 98 | } else if let Some(ch) = gref.resolve_char_ref().map_err(XmlError::new)? { 99 | text.push(ch); 100 | } else { 101 | text.push('&'); 102 | text.push_str(&entity); 103 | text.push(';'); 104 | } 105 | } 106 | Event::End(element) => { 107 | extension.name = decode(element.name().as_ref(), reader)?.into(); 108 | break; 109 | } 110 | Event::Eof => return Err(Error::Eof), 111 | _ => {} 112 | } 113 | 114 | buf.clear(); 115 | } 116 | extension.value = Some(text.trim()) 117 | .filter(|t| !t.is_empty()) 118 | .map(ToString::to_string); 119 | 120 | Ok(extension) 121 | } 122 | -------------------------------------------------------------------------------- /tests/text.rs: -------------------------------------------------------------------------------- 1 | use atom_syndication::{Feed, Text, TextType}; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | 5 | macro_rules! feed { 6 | ($f:expr) => {{ 7 | let file = File::open($f).unwrap(); 8 | let reader = BufReader::new(file); 9 | Feed::read_from(reader).unwrap() 10 | }}; 11 | } 12 | 13 | #[test] 14 | fn text_cdata_escaped() { 15 | let feed = feed!("tests/data/text_cdata_escaped.xml"); 16 | let title = feed.title(); 17 | assert_eq!(title, "<p>Feed Title</p>"); 18 | assert_eq!(title.base, None); 19 | assert_eq!(title.lang, None); 20 | assert_eq!(title.r#type, TextType::Html); 21 | } 22 | 23 | #[test] 24 | fn text_cdata() { 25 | let reader = BufReader::new(File::open("tests/data/text_cdata.xml").unwrap()); 26 | let feed = Feed::read_from(reader).unwrap(); 27 | let title = feed.title(); 28 | assert_eq!(title, "

Feed Title

"); 29 | assert_eq!(title.base, None); 30 | assert_eq!(title.lang, None); 31 | assert_eq!(title.r#type, TextType::Html); 32 | } 33 | 34 | #[test] 35 | fn text_empty() { 36 | let feed = feed!("tests/data/text_empty.xml"); 37 | let title = feed.title(); 38 | assert_eq!(title, ""); 39 | assert_eq!(title.base, None); 40 | assert_eq!(title.lang, None); 41 | assert_eq!(title.r#type, TextType::Text); 42 | } 43 | 44 | #[test] 45 | fn text_html() { 46 | let feed = feed!("tests/data/text_html.xml"); 47 | let title = feed.title(); 48 | assert_eq!( 49 | title, 50 | "

Feed Title

" 51 | ); 52 | assert_eq!(title.base, None); 53 | assert_eq!(title.lang, None); 54 | assert_eq!(title.r#type, TextType::Html); 55 | } 56 | 57 | #[test] 58 | fn text_write_html() { 59 | let mut feed = Feed::default(); 60 | feed.set_title(Text::html("

Feed Title

")); 61 | let xml = feed.to_string(); 62 | assert!(xml.contains(r#"<p>Feed Title</p>"#)); 63 | } 64 | 65 | #[test] 66 | fn text_write_html_with_lang() { 67 | let mut feed = Feed::default(); 68 | let mut title = Text::html("

Feed Title

"); 69 | title.lang = Some("en".to_string()); 70 | feed.set_title(title); 71 | let xml = feed.to_string(); 72 | assert!( 73 | xml.contains(r#"<p>Feed Title</p>"#) 74 | ); 75 | } 76 | 77 | #[test] 78 | fn text_write_html_with_base_and_lang() { 79 | let mut feed = Feed::default(); 80 | let mut title = Text::html("

Feed Title

"); 81 | title.base = Some("http://example.com/articles/".to_string()); 82 | title.lang = Some("en".to_string()); 83 | feed.set_title(title); 84 | let xml = feed.to_string(); 85 | assert!(xml.contains(r#"<p>Feed Title</p>"#)); 86 | } 87 | 88 | #[test] 89 | fn text_write_xhtml() { 90 | let mut feed = Feed::default(); 91 | feed.set_title(Text::xhtml( 92 | "

Feed Title

", 93 | )); 94 | let xml = feed.to_string(); 95 | assert!(xml.contains( 96 | r#"<p xmlns="http://www.w3.org/1999/xhtml">Feed Title</p>"# 97 | )); 98 | } 99 | 100 | #[test] 101 | fn text_plain() { 102 | let feed = feed!("tests/data/text_plain.xml"); 103 | let title = feed.title(); 104 | assert_eq!(title, "Feed Title"); 105 | assert_eq!(title.base, None); 106 | assert_eq!(title.lang, None); 107 | assert_eq!(title.r#type, TextType::Text); 108 | } 109 | 110 | #[test] 111 | fn text_plain_escaped() { 112 | let feed = feed!("tests/data/text_plain_escaped.xml"); 113 | let title = feed.title(); 114 | assert_eq!(title, "

Feed Title

"); 115 | assert_eq!(title.base, None); 116 | assert_eq!(title.lang, None); 117 | assert_eq!(title.r#type, TextType::Text); 118 | } 119 | 120 | #[test] 121 | fn text_xhtml_escaped() { 122 | let feed = feed!("tests/data/text_xhtml_escaped.xml"); 123 | let title = feed.title(); 124 | assert_eq!( 125 | title, 126 | "
<p>Feed Title</p>
" 127 | ); 128 | assert_eq!(title.base, None); 129 | assert_eq!(title.lang, None); 130 | assert_eq!(title.r#type, TextType::Xhtml); 131 | } 132 | 133 | #[test] 134 | fn text_xhtml() { 135 | let feed = feed!("tests/data/text_xhtml.xml"); 136 | let title = feed.title(); 137 | assert_eq!( 138 | title, 139 | "

Feed Title

<hr/>
" 140 | ); 141 | assert_eq!(title.base, None); 142 | assert_eq!(title.lang, None); 143 | assert_eq!(title.r#type, TextType::Xhtml); 144 | } 145 | -------------------------------------------------------------------------------- /src/category.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::{BytesStart, Event}; 5 | use quick_xml::Reader; 6 | use quick_xml::Writer; 7 | 8 | use crate::error::{Error, XmlError}; 9 | use crate::toxml::ToXml; 10 | use crate::util::{attr_value, decode}; 11 | 12 | /// Represents a category in an Atom feed 13 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 14 | #[derive(Debug, Default, Clone, PartialEq)] 15 | #[cfg_attr(feature = "builders", derive(Builder))] 16 | #[cfg_attr( 17 | feature = "builders", 18 | builder( 19 | setter(into), 20 | default, 21 | build_fn(name = "build_impl", private, error = "never::Never") 22 | ) 23 | )] 24 | pub struct Category { 25 | /// Identifies the category. 26 | pub term: String, 27 | /// Identifies the categorization scheme via a URI. 28 | pub scheme: Option, 29 | /// A human-readable label for display. 30 | pub label: Option, 31 | } 32 | 33 | impl Category { 34 | /// Return the term that identifies this category. 35 | /// 36 | /// # Examples 37 | /// 38 | /// ``` 39 | /// use atom_syndication::Category; 40 | /// 41 | /// let mut category = Category::default(); 42 | /// category.set_term("technology"); 43 | /// assert_eq!(category.term(), "technology"); 44 | /// ``` 45 | pub fn term(&self) -> &str { 46 | self.term.as_str() 47 | } 48 | 49 | /// Set the term that identifies this category. 50 | /// 51 | /// # Examples 52 | /// 53 | /// ``` 54 | /// use atom_syndication::Category; 55 | /// 56 | /// let mut category = Category::default(); 57 | /// category.set_term("technology"); 58 | /// ``` 59 | pub fn set_term(&mut self, term: V) 60 | where 61 | V: Into, 62 | { 63 | self.term = term.into(); 64 | } 65 | 66 | /// Return the categorization scheme URI. 67 | /// 68 | /// # Examples 69 | /// 70 | /// ``` 71 | /// use atom_syndication::Category; 72 | /// 73 | /// let mut category = Category::default(); 74 | /// category.set_scheme("http://example.com/scheme".to_string()); 75 | /// assert_eq!(category.scheme(), Some("http://example.com/scheme")); 76 | /// ``` 77 | pub fn scheme(&self) -> Option<&str> { 78 | self.scheme.as_deref() 79 | } 80 | 81 | /// Set the categorization scheme URI. 82 | /// 83 | /// # Examples 84 | /// 85 | /// ``` 86 | /// use atom_syndication::Category; 87 | /// 88 | /// let mut category = Category::default(); 89 | /// category.set_scheme("http://example.com/scheme".to_string()); 90 | /// ``` 91 | pub fn set_scheme(&mut self, scheme: V) 92 | where 93 | V: Into>, 94 | { 95 | self.scheme = scheme.into(); 96 | } 97 | 98 | /// Return the label for this category. 99 | /// 100 | /// # Examples 101 | /// 102 | /// ``` 103 | /// use atom_syndication::Category; 104 | /// 105 | /// let mut category = Category::default(); 106 | /// category.set_scheme("Technology".to_string()); 107 | /// assert_eq!(category.scheme(), Some("Technology")); 108 | /// ``` 109 | 110 | pub fn label(&self) -> Option<&str> { 111 | self.label.as_deref() 112 | } 113 | 114 | /// Set the label for this category. 115 | /// 116 | /// # Examples 117 | /// 118 | /// ``` 119 | /// use atom_syndication::Category; 120 | /// 121 | /// let mut category = Category::default(); 122 | /// category.set_scheme("Technology".to_string()); 123 | /// ``` 124 | pub fn set_label(&mut self, label: V) 125 | where 126 | V: Into>, 127 | { 128 | self.label = label.into(); 129 | } 130 | } 131 | 132 | impl Category { 133 | pub(crate) fn from_xml<'s, B: BufRead>( 134 | reader: &mut Reader, 135 | element: &'s BytesStart<'s>, 136 | ) -> Result { 137 | let mut category = Category::default(); 138 | 139 | for att in element.attributes().with_checks(false).flatten() { 140 | match decode(att.key.as_ref(), reader)? { 141 | Cow::Borrowed("term") => { 142 | category.term = attr_value(&att, reader)?.to_string(); 143 | } 144 | Cow::Borrowed("scheme") => { 145 | category.scheme = Some(attr_value(&att, reader)?.to_string()); 146 | } 147 | Cow::Borrowed("label") => { 148 | category.label = Some(attr_value(&att, reader)?.to_string()); 149 | } 150 | _ => {} 151 | } 152 | } 153 | Ok(category) 154 | } 155 | } 156 | 157 | impl ToXml for Category { 158 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 159 | let mut element = BytesStart::new("category"); 160 | element.push_attribute(("term", &*self.term)); 161 | 162 | if let Some(ref scheme) = self.scheme { 163 | element.push_attribute(("scheme", &**scheme)); 164 | } 165 | 166 | if let Some(ref label) = self.label { 167 | element.push_attribute(("label", &**label)); 168 | } 169 | 170 | writer 171 | .write_event(Event::Empty(element)) 172 | .map_err(XmlError::new)?; 173 | 174 | Ok(()) 175 | } 176 | } 177 | 178 | #[cfg(feature = "builders")] 179 | impl CategoryBuilder { 180 | /// Builds a new `Category`. 181 | pub fn build(&self) -> Category { 182 | self.build_impl().unwrap() 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/generator.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::attributes::Attributes; 5 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 6 | use quick_xml::Reader; 7 | use quick_xml::Writer; 8 | 9 | use crate::error::{Error, XmlError}; 10 | use crate::fromxml::FromXml; 11 | use crate::toxml::ToXml; 12 | use crate::util::{atom_text, attr_value, decode}; 13 | 14 | /// Represents the generator of an Atom feed 15 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 16 | #[derive(Debug, Default, Clone, PartialEq)] 17 | #[cfg_attr(feature = "builders", derive(Builder))] 18 | #[cfg_attr( 19 | feature = "builders", 20 | builder( 21 | setter(into), 22 | default, 23 | build_fn(name = "build_impl", private, error = "never::Never") 24 | ) 25 | )] 26 | pub struct Generator { 27 | /// The name of the generator. 28 | pub value: String, 29 | /// The generator URI. 30 | pub uri: Option, 31 | /// The generator version. 32 | pub version: Option, 33 | } 34 | 35 | impl Generator { 36 | /// Return the name of the generator. 37 | /// 38 | /// # Examples 39 | /// 40 | /// ``` 41 | /// use atom_syndication::Generator; 42 | /// 43 | /// let mut generator = Generator::default(); 44 | /// generator.set_value("Feed Generator"); 45 | /// assert_eq!(generator.value(), "Feed Generator"); 46 | /// ``` 47 | pub fn value(&self) -> &str { 48 | self.value.as_str() 49 | } 50 | 51 | /// Set the name of the generator. 52 | /// 53 | /// # Examples 54 | /// 55 | /// ``` 56 | /// use atom_syndication::Generator; 57 | /// 58 | /// let mut generator = Generator::default(); 59 | /// generator.set_value("Feed Generator"); 60 | /// assert_eq!(generator.value(), "Feed Generator"); 61 | /// ``` 62 | pub fn set_value(&mut self, value: V) 63 | where 64 | V: Into, 65 | { 66 | self.value = value.into() 67 | } 68 | 69 | /// Return the URI for the generator. 70 | /// 71 | /// # Examples 72 | /// 73 | /// ``` 74 | /// use atom_syndication::Generator; 75 | /// 76 | /// let mut generator = Generator::default(); 77 | /// generator.set_uri("http://example.com/generator".to_string()); 78 | /// assert_eq!(generator.uri(), Some("http://example.com/generator")); 79 | /// ``` 80 | pub fn uri(&self) -> Option<&str> { 81 | self.uri.as_deref() 82 | } 83 | 84 | /// Set the URI for the generator. 85 | /// 86 | /// # Examples 87 | /// 88 | /// ``` 89 | /// use atom_syndication::Generator; 90 | /// 91 | /// let mut generator = Generator::default(); 92 | /// generator.set_uri("http://example.com/generator".to_string()); 93 | /// ``` 94 | pub fn set_uri(&mut self, uri: V) 95 | where 96 | V: Into>, 97 | { 98 | self.uri = uri.into() 99 | } 100 | 101 | /// Return the version of the generator. 102 | /// 103 | /// # Examples 104 | /// 105 | /// ``` 106 | /// use atom_syndication::Generator; 107 | /// 108 | /// let mut generator = Generator::default(); 109 | /// generator.set_version("1.0".to_string()); 110 | /// assert_eq!(generator.version(), Some("1.0")); 111 | /// ``` 112 | pub fn version(&self) -> Option<&str> { 113 | self.version.as_deref() 114 | } 115 | 116 | /// Set the version of the generator. 117 | /// 118 | /// # Examples 119 | /// 120 | /// ``` 121 | /// use atom_syndication::Generator; 122 | /// 123 | /// let mut generator = Generator::default(); 124 | /// generator.set_version("1.0".to_string()); 125 | /// ``` 126 | pub fn set_version(&mut self, version: V) 127 | where 128 | V: Into>, 129 | { 130 | self.version = version.into() 131 | } 132 | } 133 | 134 | impl FromXml for Generator { 135 | fn from_xml( 136 | reader: &mut Reader, 137 | mut atts: Attributes<'_>, 138 | ) -> Result { 139 | let mut generator = Generator::default(); 140 | 141 | for att in atts.with_checks(false).flatten() { 142 | match decode(att.key.as_ref(), reader)? { 143 | Cow::Borrowed("uri") => { 144 | generator.uri = Some(attr_value(&att, reader)?.to_string()); 145 | } 146 | Cow::Borrowed("version") => { 147 | generator.version = Some(attr_value(&att, reader)?.to_string()); 148 | } 149 | _ => {} 150 | } 151 | } 152 | 153 | generator.value = atom_text(reader)?.unwrap_or_default(); 154 | 155 | Ok(generator) 156 | } 157 | } 158 | 159 | impl ToXml for Generator { 160 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 161 | let name = "generator"; 162 | let mut element = BytesStart::new(name); 163 | 164 | if let Some(ref uri) = self.uri { 165 | element.push_attribute(("uri", &**uri)); 166 | } 167 | 168 | if let Some(ref version) = self.version { 169 | element.push_attribute(("version", &**version)); 170 | } 171 | 172 | writer 173 | .write_event(Event::Start(element)) 174 | .map_err(XmlError::new)?; 175 | writer 176 | .write_event(Event::Text(BytesText::new(&self.value))) 177 | .map_err(XmlError::new)?; 178 | writer 179 | .write_event(Event::End(BytesEnd::new(name))) 180 | .map_err(XmlError::new)?; 181 | 182 | Ok(()) 183 | } 184 | } 185 | 186 | #[cfg(feature = "builders")] 187 | impl GeneratorBuilder { 188 | /// Builds a new `Generator`. 189 | pub fn build(&self) -> Generator { 190 | self.build_impl().unwrap() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/extension/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::io::Write; 3 | use std::str; 4 | 5 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 6 | use quick_xml::Writer; 7 | 8 | use crate::error::XmlError; 9 | use crate::toxml::ToXml; 10 | 11 | pub(crate) mod util; 12 | 13 | /// A map of extension namespace prefixes to local names to elements. 14 | pub type ExtensionMap = BTreeMap>>; 15 | 16 | /// A namespaced extension. 17 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 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 Extension { 29 | /// The qualified name of the extension element. 30 | pub name: String, 31 | /// The content of the extension element. 32 | pub value: Option, 33 | /// The attributes for the extension element. 34 | #[cfg_attr(feature = "builders", builder(setter(each = "attr")))] 35 | pub attrs: BTreeMap, 36 | /// The children of the extension element. A map of local names to child elements. 37 | #[cfg_attr(feature = "builders", builder(setter(each = "child")))] 38 | pub children: BTreeMap>, 39 | } 40 | 41 | impl Extension { 42 | /// Return the qualified name of this extension. 43 | /// 44 | /// # Examples 45 | /// 46 | /// ``` 47 | /// use atom_syndication::extension::Extension; 48 | /// 49 | /// let mut extension = Extension::default(); 50 | /// extension.set_name("ext:name"); 51 | /// assert_eq!(extension.name(), "ext:name"); 52 | /// ``` 53 | pub fn name(&self) -> &str { 54 | self.name.as_str() 55 | } 56 | 57 | /// Set the qualified name of this extension. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ``` 62 | /// use atom_syndication::extension::Extension; 63 | /// 64 | /// let mut extension = Extension::default(); 65 | /// extension.set_name("ext:name"); 66 | /// ``` 67 | pub fn set_name(&mut self, name: V) 68 | where 69 | V: Into, 70 | { 71 | self.name = name.into(); 72 | } 73 | 74 | /// Return the text content of this extension. 75 | /// 76 | /// # Examples 77 | /// 78 | /// ``` 79 | /// use atom_syndication::extension::Extension; 80 | /// 81 | /// let mut extension = Extension::default(); 82 | /// extension.set_value("John Doe".to_string()); 83 | /// assert_eq!(extension.value(), Some("John Doe")); 84 | /// ``` 85 | pub fn value(&self) -> Option<&str> { 86 | self.value.as_deref() 87 | } 88 | 89 | /// Set the text content of this extension. 90 | /// 91 | /// # Examples 92 | /// 93 | /// ``` 94 | /// use atom_syndication::extension::Extension; 95 | /// 96 | /// let mut extension = Extension::default(); 97 | /// extension.set_value("John Doe".to_string()); 98 | /// ``` 99 | pub fn set_value(&mut self, value: V) 100 | where 101 | V: Into>, 102 | { 103 | self.value = value.into(); 104 | } 105 | 106 | /// Return the attributes for the extension element. 107 | /// 108 | /// # Examples 109 | /// 110 | /// ``` 111 | /// use std::collections::BTreeMap; 112 | /// use atom_syndication::extension::Extension; 113 | /// 114 | /// let mut extension = Extension::default(); 115 | /// let mut attrs = BTreeMap::::new(); 116 | /// attrs.insert("email".to_string(), "johndoe@example.com".to_string()); 117 | /// extension.set_attrs(attrs.clone()); 118 | /// assert_eq!(*extension.attrs(), attrs); 119 | /// ``` 120 | pub fn attrs(&self) -> &BTreeMap { 121 | &self.attrs 122 | } 123 | 124 | /// Set the attributes for the extension element. 125 | /// 126 | /// # Examples 127 | /// 128 | /// ``` 129 | /// use std::collections::BTreeMap; 130 | /// use atom_syndication::extension::Extension; 131 | /// 132 | /// let mut extension = Extension::default(); 133 | /// extension.set_attrs(BTreeMap::new()); 134 | /// ``` 135 | pub fn set_attrs(&mut self, attrs: V) 136 | where 137 | V: Into>, 138 | { 139 | self.attrs = attrs.into(); 140 | } 141 | 142 | /// Return the children of the extension element. 143 | /// 144 | /// A map of local names to child elements. 145 | /// 146 | /// # Examples 147 | /// 148 | /// ``` 149 | /// use std::collections::BTreeMap; 150 | /// use atom_syndication::extension::Extension; 151 | /// 152 | /// let mut extension = Extension::default(); 153 | /// let mut children = BTreeMap::>::new(); 154 | /// children.insert("ext:child".to_string(), Vec::new()); 155 | /// extension.set_children(children); 156 | /// assert!(extension.children().contains_key("ext:child")); 157 | /// ``` 158 | pub fn children(&self) -> &BTreeMap> { 159 | &self.children 160 | } 161 | 162 | /// Set the children of the extension element. 163 | /// 164 | /// A map of local names to child elements. 165 | /// 166 | /// # Examples 167 | /// 168 | /// ``` 169 | /// use std::collections::BTreeMap; 170 | /// use atom_syndication::extension::Extension; 171 | /// 172 | /// let mut extension = Extension::default(); 173 | /// extension.set_children(BTreeMap::new()); 174 | /// ``` 175 | pub fn set_children(&mut self, children: V) 176 | where 177 | V: Into>>, 178 | { 179 | self.children = children.into(); 180 | } 181 | } 182 | 183 | impl ToXml for Extension { 184 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 185 | let mut element = BytesStart::new(&self.name); 186 | element.extend_attributes(self.attrs.iter().map(|a| (a.0.as_bytes(), a.1.as_bytes()))); 187 | writer 188 | .write_event(Event::Start(element)) 189 | .map_err(XmlError::new)?; 190 | 191 | if let Some(value) = self.value.as_ref() { 192 | writer 193 | .write_event(Event::Text(BytesText::new(value))) 194 | .map_err(XmlError::new)?; 195 | } 196 | 197 | for extension in self.children.values().flatten() { 198 | extension.to_xml(writer)?; 199 | } 200 | 201 | writer 202 | .write_event(Event::End(BytesEnd::new(&self.name))) 203 | .map_err(XmlError::new)?; 204 | Ok(()) 205 | } 206 | } 207 | 208 | #[cfg(feature = "builders")] 209 | impl ExtensionBuilder { 210 | /// Builds a new `Extension`. 211 | pub fn build(&self) -> Extension { 212 | self.build_impl().unwrap() 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::cmp::PartialEq; 3 | use std::convert::{AsRef, From}; 4 | use std::io::{BufRead, Write}; 5 | use std::ops::Deref; 6 | use std::str::FromStr; 7 | 8 | use quick_xml::events::attributes::Attributes; 9 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 10 | use quick_xml::Reader; 11 | use quick_xml::Writer; 12 | 13 | use crate::error::{Error, XmlError}; 14 | use crate::fromxml::FromXml; 15 | use crate::toxml::ToXmlNamed; 16 | use crate::util::{atom_text, atom_xhtml, attr_value, decode}; 17 | 18 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 19 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 20 | /// Represents the value of the [`type` attribute of a text construct](https://tools.ietf.org/html/rfc4287#section-3.1.1) 21 | /// in an Atom feed, e.g. the type of the content stored in the element. 22 | pub enum TextType { 23 | /// Plain text 24 | Text, 25 | /// HTML 26 | Html, 27 | /// XHTML 28 | Xhtml, 29 | } 30 | 31 | impl Default for TextType { 32 | fn default() -> Self { 33 | TextType::Text 34 | } 35 | } 36 | 37 | impl TextType { 38 | fn as_str(&self) -> &'static str { 39 | match self { 40 | Self::Text => "text", 41 | Self::Html => "html", 42 | Self::Xhtml => "xhtml", 43 | } 44 | } 45 | } 46 | 47 | impl FromStr for TextType { 48 | type Err = Error; 49 | 50 | fn from_str(value: &str) -> Result { 51 | match value { 52 | "text" => Ok(Self::Text), 53 | "html" => Ok(Self::Html), 54 | "xhtml" => Ok(Self::Xhtml), 55 | _ => Err(Error::WrongAttribute { 56 | attribute: "type", 57 | value: value.to_owned(), 58 | }), 59 | } 60 | } 61 | } 62 | 63 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 64 | #[derive(Debug, Clone, PartialEq, Default)] 65 | #[cfg_attr(feature = "builders", derive(Builder))] 66 | #[cfg_attr( 67 | feature = "builders", 68 | builder( 69 | setter(into), 70 | default, 71 | build_fn(name = "build_impl", private, error = "never::Never") 72 | ) 73 | )] 74 | /// Represents a [text construct](https://tools.ietf.org/html/rfc4287#section-3.1) in an Atom feed. 75 | pub struct Text { 76 | /// Content of the text construct 77 | pub value: String, 78 | /// Base URL for resolving any relative references found in the element. 79 | pub base: Option, 80 | /// Indicates the natural language for the element. 81 | pub lang: Option, 82 | /// Type of content stored in the element. 83 | pub r#type: TextType, 84 | } 85 | 86 | impl Text { 87 | /// Creates a plain text construct (type = "text"). 88 | pub fn plain(value: impl Into) -> Self { 89 | Self { 90 | value: value.into(), 91 | r#type: TextType::Text, 92 | ..Self::default() 93 | } 94 | } 95 | 96 | /// Creates an html text construct (type = "html"). 97 | pub fn html(value: impl Into) -> Self { 98 | Self { 99 | value: value.into(), 100 | r#type: TextType::Html, 101 | ..Self::default() 102 | } 103 | } 104 | 105 | /// Creates an html text construct (type = "html"). 106 | pub fn xhtml(value: impl Into) -> Self { 107 | Self { 108 | value: value.into(), 109 | r#type: TextType::Xhtml, 110 | ..Self::default() 111 | } 112 | } 113 | 114 | /// Returns a content as a `str` 115 | pub fn as_str(&self) -> &str { 116 | &self.value 117 | } 118 | } 119 | 120 | impl From for Text { 121 | fn from(value: String) -> Self { 122 | Self::plain(value) 123 | } 124 | } 125 | 126 | impl<'t> From<&'t str> for Text { 127 | fn from(value: &'t str) -> Self { 128 | Self::plain(value) 129 | } 130 | } 131 | 132 | impl AsRef for Text { 133 | fn as_ref(&self) -> &str { 134 | &self.value 135 | } 136 | } 137 | 138 | impl Deref for Text { 139 | type Target = str; 140 | 141 | fn deref(&self) -> &Self::Target { 142 | &self.value 143 | } 144 | } 145 | 146 | impl PartialEq for Text { 147 | fn eq(&self, other: &str) -> bool { 148 | self.as_str() == other 149 | } 150 | } 151 | 152 | impl PartialEq for str { 153 | fn eq(&self, other: &Text) -> bool { 154 | self == other.as_str() 155 | } 156 | } 157 | 158 | impl FromXml for Text { 159 | fn from_xml( 160 | reader: &mut Reader, 161 | mut atts: Attributes<'_>, 162 | ) -> Result { 163 | let mut text = Text::default(); 164 | 165 | for att in atts.with_checks(false).flatten() { 166 | match decode(att.key.as_ref(), reader)? { 167 | Cow::Borrowed("xml:base") => { 168 | text.base = Some(attr_value(&att, reader)?.to_string()) 169 | } 170 | Cow::Borrowed("xml:lang") => { 171 | text.lang = Some(attr_value(&att, reader)?.to_string()) 172 | } 173 | Cow::Borrowed("type") => text.r#type = attr_value(&att, reader)?.parse()?, 174 | _ => {} 175 | } 176 | } 177 | 178 | let content = if text.r#type == TextType::Xhtml { 179 | atom_xhtml(reader)? 180 | } else { 181 | atom_text(reader)? 182 | }; 183 | 184 | text.value = content.unwrap_or_default(); 185 | 186 | Ok(text) 187 | } 188 | } 189 | 190 | impl ToXmlNamed for Text { 191 | fn to_xml_named(&self, writer: &mut Writer, name: &str) -> Result<(), XmlError> 192 | where 193 | W: Write, 194 | { 195 | let mut element = BytesStart::new(name); 196 | if let Some(ref base) = self.base { 197 | element.push_attribute(("xml:base", base.as_str())); 198 | } 199 | if let Some(ref lang) = self.lang { 200 | element.push_attribute(("xml:lang", lang.as_str())); 201 | } 202 | if self.r#type != TextType::default() { 203 | element.push_attribute(("type", self.r#type.as_str())); 204 | } 205 | writer 206 | .write_event(Event::Start(element)) 207 | .map_err(XmlError::new)?; 208 | if self.r#type == TextType::Xhtml { 209 | writer 210 | .write_event(Event::Text(BytesText::from_escaped(&self.value))) 211 | .map_err(XmlError::new)?; 212 | } else { 213 | writer 214 | .write_event(Event::Text(BytesText::new(&self.value))) 215 | .map_err(XmlError::new)?; 216 | } 217 | writer 218 | .write_event(Event::End(BytesEnd::new(name))) 219 | .map_err(XmlError::new)?; 220 | 221 | Ok(()) 222 | } 223 | } 224 | 225 | #[cfg(feature = "builders")] 226 | impl TextBuilder { 227 | /// Builds a new `Text`. 228 | pub fn build(&self) -> Text { 229 | self.build_impl().unwrap() 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /tests/builders.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "builders")] 2 | 3 | use atom_syndication::extension::*; 4 | use atom_syndication::*; 5 | use std::collections::BTreeMap; 6 | use std::str::FromStr; 7 | 8 | fn join_lines(text: &str) -> String { 9 | text.lines().map(|line| line.trim()).collect() 10 | } 11 | 12 | #[test] 13 | fn test_builders() { 14 | let does = vec![ 15 | PersonBuilder::default().name("John Doe").build(), 16 | PersonBuilder::default().name("Jane Doe").build(), 17 | ]; 18 | 19 | let feed = FeedBuilder::default() 20 | .namespace(("ext".to_string(), "http://example.com".to_string())) 21 | .title("Feed Title") 22 | .subtitle(Text::plain("Feed subtitle")) 23 | .id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6") 24 | .updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()) 25 | .icon("http://example.com/icon.png".to_string()) 26 | .logo("http://example.com/logo.png".to_string()) 27 | .rights(TextBuilder::default().value("© 2017 John Doe").build()) 28 | .authors(does.clone()) 29 | .contributors(does.clone()) 30 | .category(CategoryBuilder::default().term("technology").build()) 31 | .category(CategoryBuilder::default().term("podcast").build()) 32 | .generator(GeneratorBuilder::default().value("Feed Generator").build()) 33 | .link( 34 | LinkBuilder::default() 35 | .rel("self") 36 | .href("http://example.com/feed") 37 | .build(), 38 | ) 39 | .link( 40 | LinkBuilder::default() 41 | .rel("alternate") 42 | .href("http://example.com") 43 | .build(), 44 | ) 45 | .entry( 46 | EntryBuilder::default() 47 | .title("Entry Title") 48 | .id("http://example.com/article/1") 49 | .updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()) 50 | .authors(does.clone()) 51 | .category(CategoryBuilder::default().term("technology").build()) 52 | .category(CategoryBuilder::default().term("podcast").build()) 53 | .contributors(does.clone()) 54 | .links(vec![ 55 | LinkBuilder::default() 56 | .rel("alternate") 57 | .href("http://example.com/article/") 58 | .build(), 59 | LinkBuilder::default() 60 | .rel("enclosure") 61 | .href("http://example.com/audio.mp3") 62 | .mime_type("audio/mpeg".to_string()) 63 | .length("1000".to_string()) 64 | .build(), 65 | ]) 66 | .published(FixedDateTime::from_str("2017-06-01T15:15:44-05:00").unwrap()) 67 | .summary(Text::plain("Entry summary")) 68 | .rights(Text::plain("© 2017 John Doe")) 69 | .content( 70 | ContentBuilder::default() 71 | .value("Entry content".to_string()) 72 | .build(), 73 | ) 74 | .source( 75 | SourceBuilder::default() 76 | .title("Entry Title") 77 | .id("http://source.example.com/content/article/1") 78 | .updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()) 79 | .build(), 80 | ) 81 | .extension(("ext".to_string(), { 82 | let mut map = BTreeMap::new(); 83 | map.insert( 84 | "title".to_string(), 85 | vec![ExtensionBuilder::default() 86 | .name("ext:title") 87 | .value("Title".to_string()) 88 | .attr(("type".to_string(), "text".to_string())) 89 | .build()], 90 | ); 91 | map 92 | })) 93 | .build(), 94 | ) 95 | .build(); 96 | 97 | assert_eq!( 98 | join_lines(&feed.to_string()), 99 | join_lines( 100 | r#" 101 | 102 | 103 | Feed Title 104 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 105 | 2017-06-03T15:15:44-05:00 106 | 107 | John Doe 108 | 109 | 110 | Jane Doe 111 | 112 | 113 | 114 | 115 | John Doe 116 | 117 | 118 | Jane Doe 119 | 120 | Feed Generator 121 | http://example.com/icon.png 122 | 123 | 124 | http://example.com/logo.png 125 | © 2017 John Doe 126 | Feed subtitle 127 | 128 | Entry Title 129 | http://example.com/article/1 130 | 2017-06-03T15:15:44-05:00 131 | 132 | John Doe 133 | 134 | 135 | Jane Doe 136 | 137 | 138 | 139 | 140 | John Doe 141 | 142 | 143 | Jane Doe 144 | 145 | 146 | 147 | 2017-06-01T15:15:44-05:00 148 | © 2017 John Doe 149 | 150 | Entry Title 151 | http://source.example.com/content/article/1 152 | 2017-06-03T15:15:44-05:00 153 | 154 | Entry summary 155 | Entry content 156 | Title 157 | 158 | 159 | "# 160 | ) 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Enables the parsing of elements defined in another namespace in Person constructs [`#91`](https://github.com/rust-syndication/atom/pull/91) 6 | - Update `quick-xml` to `0.38` [`#92`](https://github.com/rust-syndication/atom/pull/92) 7 | - Bump MSRV (Minimum Supported Rust Version) from 1.57.0 to 1.83.0 8 | 9 | ## 0.12.7 - 2025-02-16 10 | 11 | - Publish tests [`#90`](https://github.com/rust-syndication/atom/pull/90) 12 | 13 | ## 0.12.6 - 2024-12-20 14 | 15 | - Fix a regression: make `Error` and `XmlError` be `Send` and `Sync` again [`#89`](https://github.com/rust-syndication/atom/pull/89) 16 | 17 | ## 0.12.5 - 2024-11-16 18 | 19 | - Remove ambiguous statements about escaping from documentation. [`#85`](https://github.com/rust-syndication/atom/pull/85) 20 | - Update `quick-xml` to `0.37`. [`#86`](https://github.com/rust-syndication/atom/pull/86) 21 | - Clarify usage of `src` attribute of a `Content` structure. [`#87`](https://github.com/rust-syndication/atom/pull/87) 22 | 23 | ## 0.12.4 - 2024-08-28 24 | 25 | - Update `quick-xml` to `0.32`. [`#82`](https://github.com/rust-syndication/atom/pull/82) 26 | - Update `quick-xml` to `0.36`. [`#83`](https://github.com/rust-syndication/atom/pull/83) 27 | 28 | ## 0.12.3 - 2024-05-11 29 | 30 | - Bump MSRV (Minimum Supported Rust Version) from 1.56.0 to 1.57.0 [`#80`](https://github.com/rust-syndication/atom/pull/80) 31 | - Update `quick-xml` and `derive_builder` dependencies. [`#81`](https://github.com/rust-syndication/atom/pull/81) 32 | 33 | ## 0.12.2 - 2023-07-26 34 | 35 | - Upgrade `quick_xml` to `0.30` [`#78`](https://github.com/rust-syndication/atom/pull/78) 36 | 37 | ## 0.12.1 - 2023-03-27 38 | 39 | - Upgrade `quick_xml` to `0.28` [`#77`](https://github.com/rust-syndication/atom/pull/77) 40 | 41 | ## 0.12.0 - 2022-12-29 42 | 43 | - Wrap `quick_xml::XmlError` into a newtype [`#65`](https://github.com/rust-syndication/atom/pull/65) 44 | - Implement `std::error::Error` for `XmlError`. Mark helper traits as `pub(crate)` to prevent their accidental leakage to public API [`#66`](https://github.com/rust-syndication/atom/pull/66) 45 | - Bump MSRV (Minimum Supported Rust Version) from 1.40.0 to 1.56.0 [`#66`](https://github.com/rust-syndication/atom/pull/66) and [`#69`](https://github.com/rust-syndication/atom/pull/69) and [`#74`](https://github.com/rust-syndication/atom/pull/74) 46 | - Upgrade `quick_xml` to `0.27` and `derive_builder` to `0.12` [`#67`](https://github.com/rust-syndication/atom/pull/67) 47 | - Allow to configure emitted XML [`#70`](https://github.com/rust-syndication/atom/pull/70) 48 | - Switch to Rust 2021 Edition [`#74`](https://github.com/rust-syndication/atom/pull/74) 49 | - Remove unused error case `Error::Utf8` [`#73`](https://github.com/rust-syndication/atom/pull/73) 50 | 51 | ## 0.11.0 - 2021-10-20 52 | 53 | - Disable clock feature of chrono to mitigate RUSTSEC-2020-0159 [`#57`](https://github.com/rust-syndication/atom/pull/57) 54 | - Escape Content's value unless it contains xhtml [`#52`](https://github.com/rust-syndication/atom/pull/52) 55 | - Preserve entities and open tags (e.g `&`, `
`) in xhtml content [`#53`](https://github.com/rust-syndication/atom/pull/53) 56 | - Add support of xml:base and xml:land in a Feed [`#55`](https://github.com/rust-syndication/atom/pull/55) 57 | 58 | ## 0.10.0 - 2021-06-06 59 | 60 | - Infallible builders [`13af228`](https://github.com/rust-syndication/atom/commit/13af228967934f6869886a42bd6427cd6d24da64) 61 | - Introduce Text type to represent Atom Text constructs [`45cbd6b`](https://github.com/rust-syndication/atom/commit/45cbd6b61af57a4bcfc98600b5510139c75baf10) 62 | - Rename text::ContentType to TextType [`fa32372`](https://github.com/rust-syndication/atom/commit/fa323721845b496b9264ab92937fa3f29ca11c1d) 63 | 64 | ## 0.9.1 - 2021-01-07 65 | 66 | - update quick-xml version to 0.20 [`48caa33`](https://github.com/rust-syndication/atom/commit/48caa33be11f33ad543de29b2522c90766a5eaf9) 67 | 68 | ## 0.9.0 - 2020-05-13 69 | 70 | - Update quick-xml to 0.18 [`#28`](https://github.com/rust-syndication/atom/pull/28) 71 | - Diligently parse dates [`#23`](https://github.com/rust-syndication/atom/pull/23) 72 | - Expose all fields of the model types [`#25`](https://github.com/rust-syndication/atom/pull/25) 73 | 74 | ## 0.8.0 - 2020-01-23 75 | 76 | - Add a default builders feature that can be disabled [`#19`](https://github.com/rust-syndication/atom/pull/19) 77 | - Handwrite Error conformance so thiserror is not needed [`#18`](https://github.com/rust-syndication/atom/pull/18) 78 | - Use thiserror instead of failure [`#17`](https://github.com/rust-syndication/atom/pull/17) 79 | - prepare for 0.8.0 release [`ce4c783`](https://github.com/rust-syndication/atom/commit/ce4c783d01edf8266456a49bf2c2a75da10d5b24) 80 | 81 | ## 0.7.0 - 2019-12-15 82 | 83 | - update dependencies [`#16`](https://github.com/rust-syndication/atom/pull/16) 84 | - add feature: with-serde [`#13`](https://github.com/rust-syndication/atom/pull/13) 85 | - replace String with ::chrono::DateTime<::chrono::FixedOffset> [`#12`](https://github.com/rust-syndication/atom/pull/12) 86 | - update edition: from 2015 to 2018 [`b74bb6a`](https://github.com/rust-syndication/atom/commit/b74bb6ac8243187008cb084c661592e7aa887426) 87 | 88 | ## 0.6.0 - 2018-05-29 89 | 90 | - prevent mixed content warning on crates.io [`#8`](https://github.com/rust-syndication/atom/pull/8) 91 | - update quick-xml and use failure crate for Error [`7d75966`](https://github.com/rust-syndication/atom/commit/7d759667cfae0f90b7449e09dbe08678aed47c0b) 92 | 93 | ## 0.5.8 - 2018-02-12 94 | 95 | - Optionally implement Serialize/Deserialize for data structures (fixes #5) [`#5`](https://github.com/rust-syndication/atom/issues/5) 96 | 97 | ## 0.5.7 - 2018-01-27 98 | 99 | - fix text extraction [`78d62f0`](https://github.com/rust-syndication/atom/commit/78d62f0527ee9a339b9016362fdd2e02688ef74d) 100 | 101 | ## 0.5.6 - 2017-11-27 102 | 103 | - Update quick-xml to 0.10 [`9cf8d23`](https://github.com/rust-syndication/atom/commit/9cf8d23c55efd08a6bb5369ea64370f02b60cfeb) 104 | 105 | ## 0.5.5 - 2017-11-07 106 | 107 | - content type needs to be xhtml for xhtml content [`#2`](https://github.com/rust-syndication/atom/pull/2) 108 | 109 | ## 0.5.4 - 2017-07-16 110 | 111 | - Derive builders with Into<T> and default values [`e72b20a`](https://github.com/rust-syndication/atom/commit/e72b20aa259292b8c9e390252266a1b7057d42e0) 112 | 113 | ## 0.5.3 - 2017-07-02 114 | 115 | - Write Atom namespace [`#1`](https://github.com/rust-syndication/atom/issues/1) 116 | - Formatted with rustfmt-nightly 0.1.7 [`5a5812c`](https://github.com/rust-syndication/atom/commit/5a5812c9a504fef681a3bfe11c069e43655767c9) 117 | 118 | ## 0.5.2 - 2017-06-23 119 | 120 | - Added examples for Extension methods [`b5acfe0`](https://github.com/rust-syndication/atom/commit/b5acfe0a21f556d0205279bb7160151b2a7e5823) 121 | - Added builders using derive_builders, added missing extension setter methods [`33ddd21`](https://github.com/rust-syndication/atom/commit/33ddd21e55739b7c9a7c01203c028bc64d197878) 122 | 123 | ## 0.5.1 - 2017-06-23 124 | 125 | - Fixed Cargo.toml category slug [ci skip] [`3711804`](https://github.com/rust-syndication/atom/commit/3711804ade9769b29ca698296337a334003bbb8f) 126 | 127 | ## 0.5.0 - 2017-06-23 128 | 129 | - Added reading, tests [`9cb8e2b`](https://github.com/rust-syndication/atom/commit/9cb8e2be94a67aee2e9a69624ba8e7473ab83ad9) 130 | - Added writing [`338c840`](https://github.com/rust-syndication/atom/commit/338c840ee780c8c9726a63171684a094e1d7ccf0) 131 | - Added support for extensions [`825d782`](https://github.com/rust-syndication/atom/commit/825d7821a47dcddada177e1d37fe20a35786bd63) 132 | -------------------------------------------------------------------------------- /src/person.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::attributes::Attributes; 5 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 6 | use quick_xml::Reader; 7 | use quick_xml::Writer; 8 | 9 | use crate::error::{Error, XmlError}; 10 | use crate::extension::util::{extension_name, parse_extension}; 11 | use crate::extension::ExtensionMap; 12 | use crate::fromxml::FromXml; 13 | use crate::toxml::{ToXmlNamed, WriterExt}; 14 | use crate::util::{atom_text, decode, skip}; 15 | 16 | /// Represents a person in an Atom feed 17 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 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 Person { 29 | /// A human-readable name for the person. 30 | pub name: String, 31 | /// An email address for the person. 32 | pub email: Option, 33 | /// A Web page for the person. 34 | pub uri: Option, 35 | /// A list of extensions for the person. 36 | pub extensions: ExtensionMap, 37 | } 38 | 39 | impl Person { 40 | /// Return the name of this person. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ``` 45 | /// use atom_syndication::Person; 46 | /// 47 | /// let mut person = Person::default(); 48 | /// person.set_name("John Doe"); 49 | /// assert_eq!(person.name(), "John Doe"); 50 | /// ``` 51 | pub fn name(&self) -> &str { 52 | self.name.as_str() 53 | } 54 | 55 | /// Return the name of this person. 56 | /// 57 | /// # Examples 58 | /// 59 | /// ``` 60 | /// use atom_syndication::Person; 61 | /// 62 | /// let mut person = Person::default(); 63 | /// person.set_name("John Doe"); 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 email address for this person. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ``` 77 | /// use atom_syndication::Person; 78 | /// 79 | /// let mut person = Person::default(); 80 | /// person.set_email("johndoe@example.com".to_string()); 81 | /// assert_eq!(person.email(), Some("johndoe@example.com")); 82 | /// ``` 83 | pub fn email(&self) -> Option<&str> { 84 | self.email.as_deref() 85 | } 86 | 87 | /// Set the email address for this person. 88 | /// 89 | /// # Examples 90 | /// 91 | /// ``` 92 | /// use atom_syndication::Person; 93 | /// 94 | /// let mut person = Person::default(); 95 | /// person.set_email("johndoe@example.com".to_string()); 96 | /// ``` 97 | pub fn set_email(&mut self, email: V) 98 | where 99 | V: Into>, 100 | { 101 | self.email = email.into() 102 | } 103 | 104 | /// Return the Web page for this person. 105 | /// 106 | /// # Examples 107 | /// 108 | /// ``` 109 | /// use atom_syndication::Person; 110 | /// 111 | /// let mut person = Person::default(); 112 | /// person.set_uri("http://example.com".to_string()); 113 | /// assert_eq!(person.uri(), Some("http://example.com")); 114 | /// ``` 115 | pub fn uri(&self) -> Option<&str> { 116 | self.uri.as_deref() 117 | } 118 | 119 | /// Set the Web page for this person. 120 | /// 121 | /// # Examples 122 | /// 123 | /// ``` 124 | /// use atom_syndication::Person; 125 | /// 126 | /// let mut person = Person::default(); 127 | /// person.set_uri("http://example.com".to_string()); 128 | /// ``` 129 | pub fn set_uri(&mut self, uri: V) 130 | where 131 | V: Into>, 132 | { 133 | self.uri = uri.into() 134 | } 135 | 136 | /// Return the extensions for this person. 137 | /// 138 | /// # Examples 139 | /// 140 | /// ``` 141 | /// use atom_syndication::{Person, extension::ExtensionMap}; 142 | /// 143 | /// let mut person = Person::default(); 144 | /// person.set_extensions(ExtensionMap::new()); 145 | /// assert_eq!(person.extensions().len(), 0); 146 | /// ``` 147 | pub fn extensions(&self) -> &ExtensionMap { 148 | &self.extensions 149 | } 150 | 151 | /// Set the extensions for this person. 152 | /// 153 | /// # Examples 154 | /// 155 | /// ``` 156 | /// use atom_syndication::{Person, extension::ExtensionMap}; 157 | /// 158 | /// let mut person = Person::default(); 159 | /// person.set_extensions(ExtensionMap::new()); 160 | /// ``` 161 | pub fn set_extensions(&mut self, extensions: ExtensionMap) { 162 | self.extensions = extensions; 163 | } 164 | } 165 | 166 | impl FromXml for Person { 167 | fn from_xml(reader: &mut Reader, _: Attributes<'_>) -> Result { 168 | let mut person = Person::default(); 169 | let mut buf = Vec::new(); 170 | 171 | loop { 172 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 173 | Event::Start(element) => match decode(element.name().as_ref(), reader)? { 174 | Cow::Borrowed("name") => person.name = atom_text(reader)?.unwrap_or_default(), 175 | Cow::Borrowed("email") => person.email = atom_text(reader)?, 176 | Cow::Borrowed("uri") => person.uri = atom_text(reader)?, 177 | n => { 178 | if let Some((ns, name)) = extension_name(n.as_ref()) { 179 | parse_extension( 180 | reader, 181 | element.attributes(), 182 | ns, 183 | name, 184 | &mut person.extensions, 185 | )?; 186 | } else { 187 | skip(element.name(), reader)?; 188 | } 189 | } 190 | }, 191 | Event::End(_) => break, 192 | Event::Eof => return Err(Error::Eof), 193 | _ => {} 194 | } 195 | 196 | buf.clear(); 197 | } 198 | 199 | Ok(person) 200 | } 201 | } 202 | 203 | impl ToXmlNamed for Person { 204 | fn to_xml_named(&self, writer: &mut Writer, name: &str) -> Result<(), XmlError> 205 | where 206 | W: Write, 207 | { 208 | writer 209 | .write_event(Event::Start(BytesStart::new(name))) 210 | .map_err(XmlError::new)?; 211 | writer.write_text_element("name", &self.name)?; 212 | 213 | if let Some(ref email) = self.email { 214 | writer.write_text_element("email", email)?; 215 | } 216 | 217 | if let Some(ref uri) = self.uri { 218 | writer.write_text_element("uri", uri)?; 219 | } 220 | 221 | for map in self.extensions.values() { 222 | for extensions in map.values() { 223 | writer.write_objects(extensions)?; 224 | } 225 | } 226 | 227 | writer 228 | .write_event(Event::End(BytesEnd::new(name))) 229 | .map_err(XmlError::new)?; 230 | 231 | Ok(()) 232 | } 233 | } 234 | 235 | #[cfg(feature = "builders")] 236 | impl PersonBuilder { 237 | /// Builds a new `Person`. 238 | pub fn build(&self) -> Person { 239 | self.build_impl().unwrap() 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::{ 2 | escape::{escape, resolve_predefined_entity}, 3 | events::{attributes::Attribute, Event}, 4 | name::QName, 5 | Reader, 6 | }; 7 | 8 | use crate::error::{Error, XmlError}; 9 | use std::borrow::Cow; 10 | use std::io::BufRead; 11 | use std::str::FromStr; 12 | 13 | /// Alias of `::chrono::DateTime<::chrono::FixedOffset>` 14 | pub type FixedDateTime = ::chrono::DateTime<::chrono::FixedOffset>; 15 | 16 | pub fn default_fixed_datetime() -> FixedDateTime { 17 | FixedDateTime::from_str("1970-01-01T00:00:00Z").unwrap() 18 | } 19 | 20 | pub(crate) fn decode<'s, 'r, B: BufRead>( 21 | bytes: &'s [u8], 22 | reader: &'r Reader, 23 | ) -> Result, Error> { 24 | let text = reader.decoder().decode(bytes).map_err(XmlError::new)?; 25 | Ok(text) 26 | } 27 | 28 | pub(crate) fn attr_value<'s, 'r, B: BufRead>( 29 | attr: &'s Attribute<'s>, 30 | reader: &'r Reader, 31 | ) -> Result, Error> { 32 | let value = attr 33 | .decode_and_unescape_value(reader.decoder()) 34 | .map_err(XmlError::new)?; 35 | Ok(value) 36 | } 37 | 38 | pub(crate) fn skip(end: QName<'_>, reader: &mut Reader) -> Result<(), Error> { 39 | reader 40 | .read_to_end_into(end, &mut Vec::new()) 41 | .map_err(XmlError::new)?; 42 | Ok(()) 43 | } 44 | 45 | fn non_empty(string: String) -> Option { 46 | if !string.is_empty() { 47 | Some(string) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | pub fn atom_text(reader: &mut Reader) -> Result, Error> { 54 | reader.config_mut().expand_empty_elements = false; 55 | 56 | let mut innerbuf = Vec::new(); 57 | let mut depth = 0; 58 | let mut result = String::new(); 59 | 60 | loop { 61 | match reader 62 | .read_event_into(&mut innerbuf) 63 | .map_err(XmlError::new)? 64 | { 65 | Event::Start(start) => { 66 | depth += 1; 67 | result.push('<'); 68 | result.push_str(decode(&start, reader)?.as_ref()); 69 | result.push('>'); 70 | } 71 | Event::End(end) => { 72 | if depth <= 0 { 73 | break; 74 | } 75 | depth -= 1; 76 | result.push_str("'); 79 | } 80 | Event::Empty(start) => { 81 | result.push('<'); 82 | result.push_str(decode(&start, reader)?.as_ref()); 83 | result.push_str("/>"); 84 | } 85 | Event::CData(text) => { 86 | result.push_str(decode(&text, reader)?.as_ref()); 87 | } 88 | Event::Text(text) => { 89 | let decoded = text.decode().map_err(XmlError::new)?; 90 | result.push_str(&decoded); 91 | } 92 | Event::GeneralRef(gref) => { 93 | let entity = gref.decode().map_err(XmlError::new)?; 94 | if let Some(resolved_entity) = resolve_predefined_entity(&entity) { 95 | result.push_str(resolved_entity); 96 | } else if let Some(ch) = gref.resolve_char_ref().map_err(XmlError::new)? { 97 | result.push(ch); 98 | } else { 99 | result.push('&'); 100 | result.push_str(&entity); 101 | result.push(';'); 102 | } 103 | } 104 | Event::Comment(text) => { 105 | let decoded = text.decode().map_err(XmlError::new)?; 106 | result.push_str(""); 109 | } 110 | Event::Decl(_decl) => {} 111 | Event::PI(_text) => {} 112 | Event::DocType(_text) => {} 113 | Event::Eof => return Err(Error::Eof), 114 | } 115 | 116 | innerbuf.clear(); 117 | } 118 | 119 | reader.config_mut().expand_empty_elements = true; 120 | 121 | Ok(non_empty(result)) 122 | } 123 | 124 | pub fn atom_xhtml(reader: &mut Reader) -> Result, Error> { 125 | reader.config_mut().expand_empty_elements = false; 126 | 127 | let mut innerbuf = Vec::new(); 128 | let mut depth = 0; 129 | let mut result = String::new(); 130 | 131 | loop { 132 | match reader 133 | .read_event_into(&mut innerbuf) 134 | .map_err(XmlError::new)? 135 | { 136 | Event::Start(start) => { 137 | depth += 1; 138 | result.push('<'); 139 | result.push_str(decode(&start, reader)?.as_ref()); 140 | result.push('>'); 141 | } 142 | Event::End(end) => { 143 | if depth <= 0 { 144 | break; 145 | } 146 | depth -= 1; 147 | result.push_str("'); 150 | } 151 | Event::Empty(start) => { 152 | result.push('<'); 153 | result.push_str(decode(&start, reader)?.as_ref()); 154 | result.push_str("/>"); 155 | } 156 | Event::CData(text) => { 157 | result.push_str(escape(decode(&text, reader)?.as_ref()).as_ref()); 158 | } 159 | Event::Text(text) => { 160 | let decoded = text.decode().map_err(XmlError::new)?; 161 | result.push_str(escape(decoded.as_ref()).as_ref()); 162 | } 163 | Event::GeneralRef(gref) => { 164 | let entity = gref.decode().map_err(XmlError::new)?; 165 | result.push('&'); 166 | result.push_str(&entity); 167 | result.push(';'); 168 | } 169 | Event::Comment(text) => { 170 | let decoded = text.decode().map_err(XmlError::new)?; 171 | result.push_str(""); 174 | } 175 | Event::Decl(_decl) => {} 176 | Event::PI(_text) => {} 177 | Event::DocType(_text) => {} 178 | Event::Eof => return Err(Error::Eof), 179 | } 180 | 181 | innerbuf.clear(); 182 | } 183 | 184 | reader.config_mut().expand_empty_elements = true; 185 | 186 | Ok(non_empty(result)) 187 | } 188 | 189 | pub fn atom_datetime(reader: &mut Reader) -> Result, Error> { 190 | if let Some(datetime_text) = atom_text(reader)? { 191 | match diligent_date_parser::parse_date(&datetime_text) { 192 | None => Err(Error::WrongDatetime(datetime_text)), 193 | Some(datetime) => Ok(Some(datetime)), 194 | } 195 | } else { 196 | Ok(None) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod test { 202 | use super::*; 203 | use crate::error::Error; 204 | 205 | fn read_x(xml: &str) -> Result, Error> { 206 | let mut reader = Reader::from_reader(xml.as_bytes()); 207 | reader.config_mut().expand_empty_elements = true; 208 | loop { 209 | let mut buf = Vec::new(); 210 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 211 | Event::Start(element) => { 212 | return match decode(element.name().as_ref(), &reader)? { 213 | Cow::Borrowed("text") => atom_text(&mut reader), 214 | Cow::Borrowed("raw") => atom_xhtml(&mut reader), 215 | _ => Err(Error::InvalidStartTag), 216 | } 217 | } 218 | Event::Eof => return Err(Error::Eof), 219 | _ => {} 220 | } 221 | } 222 | } 223 | 224 | #[test] 225 | fn test_read_text() { 226 | let xml_fragment = r#" 227 | Text with ampersand & <tag> and . 228 | "#; 229 | assert_eq!( 230 | read_x(xml_fragment).unwrap().unwrap().trim(), 231 | "Text with ampersand & and ." 232 | ); 233 | } 234 | 235 | #[test] 236 | fn test_read_xhtml() { 237 | let xml_fragment = r#" 238 |
a line
& one more
239 |
"#; 240 | assert_eq!( 241 | read_x(xml_fragment).unwrap().unwrap().trim(), 242 | r#"
a line
& one more
"# 243 | ); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/link.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::{BytesStart, Event}; 5 | use quick_xml::Reader; 6 | use quick_xml::Writer; 7 | 8 | use crate::error::{Error, XmlError}; 9 | use crate::toxml::ToXml; 10 | use crate::util::{attr_value, decode}; 11 | 12 | /// Represents a link in an Atom feed 13 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 14 | #[derive(Debug, Clone, PartialEq)] 15 | #[cfg_attr(feature = "builders", derive(Builder))] 16 | #[cfg_attr( 17 | feature = "builders", 18 | builder( 19 | setter(into), 20 | default, 21 | build_fn(name = "build_impl", private, error = "never::Never") 22 | ) 23 | )] 24 | pub struct Link { 25 | /// The URI of the referenced resource. 26 | pub href: String, 27 | /// The link relationship type. 28 | pub rel: String, 29 | /// The language of the resource. 30 | pub hreflang: Option, 31 | /// The MIME type of the resource. 32 | pub mime_type: Option, 33 | /// Human-readable information about the link. 34 | pub title: Option, 35 | /// The length of the resource, in bytes. 36 | pub length: Option, 37 | } 38 | 39 | impl Default for Link { 40 | fn default() -> Self { 41 | Link { 42 | href: Default::default(), 43 | rel: "alternate".into(), 44 | hreflang: Default::default(), 45 | mime_type: Default::default(), 46 | title: Default::default(), 47 | length: Default::default(), 48 | } 49 | } 50 | } 51 | 52 | impl Link { 53 | /// Return the URI the referenced resource. 54 | /// 55 | /// # Examples 56 | /// 57 | /// ``` 58 | /// use atom_syndication::Link; 59 | /// 60 | /// let mut link = Link::default(); 61 | /// link.set_href("http://example.com"); 62 | /// assert_eq!(link.href(), "http://example.com"); 63 | /// ``` 64 | pub fn href(&self) -> &str { 65 | self.href.as_str() 66 | } 67 | 68 | /// Set the URI of the referenced resource. 69 | /// 70 | /// # Examples 71 | /// 72 | /// ``` 73 | /// use atom_syndication::Link; 74 | /// 75 | /// let mut link = Link::default(); 76 | /// link.set_href("http://example.com"); 77 | /// ``` 78 | pub fn set_href(&mut self, href: V) 79 | where 80 | V: Into, 81 | { 82 | self.href = href.into() 83 | } 84 | 85 | /// Return the relation type of this link. 86 | /// 87 | /// # Examples 88 | /// 89 | /// ``` 90 | /// use atom_syndication::Link; 91 | /// 92 | /// let mut link = Link::default(); 93 | /// link.set_rel("alternate"); 94 | /// assert_eq!(link.rel(), "alternate"); 95 | /// ``` 96 | pub fn rel(&self) -> &str { 97 | self.rel.as_str() 98 | } 99 | 100 | /// Set the relation type of this link. 101 | /// 102 | /// # Examples 103 | /// 104 | /// ``` 105 | /// use atom_syndication::Link; 106 | /// 107 | /// let mut link = Link::default(); 108 | /// link.set_rel("alternate"); 109 | /// ``` 110 | pub fn set_rel(&mut self, rel: V) 111 | where 112 | V: Into, 113 | { 114 | self.rel = rel.into() 115 | } 116 | 117 | /// Return the language of the referenced resource. 118 | /// 119 | /// # Examples 120 | /// 121 | /// ``` 122 | /// use atom_syndication::Link; 123 | /// 124 | /// let mut link = Link::default(); 125 | /// link.set_hreflang("en".to_string()); 126 | /// assert_eq!(link.hreflang(), Some("en")); 127 | /// ``` 128 | pub fn hreflang(&self) -> Option<&str> { 129 | self.hreflang.as_deref() 130 | } 131 | 132 | /// Set the language of the referenced resource. 133 | /// 134 | /// # Examples 135 | /// 136 | /// ``` 137 | /// use atom_syndication::Link; 138 | /// 139 | /// let mut link = Link::default(); 140 | /// link.set_hreflang("en".to_string()); 141 | /// ``` 142 | pub fn set_hreflang(&mut self, hreflang: V) 143 | where 144 | V: Into>, 145 | { 146 | self.hreflang = hreflang.into() 147 | } 148 | 149 | /// Return the MIME type of the referenced resource. 150 | /// 151 | /// # Examples 152 | /// 153 | /// ``` 154 | /// use atom_syndication::Link; 155 | /// 156 | /// let mut link = Link::default(); 157 | /// link.set_mime_type("text/html".to_string()); 158 | /// assert_eq!(link.mime_type(), Some("text/html")); 159 | /// ``` 160 | pub fn mime_type(&self) -> Option<&str> { 161 | self.mime_type.as_deref() 162 | } 163 | 164 | /// Set the MIME type of the referenced resource. 165 | /// 166 | /// # Examples 167 | /// 168 | /// ``` 169 | /// use atom_syndication::Link; 170 | /// 171 | /// let mut link = Link::default(); 172 | /// link.set_mime_type("text/html".to_string()); 173 | /// ``` 174 | pub fn set_mime_type(&mut self, mime_type: V) 175 | where 176 | V: Into>, 177 | { 178 | self.mime_type = mime_type.into() 179 | } 180 | 181 | /// Return the title of the referenced resource. 182 | /// 183 | /// # Examples 184 | /// 185 | /// ``` 186 | /// use atom_syndication::Link; 187 | /// 188 | /// let mut link = Link::default(); 189 | /// link.set_title("Article Title".to_string()); 190 | /// assert_eq!(link.title(), Some("Article Title")); 191 | /// ``` 192 | pub fn title(&self) -> Option<&str> { 193 | self.title.as_deref() 194 | } 195 | 196 | /// Set the title of the referenced resource. 197 | /// 198 | /// # Examples 199 | /// 200 | /// ``` 201 | /// use atom_syndication::Link; 202 | /// 203 | /// let mut link = Link::default(); 204 | /// link.set_title("Article Title".to_string()); 205 | /// ``` 206 | pub fn set_title(&mut self, title: V) 207 | where 208 | V: Into>, 209 | { 210 | self.title = title.into() 211 | } 212 | 213 | /// Return the content length of the referenced resource in bytes. 214 | /// 215 | /// # Examples 216 | /// 217 | /// ``` 218 | /// use atom_syndication::Link; 219 | /// 220 | /// let mut link = Link::default(); 221 | /// link.set_length("1000".to_string()); 222 | /// assert_eq!(link.length(), Some("1000")); 223 | /// ``` 224 | pub fn length(&self) -> Option<&str> { 225 | self.length.as_deref() 226 | } 227 | 228 | /// Set the content length of the referenced resource in bytes. 229 | /// 230 | /// # Examples 231 | /// 232 | /// ``` 233 | /// use atom_syndication::Link; 234 | /// 235 | /// let mut link = Link::default(); 236 | /// link.set_length("1000".to_string()); 237 | /// ``` 238 | pub fn set_length(&mut self, length: V) 239 | where 240 | V: Into>, 241 | { 242 | self.length = length.into() 243 | } 244 | } 245 | 246 | impl Link { 247 | pub(crate) fn from_xml<'s, B: BufRead>( 248 | reader: &mut Reader, 249 | element: &'s BytesStart<'s>, 250 | ) -> Result { 251 | let mut link = Link::default(); 252 | 253 | for att in element.attributes().with_checks(false).flatten() { 254 | match decode(att.key.as_ref(), reader)? { 255 | Cow::Borrowed("href") => link.href = attr_value(&att, reader)?.to_string(), 256 | Cow::Borrowed("rel") => link.rel = attr_value(&att, reader)?.to_string(), 257 | Cow::Borrowed("hreflang") => { 258 | link.hreflang = Some(attr_value(&att, reader)?.to_string()) 259 | } 260 | Cow::Borrowed("type") => { 261 | link.mime_type = Some(attr_value(&att, reader)?.to_string()) 262 | } 263 | Cow::Borrowed("title") => link.title = Some(attr_value(&att, reader)?.to_string()), 264 | Cow::Borrowed("length") => { 265 | link.length = Some(attr_value(&att, reader)?.to_string()) 266 | } 267 | _ => {} 268 | } 269 | } 270 | 271 | Ok(link) 272 | } 273 | } 274 | 275 | impl ToXml for Link { 276 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 277 | let mut element = BytesStart::new("link"); 278 | element.push_attribute(("href", &*self.href)); 279 | element.push_attribute(("rel", &*self.rel)); 280 | 281 | if let Some(ref hreflang) = self.hreflang { 282 | element.push_attribute(("hreflang", &**hreflang)); 283 | } 284 | 285 | if let Some(ref mime_type) = self.mime_type { 286 | element.push_attribute(("type", &**mime_type)); 287 | } 288 | 289 | if let Some(ref title) = self.title { 290 | element.push_attribute(("title", &**title)); 291 | } 292 | 293 | if let Some(ref length) = self.length { 294 | element.push_attribute(("length", &**length)); 295 | } 296 | 297 | writer 298 | .write_event(Event::Empty(element)) 299 | .map_err(XmlError::new)?; 300 | 301 | Ok(()) 302 | } 303 | } 304 | 305 | #[cfg(feature = "builders")] 306 | impl LinkBuilder { 307 | /// Builds a new `Link`. 308 | pub fn build(&self) -> Link { 309 | self.build_impl().unwrap() 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /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 2017 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 | -------------------------------------------------------------------------------- /tests/read.rs: -------------------------------------------------------------------------------- 1 | extern crate atom_syndication as atom; 2 | 3 | use std::fs::File; 4 | use std::io::BufReader; 5 | 6 | use atom::Error; 7 | 8 | use crate::atom::extension::ExtensionMap; 9 | use crate::atom::{Feed, Text}; 10 | 11 | macro_rules! feed { 12 | ($f:expr) => {{ 13 | let file = File::open($f).unwrap(); 14 | let reader = BufReader::new(file); 15 | Feed::read_from(reader).unwrap() 16 | }}; 17 | } 18 | 19 | #[test] 20 | fn read_feed() { 21 | let feed = feed!("tests/data/feed.xml"); 22 | assert_eq!(feed.title(), "Feed Title"); 23 | assert_eq!(feed.id(), "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 24 | assert_eq!(feed.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 25 | assert_eq!(feed.icon(), Some("http://example.com/icon.png")); 26 | assert_eq!(feed.logo(), Some("http://example.com/logo.png")); 27 | assert_eq!(feed.rights().map(Text::as_str), Some("© 2017 John Doe")); 28 | assert_eq!(feed.subtitle().map(Text::as_str), Some("Feed subtitle")); 29 | assert_eq!(feed.authors().len(), 2); 30 | assert_eq!(feed.categories().len(), 2); 31 | assert_eq!(feed.contributors().len(), 2); 32 | assert!(feed.generator().is_some()); 33 | assert_eq!(feed.links().len(), 2); 34 | } 35 | 36 | #[test] 37 | fn read_entry() { 38 | let feed = feed!("tests/data/entry.xml"); 39 | assert_eq!(feed.entries().len(), 1); 40 | 41 | let entry = feed.entries().first().unwrap(); 42 | assert_eq!(entry.title(), "Entry Title"); 43 | assert_eq!(entry.id(), "http://example.com/article/1"); 44 | assert_eq!(entry.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 45 | assert_eq!(entry.authors().len(), 2); 46 | assert_eq!(entry.categories().len(), 2); 47 | assert_eq!(entry.contributors().len(), 2); 48 | assert_eq!(entry.links().len(), 2); 49 | assert_eq!( 50 | entry.published().map(chrono::DateTime::to_rfc3339), 51 | Some("2017-06-01T15:15:44-05:00".to_string()) 52 | ); 53 | assert_eq!(entry.summary().map(Text::as_str), Some("Entry summary")); 54 | assert_eq!(entry.rights().map(Text::as_str), Some("© 2017 John Doe")); 55 | 56 | let content = entry.content().unwrap(); 57 | assert_eq!(content.value(), Some("Entry content")); 58 | } 59 | 60 | #[test] 61 | fn read_entry_with_non_standard_dates() { 62 | let feed = feed!("tests/data/entry_with_non_standard_dates.xml"); 63 | assert_eq!(feed.entries().len(), 1); 64 | 65 | let entry = feed.entries().first().unwrap(); 66 | assert_eq!(entry.title(), "Entry Title"); 67 | assert_eq!(entry.id(), "http://example.com/article/1"); 68 | assert_eq!(entry.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 69 | assert_eq!(entry.authors().len(), 2); 70 | assert_eq!(entry.categories().len(), 2); 71 | assert_eq!(entry.contributors().len(), 2); 72 | assert_eq!(entry.links().len(), 2); 73 | assert_eq!( 74 | entry.published().map(chrono::DateTime::to_rfc3339), 75 | Some("2017-06-01T15:15:44-05:00".to_string()) 76 | ); 77 | assert_eq!(entry.summary().map(Text::as_str), Some("Entry summary")); 78 | assert_eq!(entry.rights().map(Text::as_str), Some("© 2017 John Doe")); 79 | 80 | let content = entry.content().unwrap(); 81 | assert_eq!(content.value(), Some("Entry content")); 82 | } 83 | 84 | #[test] 85 | fn read_category() { 86 | let feed = feed!("tests/data/category.xml"); 87 | let category = feed.categories().first().unwrap(); 88 | assert_eq!(category.term(), "technology"); 89 | assert_eq!(category.scheme(), Some("http://example.com/scheme")); 90 | assert_eq!(category.label(), Some("Technology")); 91 | } 92 | 93 | #[test] 94 | fn read_generator() { 95 | let feed = feed!("tests/data/generator.xml"); 96 | let generator = feed.generator().unwrap(); 97 | assert_eq!(generator.value(), "Example Generator"); 98 | assert_eq!(generator.uri(), Some("http://example.com/generator")); 99 | assert_eq!(generator.version(), Some("1.0")); 100 | } 101 | 102 | #[test] 103 | fn read_link() { 104 | let feed = feed!("tests/data/link.xml"); 105 | let link = feed.links().first().unwrap(); 106 | assert_eq!(link.rel(), "enclosure"); 107 | assert_eq!(link.href(), "http://example.com/audio.mp3"); 108 | assert_eq!(link.hreflang(), Some("en")); 109 | assert_eq!(link.mime_type(), Some("audio/mpeg")); 110 | assert_eq!(link.title(), Some("audio")); 111 | assert_eq!(link.length(), Some("1000")); 112 | } 113 | 114 | #[test] 115 | fn read_person() { 116 | let feed = feed!("tests/data/person.xml"); 117 | let person = feed.authors().first().unwrap(); 118 | assert_eq!(person.name(), "John Doe"); 119 | assert_eq!(person.email(), Some("johndoe@example.com")); 120 | assert_eq!(person.uri(), Some("http://example.com")); 121 | 122 | // Person extensions 123 | assert!(person.extensions().contains_key("ext")); 124 | let map = person.extensions().get("ext").unwrap(); 125 | assert!(map.contains_key("name")); 126 | let name = map.get("name").unwrap().first().unwrap(); 127 | assert_eq!(name.value(), Some("Example Name")); 128 | assert_eq!( 129 | name.attrs().get("exattr").map(String::as_str), 130 | Some("exvalue") 131 | ); 132 | } 133 | 134 | #[test] 135 | fn read_source() { 136 | let feed = feed!("tests/data/source.xml"); 137 | 138 | let entry = feed.entries().first().unwrap(); 139 | assert!(entry.source().is_some()); 140 | 141 | let source = entry.source().unwrap(); 142 | assert_eq!(source.title(), "Feed Title"); 143 | assert_eq!(source.id(), "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 144 | assert_eq!(source.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 145 | assert_eq!(source.icon(), Some("http://example.com/icon.png")); 146 | assert_eq!(source.logo(), Some("http://example.com/logo.png")); 147 | assert_eq!(source.rights().map(Text::as_str), Some("© 2017 John Doe")); 148 | assert_eq!(source.subtitle().map(Text::as_str), Some("Feed subtitle")); 149 | assert_eq!(source.authors().len(), 2); 150 | assert_eq!(source.categories().len(), 2); 151 | assert_eq!(source.contributors().len(), 2); 152 | assert!(source.generator().is_some()); 153 | } 154 | 155 | #[test] 156 | fn read_extension() { 157 | let feed = feed!("tests/data/extension.xml"); 158 | let entry = feed.entries().first().unwrap(); 159 | 160 | assert_eq!( 161 | feed.namespaces().get("ext").map(String::as_str), 162 | Some("http://example.com") 163 | ); 164 | 165 | let check_extensions = |extensions: &ExtensionMap| { 166 | assert!(extensions.contains_key("ext")); 167 | let map = extensions.get("ext").unwrap(); 168 | 169 | assert!(map.contains_key("title")); 170 | let title = map.get("title").unwrap().first().unwrap(); 171 | assert_eq!(title.value(), Some("Title")); 172 | assert_eq!(title.attrs().get("type").map(String::as_str), Some("text")); 173 | 174 | assert!(map.contains_key("parent")); 175 | let parent = map.get("parent").unwrap().first().unwrap(); 176 | 177 | assert!(parent.children().contains_key("child")); 178 | let child = parent.children().get("child").unwrap().first().unwrap(); 179 | assert_eq!(child.value(), Some("Child")); 180 | }; 181 | 182 | check_extensions(feed.extensions()); 183 | check_extensions(entry.extensions()); 184 | } 185 | 186 | #[test] 187 | fn read_eof() { 188 | let result = Feed::read_from("".as_bytes()); 189 | assert!(matches!(result, Err(Error::Eof))); 190 | } 191 | 192 | #[test] 193 | fn read_invalid_start() { 194 | let result = Feed::read_from("".as_bytes()); 195 | assert!(matches!(result, Err(Error::InvalidStartTag))); 196 | } 197 | 198 | #[test] 199 | fn read_invalid_attribute_lang() { 200 | let result = Feed::read_from("".as_bytes()); 201 | assert!(matches!(result, Err(Error::Xml(_)))); 202 | } 203 | 204 | #[test] 205 | fn read_invalid_attribute_base() { 206 | let result = Feed::read_from("".as_bytes()); 207 | assert!(matches!(result, Err(Error::Xml(_)))); 208 | } 209 | 210 | #[test] 211 | fn read_invalid_attribute_namespace() { 212 | let result = Feed::read_from("".as_bytes()); 213 | assert!(matches!(result, Err(Error::Xml(_)))); 214 | } 215 | 216 | #[test] 217 | fn read_mismatched_tags() { 218 | let result = Feed::read_from("
".as_bytes()); 219 | assert!(matches!(result, Err(Error::Xml(_)))); 220 | } 221 | 222 | #[test] 223 | fn read_internal_invalid_tag() { 224 | let result = Feed::read_from("".as_bytes()); 225 | assert!(matches!(result, Err(Error::Xml(_)))); 226 | } 227 | 228 | #[test] 229 | fn read_entry_internal_invalid_tag() { 230 | let result = Feed::read_from("".as_bytes()); 231 | assert!(matches!(result, Err(Error::Xml(_)))); 232 | } 233 | 234 | #[test] 235 | fn link_invalid_attribute() { 236 | let result = Feed::read_from("".as_bytes()); 237 | assert!(matches!(result, Err(Error::Xml(_)))); 238 | } 239 | 240 | #[test] 241 | fn text_invalid_xml_base() { 242 | let result = Feed::read_from("".as_bytes()); 243 | assert!(matches!(result, Err(Error::Xml(_)))); 244 | } 245 | 246 | #[test] 247 | fn text_invalid_xml_lang() { 248 | let result = Feed::read_from("".as_bytes()); 249 | assert!(matches!(result, Err(Error::Xml(_)))); 250 | } 251 | 252 | #[test] 253 | fn text_invalid_type() { 254 | let result = Feed::read_from("".as_bytes()); 255 | assert!(matches!(result, Err(Error::Xml(_)))); 256 | } 257 | 258 | #[test] 259 | fn author_internal_invalid_tag() { 260 | let result = Feed::read_from("".as_bytes()); 261 | assert!(matches!(result, Err(Error::Xml(_)))); 262 | } 263 | 264 | #[test] 265 | fn source_internal_invalid_tag() { 266 | let result = 267 | Feed::read_from("".as_bytes()); 268 | assert!(matches!(result, Err(Error::Xml(_)))); 269 | } 270 | 271 | #[test] 272 | fn content_invalid_xml_lang() { 273 | let result = Feed::read_from( 274 | "".as_bytes(), 275 | ); 276 | assert!(matches!(result, Err(Error::Xml(_)))); 277 | } 278 | 279 | #[test] 280 | fn content_invalid_xml_base() { 281 | let result = Feed::read_from( 282 | "".as_bytes(), 283 | ); 284 | assert!(matches!(result, Err(Error::Xml(_)))); 285 | } 286 | 287 | #[test] 288 | fn content_invalid_type() { 289 | let result = 290 | Feed::read_from("".as_bytes()); 291 | assert!(matches!(result, Err(Error::Xml(_)))); 292 | } 293 | 294 | #[test] 295 | fn content_invalid_src() { 296 | let result = 297 | Feed::read_from("".as_bytes()); 298 | assert!(matches!(result, Err(Error::Xml(_)))); 299 | } 300 | 301 | #[test] 302 | fn category_invalid_term() { 303 | let result = Feed::read_from("".as_bytes()); 304 | assert!(matches!(result, Err(Error::Xml(_)))); 305 | } 306 | 307 | #[test] 308 | fn category_invalid_scheme() { 309 | let result = Feed::read_from("".as_bytes()); 310 | assert!(matches!(result, Err(Error::Xml(_)))); 311 | } 312 | 313 | #[test] 314 | fn category_invalid_label() { 315 | let result = Feed::read_from("".as_bytes()); 316 | assert!(matches!(result, Err(Error::Xml(_)))); 317 | } 318 | 319 | #[test] 320 | fn generator_invalid_uri() { 321 | let result = Feed::read_from("".as_bytes()); 322 | assert!(matches!(result, Err(Error::Xml(_)))); 323 | } 324 | 325 | #[test] 326 | fn generator_invalid_version() { 327 | let result = Feed::read_from("".as_bytes()); 328 | assert!(matches!(result, Err(Error::Xml(_)))); 329 | } 330 | -------------------------------------------------------------------------------- /src/content.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::attributes::Attributes; 5 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 6 | use quick_xml::Reader; 7 | use quick_xml::Writer; 8 | 9 | use crate::error::{Error, XmlError}; 10 | use crate::fromxml::FromXml; 11 | use crate::toxml::ToXml; 12 | use crate::util::{atom_text, atom_xhtml, attr_value, decode}; 13 | 14 | /// Represents the content of an Atom entry 15 | // 16 | /// ## Attention 17 | /// 18 | /// Atom format specification [RFC4287](https://datatracker.ietf.org/doc/html/rfc4287#section-4.1.3.2) 19 | /// states that `src` and `value` (content) fields are mutually exclusive: 20 | /// 21 | /// > atom:content MAY have a "src" attribute, whose value MUST be an IRI reference. 22 | /// > If the "src" attribute is present, atom:content MUST be empty. 23 | /// 24 | /// Setting of both fields when authoring an Atom feed is still technically possible, 25 | /// but it will lead to a non-compliant result. 26 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 27 | #[derive(Debug, Default, Clone, PartialEq)] 28 | #[cfg_attr(feature = "builders", derive(Builder))] 29 | #[cfg_attr( 30 | feature = "builders", 31 | builder( 32 | setter(into), 33 | default, 34 | build_fn(name = "build_impl", private, error = "never::Never") 35 | ) 36 | )] 37 | pub struct Content { 38 | /// Base URL for resolving any relative references found in the element. 39 | pub base: Option, 40 | /// Indicates the natural language for the element. 41 | pub lang: Option, 42 | /// The text value of the content. 43 | pub value: Option, 44 | /// The URI of where the content can be found. 45 | pub src: Option, 46 | /// Either "text", "html", "xhtml", or the MIME type of the content. 47 | pub content_type: Option, 48 | } 49 | 50 | impl Content { 51 | /// Return base URL of the content. 52 | pub fn base(&self) -> Option<&str> { 53 | self.base.as_deref() 54 | } 55 | 56 | /// Set base URL of the content. 57 | pub fn set_base(&mut self, base: V) 58 | where 59 | V: Into>, 60 | { 61 | self.base = base.into(); 62 | } 63 | 64 | /// Return natural language of the content. 65 | pub fn lang(&self) -> Option<&str> { 66 | self.lang.as_deref() 67 | } 68 | 69 | /// Set the base URL of the content. 70 | pub fn set_lang(&mut self, lang: V) 71 | where 72 | V: Into>, 73 | { 74 | self.lang = lang.into(); 75 | } 76 | 77 | /// Return the text value of the content. 78 | /// 79 | /// If the `content_type` is neither `"text"`, `"html"`, or `"xhtml"` then the value should 80 | /// be a base64 encoded document of the indicated MIME type. 81 | /// 82 | /// # Examples 83 | /// 84 | /// ``` 85 | /// use atom_syndication::Content; 86 | /// 87 | /// let mut content = Content::default(); 88 | /// content.set_value("Example content".to_string()); 89 | /// assert_eq!(content.value(), Some("Example content")); 90 | /// ``` 91 | pub fn value(&self) -> Option<&str> { 92 | self.value.as_deref() 93 | } 94 | 95 | /// Set the text value of the content. 96 | /// 97 | /// # Examples 98 | /// 99 | /// ``` 100 | /// use atom_syndication::Content; 101 | /// 102 | /// let mut content = Content::default(); 103 | /// content.set_value("Example content".to_string()); 104 | /// ``` 105 | pub fn set_value(&mut self, value: V) 106 | where 107 | V: Into>, 108 | { 109 | self.value = value.into(); 110 | } 111 | 112 | /// Return the URI where the content can be found. 113 | /// 114 | /// # Examples 115 | /// 116 | /// ``` 117 | /// use atom_syndication::Content; 118 | /// 119 | /// let mut content = Content::default(); 120 | /// content.set_src("http://example.com/content.html".to_string()); 121 | /// assert_eq!(content.src(), Some("http://example.com/content.html")); 122 | /// ``` 123 | pub fn src(&self) -> Option<&str> { 124 | self.src.as_deref() 125 | } 126 | 127 | /// Set the URI where the content can be found. 128 | /// 129 | /// # Examples 130 | /// 131 | /// ``` 132 | /// use atom_syndication::Content; 133 | /// 134 | /// let mut content = Content::default(); 135 | /// content.set_src("http://example.com/content.html".to_string()); 136 | /// ``` 137 | pub fn set_src(&mut self, src: V) 138 | where 139 | V: Into>, 140 | { 141 | self.src = src.into(); 142 | } 143 | 144 | /// Return the type of the content. 145 | /// 146 | /// The type is either `"text"`, `"html"`, `"xhtml"`, or the MIME type of the content. 147 | /// 148 | /// # Examples 149 | /// 150 | /// ``` 151 | /// use atom_syndication::Content; 152 | /// 153 | /// let mut content = Content::default(); 154 | /// content.set_content_type("image/png".to_string()); 155 | /// assert_eq!(content.content_type(), Some("image/png")); 156 | /// ``` 157 | pub fn content_type(&self) -> Option<&str> { 158 | self.content_type.as_deref() 159 | } 160 | 161 | /// Set the type of the content. 162 | /// 163 | /// # Examples 164 | /// 165 | /// ``` 166 | /// use atom_syndication::Content; 167 | /// 168 | /// let mut content = Content::default(); 169 | /// content.set_content_type("image/png".to_string()); 170 | /// assert_eq!(content.content_type(), Some("image/png")); 171 | /// ``` 172 | pub fn set_content_type(&mut self, content_type: V) 173 | where 174 | V: Into>, 175 | { 176 | self.content_type = content_type.into(); 177 | } 178 | } 179 | 180 | impl FromXml for Content { 181 | fn from_xml( 182 | reader: &mut Reader, 183 | mut atts: Attributes<'_>, 184 | ) -> Result { 185 | let mut content = Content::default(); 186 | 187 | for att in atts.with_checks(false).flatten() { 188 | match decode(att.key.as_ref(), reader)? { 189 | Cow::Borrowed("xml:base") => { 190 | content.base = Some(attr_value(&att, reader)?.to_string()); 191 | } 192 | Cow::Borrowed("xml:lang") => { 193 | content.lang = Some(attr_value(&att, reader)?.to_string()); 194 | } 195 | Cow::Borrowed("type") => { 196 | content.content_type = Some(attr_value(&att, reader)?.to_string()); 197 | } 198 | Cow::Borrowed("src") => { 199 | content.src = Some(attr_value(&att, reader)?.to_string()); 200 | } 201 | _ => {} 202 | } 203 | } 204 | 205 | content.value = match content.content_type { 206 | Some(ref t) if t == "xhtml" => atom_xhtml(reader)?, 207 | _ => atom_text(reader)?, 208 | }; 209 | 210 | Ok(content) 211 | } 212 | } 213 | 214 | impl ToXml for Content { 215 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 216 | let name = "content"; 217 | let mut element = BytesStart::new(name); 218 | 219 | if let Some(ref base) = self.base { 220 | element.push_attribute(("xml:base", base.as_str())); 221 | } 222 | 223 | if let Some(ref lang) = self.lang { 224 | element.push_attribute(("xml:lang", lang.as_str())); 225 | } 226 | 227 | if let Some(ref content_type) = self.content_type { 228 | if content_type == "xhtml" { 229 | element.push_attribute(("type", "xhtml")); 230 | } else { 231 | element.push_attribute(("type", &**content_type)); 232 | } 233 | } 234 | 235 | if let Some(ref src) = self.src { 236 | element.push_attribute(("src", &**src)); 237 | } 238 | 239 | writer 240 | .write_event(Event::Start(element)) 241 | .map_err(XmlError::new)?; 242 | 243 | if let Some(ref value) = self.value { 244 | writer 245 | .write_event(Event::Text( 246 | if self.content_type.as_deref() == Some("xhtml") { 247 | BytesText::from_escaped(value) 248 | } else { 249 | BytesText::new(value) 250 | }, 251 | )) 252 | .map_err(XmlError::new)?; 253 | } 254 | 255 | writer 256 | .write_event(Event::End(BytesEnd::new(name))) 257 | .map_err(XmlError::new)?; 258 | 259 | Ok(()) 260 | } 261 | } 262 | 263 | #[cfg(feature = "builders")] 264 | impl ContentBuilder { 265 | /// Builds a new `Content`. 266 | pub fn build(&self) -> Content { 267 | self.build_impl().unwrap() 268 | } 269 | } 270 | 271 | #[cfg(test)] 272 | mod test { 273 | use super::*; 274 | use crate::error::Error; 275 | use crate::util::decode; 276 | 277 | fn lines(text: &str) -> Vec<&str> { 278 | text.lines() 279 | .map(|line| line.trim()) 280 | .filter(|line| !line.is_empty()) 281 | .collect::>() 282 | } 283 | 284 | fn to_xml(content: &Content) -> String { 285 | let mut buffer = Vec::new(); 286 | let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4); 287 | content.to_xml(&mut writer).unwrap(); 288 | String::from_utf8(buffer).unwrap() 289 | } 290 | 291 | fn from_xml(xml: &str) -> Result { 292 | let mut reader = Reader::from_reader(xml.as_bytes()); 293 | reader.config_mut().expand_empty_elements = true; 294 | 295 | loop { 296 | let mut buf = Vec::new(); 297 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 298 | Event::Start(element) => { 299 | if decode(element.name().as_ref(), &reader)? == "content" { 300 | let content = Content::from_xml(&mut reader, element.attributes())?; 301 | return Ok(content); 302 | } else { 303 | return Err(Error::InvalidStartTag); 304 | } 305 | } 306 | Event::Eof => return Err(Error::Eof), 307 | _ => {} 308 | } 309 | } 310 | } 311 | 312 | #[test] 313 | fn test_plain_text() { 314 | let content = Content { 315 | value: Some("Text with ampersand & .".into()), 316 | ..Default::default() 317 | }; 318 | let xml_fragment = r#"Text with ampersand & <tag>."#; 319 | assert_eq!(to_xml(&content), xml_fragment); 320 | assert_eq!(from_xml(xml_fragment).unwrap(), content); 321 | } 322 | 323 | #[test] 324 | fn test_html() { 325 | let content = Content { 326 | content_type: Some("html".into()), 327 | value: Some("Markup with ampersand, , & .".into()), 328 | ..Default::default() 329 | }; 330 | let xml_fragment = r#"Markup with ampersand, <tag>, & </closing-tag>."#; 331 | assert_eq!(to_xml(&content), xml_fragment); 332 | assert_eq!(from_xml(xml_fragment).unwrap(), content); 333 | } 334 | 335 | #[test] 336 | fn test_xhtml() { 337 | let content = Content { 338 | content_type: Some("xhtml".into()), 339 | value: Some(r#"
a line
& one more
"#.into()), 340 | ..Default::default() 341 | }; 342 | let xml_fragment = 343 | r#"
a line
& one more
"#; 344 | assert_eq!(to_xml(&content), xml_fragment); 345 | assert_eq!(from_xml(xml_fragment).unwrap(), content); 346 | } 347 | 348 | #[test] 349 | fn test_write_image() { 350 | let content = Content { 351 | content_type: Some("image/png".into()), 352 | src: Some("http://example.com/image.png".into()), 353 | ..Default::default() 354 | }; 355 | assert_eq!( 356 | lines(&to_xml(&content)), 357 | lines( 358 | r#" 359 | 360 | 361 | "# 362 | ) 363 | ); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/source.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::attributes::Attributes; 5 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 6 | use quick_xml::Reader; 7 | use quick_xml::Writer; 8 | 9 | use crate::category::Category; 10 | use crate::error::{Error, XmlError}; 11 | use crate::fromxml::FromXml; 12 | use crate::generator::Generator; 13 | use crate::link::Link; 14 | use crate::person::Person; 15 | use crate::text::Text; 16 | use crate::toxml::{ToXml, WriterExt}; 17 | use crate::util::{atom_datetime, atom_text, decode, default_fixed_datetime, skip, FixedDateTime}; 18 | 19 | /// Represents the source of an Atom entry 20 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 21 | #[derive(Debug, 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 Source { 32 | /// A human-readable title for the feed. 33 | pub title: Text, 34 | /// A universally unique and permanent URI. 35 | pub id: String, 36 | /// The last time the feed was modified in a significant way. 37 | pub updated: FixedDateTime, 38 | /// The authors of the feed. 39 | #[cfg_attr(feature = "builders", builder(setter(each = "author")))] 40 | pub authors: Vec, 41 | /// The categories that the feed belongs to. 42 | #[cfg_attr(feature = "builders", builder(setter(each = "category")))] 43 | pub categories: Vec, 44 | /// The contributors to the feed. 45 | #[cfg_attr(feature = "builders", builder(setter(each = "contributor")))] 46 | pub contributors: Vec, 47 | /// The software used to generate the feed. 48 | pub generator: Option, 49 | /// A small image which provides visual identification for the feed. 50 | pub icon: Option, 51 | /// The Web pages related to the feed. 52 | #[cfg_attr(feature = "builders", builder(setter(each = "link")))] 53 | pub links: Vec, 54 | /// A larger image which provides visual identification for the feed. 55 | pub logo: Option, 56 | /// Information about rights held in and over the feed. 57 | pub rights: Option, 58 | /// A human-readable description or subtitle for the feed. 59 | pub subtitle: Option, 60 | } 61 | 62 | impl Source { 63 | /// Return the title of the source feed. 64 | /// 65 | /// # Examples 66 | /// 67 | /// ``` 68 | /// use atom_syndication::Source; 69 | /// 70 | /// let mut source = Source::default(); 71 | /// source.set_title("Feed Title"); 72 | /// assert_eq!(source.title(), "Feed Title"); 73 | /// ``` 74 | pub fn title(&self) -> &Text { 75 | &self.title 76 | } 77 | 78 | /// Set the title of the source feed. 79 | /// 80 | /// # Examples 81 | /// 82 | /// ``` 83 | /// use atom_syndication::Source; 84 | /// 85 | /// let mut source = Source::default(); 86 | /// source.set_title("Feed Title"); 87 | /// ``` 88 | pub fn set_title(&mut self, title: V) 89 | where 90 | V: Into, 91 | { 92 | self.title = title.into(); 93 | } 94 | 95 | /// Return the unique URI of the source feed. 96 | /// 97 | /// # Examples 98 | /// 99 | /// ``` 100 | /// use atom_syndication::Source; 101 | /// 102 | /// let mut source = Source::default(); 103 | /// source.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 104 | /// assert_eq!(source.id(), "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 105 | /// ``` 106 | pub fn id(&self) -> &str { 107 | self.id.as_str() 108 | } 109 | 110 | /// Set the unique URI of the source feed. 111 | /// 112 | /// # Examples 113 | /// 114 | /// ``` 115 | /// use atom_syndication::Source; 116 | /// 117 | /// let mut source = Source::default(); 118 | /// source.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 119 | /// ``` 120 | pub fn set_id(&mut self, id: V) 121 | where 122 | V: Into, 123 | { 124 | self.id = id.into(); 125 | } 126 | 127 | /// Return the last time that the source feed was modified. 128 | /// 129 | /// # Examples 130 | /// 131 | /// ``` 132 | /// use atom_syndication::Source; 133 | /// use atom_syndication::FixedDateTime; 134 | /// use std::str::FromStr; 135 | /// 136 | /// let mut source = Source::default(); 137 | /// source.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 138 | /// assert_eq!(source.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 139 | /// ``` 140 | pub fn updated(&self) -> &FixedDateTime { 141 | &self.updated 142 | } 143 | 144 | /// Set the last time that the source feed was modified. 145 | /// 146 | /// # Examples 147 | /// 148 | /// ``` 149 | /// use atom_syndication::Source; 150 | /// use atom_syndication::FixedDateTime; 151 | /// use std::str::FromStr; 152 | /// 153 | /// let mut source = Source::default(); 154 | /// source.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 155 | /// ``` 156 | pub fn set_updated(&mut self, updated: V) 157 | where 158 | V: Into, 159 | { 160 | self.updated = updated.into(); 161 | } 162 | 163 | /// Return the authors of the source feed. 164 | /// 165 | /// # Examples 166 | /// 167 | /// ``` 168 | /// use atom_syndication::{Source, Person}; 169 | /// 170 | /// let mut source = Source::default(); 171 | /// source.set_authors(vec![Person::default()]); 172 | /// assert_eq!(source.authors().len(), 1); 173 | /// ``` 174 | pub fn authors(&self) -> &[Person] { 175 | self.authors.as_slice() 176 | } 177 | 178 | /// Set the authors of the source feed. 179 | /// 180 | /// # Examples 181 | /// 182 | /// ``` 183 | /// use atom_syndication::{Source, Person}; 184 | /// 185 | /// let mut source = Source::default(); 186 | /// source.set_authors(vec![Person::default()]); 187 | /// ``` 188 | pub fn set_authors(&mut self, authors: V) 189 | where 190 | V: Into>, 191 | { 192 | self.authors = authors.into(); 193 | } 194 | 195 | /// Return the categories the source feed belongs to. 196 | /// 197 | /// # Examples 198 | /// 199 | /// ``` 200 | /// use atom_syndication::{Source, Category}; 201 | /// 202 | /// let mut source = Source::default(); 203 | /// source.set_categories(vec![Category::default()]); 204 | /// assert_eq!(source.categories().len(), 1); 205 | /// ``` 206 | pub fn categories(&self) -> &[Category] { 207 | self.categories.as_slice() 208 | } 209 | 210 | /// Set the categories the source feed belongs to. 211 | /// 212 | /// # Examples 213 | /// 214 | /// ``` 215 | /// use atom_syndication::{Source, Category}; 216 | /// 217 | /// let mut source = Source::default(); 218 | /// source.set_categories(vec![Category::default()]); 219 | /// ``` 220 | pub fn set_categories(&mut self, categories: V) 221 | where 222 | V: Into>, 223 | { 224 | self.categories = categories.into(); 225 | } 226 | 227 | /// Return the contributors to the source feed. 228 | /// 229 | /// # Examples 230 | /// 231 | /// ``` 232 | /// use atom_syndication::{Source, Person}; 233 | /// 234 | /// let mut source = Source::default(); 235 | /// source.set_contributors(vec![Person::default()]); 236 | /// assert_eq!(source.contributors().len(), 1); 237 | /// ``` 238 | pub fn contributors(&self) -> &[Person] { 239 | self.contributors.as_slice() 240 | } 241 | 242 | /// Set the contributors to the source feed. 243 | /// 244 | /// # Examples 245 | /// 246 | /// ``` 247 | /// use atom_syndication::{Source, Person}; 248 | /// 249 | /// let mut source = Source::default(); 250 | /// source.set_contributors(vec![Person::default()]); 251 | /// ``` 252 | pub fn set_contributors(&mut self, contributors: V) 253 | where 254 | V: Into>, 255 | { 256 | self.contributors = contributors.into(); 257 | } 258 | 259 | /// Return the name of the software used to generate the source feed. 260 | /// 261 | /// # Examples 262 | /// 263 | /// ``` 264 | /// use atom_syndication::{Source, Generator}; 265 | /// 266 | /// let mut source = Source::default(); 267 | /// source.set_generator(Generator::default()); 268 | /// assert!(source.generator().is_some()); 269 | /// ``` 270 | pub fn generator(&self) -> Option<&Generator> { 271 | self.generator.as_ref() 272 | } 273 | 274 | /// Set the name of the software used to generate the source feed. 275 | /// 276 | /// # Examples 277 | /// 278 | /// ``` 279 | /// use atom_syndication::{Source, Generator}; 280 | /// 281 | /// let mut source = Source::default(); 282 | /// source.set_generator(Generator::default()); 283 | /// ``` 284 | pub fn set_generator(&mut self, generator: V) 285 | where 286 | V: Into>, 287 | { 288 | self.generator = generator.into() 289 | } 290 | 291 | /// Return the icon for the source feed. 292 | /// 293 | /// # Examples 294 | /// 295 | /// ``` 296 | /// use atom_syndication::Source; 297 | /// 298 | /// let mut source = Source::default(); 299 | /// source.set_icon("http://example.com/icon.png".to_string()); 300 | /// assert_eq!(source.icon(), Some("http://example.com/icon.png")); 301 | /// ``` 302 | pub fn icon(&self) -> Option<&str> { 303 | self.icon.as_deref() 304 | } 305 | 306 | /// Set the icon for the source feed. 307 | /// 308 | /// # Examples 309 | /// 310 | /// ``` 311 | /// use atom_syndication::Source; 312 | /// 313 | /// let mut source = Source::default(); 314 | /// source.set_icon("http://example.com/icon.png".to_string()); 315 | /// ``` 316 | pub fn set_icon(&mut self, icon: V) 317 | where 318 | V: Into>, 319 | { 320 | self.icon = icon.into() 321 | } 322 | 323 | /// Return the Web pages related to the source feed. 324 | /// 325 | /// # Examples 326 | /// 327 | /// ``` 328 | /// use atom_syndication::{Source, Link}; 329 | /// 330 | /// let mut source = Source::default(); 331 | /// source.set_links(vec![Link::default()]); 332 | /// assert_eq!(source.links().len(), 1); 333 | /// ``` 334 | pub fn links(&self) -> &[Link] { 335 | self.links.as_slice() 336 | } 337 | 338 | /// Set the Web pages related to the source feed. 339 | /// 340 | /// # Examples 341 | /// 342 | /// ``` 343 | /// use atom_syndication::{Source, Link}; 344 | /// 345 | /// let mut source = Source::default(); 346 | /// source.set_links(vec![Link::default()]); 347 | /// ``` 348 | pub fn set_links(&mut self, links: V) 349 | where 350 | V: Into>, 351 | { 352 | self.links = links.into(); 353 | } 354 | 355 | /// Return the logo for the source feed. 356 | /// 357 | /// # Examples 358 | /// 359 | /// ``` 360 | /// use atom_syndication::Source; 361 | /// 362 | /// let mut source = Source::default(); 363 | /// source.set_logo("http://example.com/logo.png".to_string()); 364 | /// assert_eq!(source.logo(), Some("http://example.com/logo.png")); 365 | /// ``` 366 | pub fn logo(&self) -> Option<&str> { 367 | self.logo.as_deref() 368 | } 369 | 370 | /// Set the logo for the source feed. 371 | /// 372 | /// # Examples 373 | /// 374 | /// ``` 375 | /// use atom_syndication::Source; 376 | /// 377 | /// let mut source = Source::default(); 378 | /// source.set_logo("http://example.com/logo.png".to_string()); 379 | /// ``` 380 | pub fn set_logo(&mut self, logo: V) 381 | where 382 | V: Into>, 383 | { 384 | self.logo = logo.into() 385 | } 386 | 387 | /// Return the information about the rights held in and over the source feed. 388 | /// 389 | /// # Examples 390 | /// 391 | /// ``` 392 | /// use atom_syndication::{Source, Text}; 393 | /// 394 | /// let mut source = Source::default(); 395 | /// source.set_rights(Text::from("© 2017 John Doe")); 396 | /// assert_eq!(source.rights().map(Text::as_str), Some("© 2017 John Doe")); 397 | /// ``` 398 | pub fn rights(&self) -> Option<&Text> { 399 | self.rights.as_ref() 400 | } 401 | 402 | /// Set the information about the rights held in and over the source feed. 403 | /// 404 | /// # Examples 405 | /// 406 | /// ``` 407 | /// use atom_syndication::{Source, Text}; 408 | /// 409 | /// let mut source = Source::default(); 410 | /// source.set_rights(Text::from("© 2017 John Doe")); 411 | /// ``` 412 | pub fn set_rights(&mut self, rights: V) 413 | where 414 | V: Into>, 415 | { 416 | self.rights = rights.into() 417 | } 418 | 419 | /// Return the description or subtitle of the source feed. 420 | /// 421 | /// # Examples 422 | /// 423 | /// ``` 424 | /// use atom_syndication::{Source, Text}; 425 | /// 426 | /// let mut source = Source::default(); 427 | /// source.set_subtitle(Text::from("Feed subtitle")); 428 | /// assert_eq!(source.subtitle().map(Text::as_str), Some("Feed subtitle")); 429 | /// ``` 430 | pub fn subtitle(&self) -> Option<&Text> { 431 | self.subtitle.as_ref() 432 | } 433 | 434 | /// Set the description or subtitle of the source feed. 435 | /// 436 | /// # Examples 437 | /// 438 | /// ``` 439 | /// use atom_syndication::{Source, Text}; 440 | /// 441 | /// let mut source = Source::default(); 442 | /// source.set_subtitle(Text::from("Feed subtitle")); 443 | /// ``` 444 | pub fn set_subtitle(&mut self, subtitle: V) 445 | where 446 | V: Into>, 447 | { 448 | self.subtitle = subtitle.into() 449 | } 450 | } 451 | 452 | impl FromXml for Source { 453 | fn from_xml(reader: &mut Reader, _: Attributes<'_>) -> Result { 454 | let mut source = Source::default(); 455 | let mut buf = Vec::new(); 456 | 457 | loop { 458 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 459 | Event::Start(element) => match decode(element.name().as_ref(), reader)? { 460 | Cow::Borrowed("id") => source.id = atom_text(reader)?.unwrap_or_default(), 461 | Cow::Borrowed("title") => { 462 | source.title = Text::from_xml(reader, element.attributes())? 463 | } 464 | Cow::Borrowed("updated") => { 465 | source.updated = 466 | atom_datetime(reader)?.unwrap_or_else(default_fixed_datetime) 467 | } 468 | Cow::Borrowed("author") => source 469 | .authors 470 | .push(Person::from_xml(reader, element.attributes())?), 471 | Cow::Borrowed("category") => { 472 | source 473 | .categories 474 | .push(Category::from_xml(reader, &element)?); 475 | skip(element.name(), reader)?; 476 | } 477 | Cow::Borrowed("contributor") => source 478 | .contributors 479 | .push(Person::from_xml(reader, element.attributes())?), 480 | Cow::Borrowed("generator") => { 481 | source.generator = Some(Generator::from_xml(reader, element.attributes())?) 482 | } 483 | Cow::Borrowed("icon") => source.icon = atom_text(reader)?, 484 | Cow::Borrowed("link") => { 485 | source.links.push(Link::from_xml(reader, &element)?); 486 | skip(element.name(), reader)?; 487 | } 488 | Cow::Borrowed("logo") => source.logo = atom_text(reader)?, 489 | Cow::Borrowed("rights") => { 490 | source.rights = Some(Text::from_xml(reader, element.attributes())?) 491 | } 492 | Cow::Borrowed("subtitle") => { 493 | source.subtitle = Some(Text::from_xml(reader, element.attributes())?) 494 | } 495 | _ => skip(element.name(), reader)?, 496 | }, 497 | Event::End(_) => break, 498 | Event::Eof => return Err(Error::Eof), 499 | _ => {} 500 | } 501 | 502 | buf.clear(); 503 | } 504 | 505 | Ok(source) 506 | } 507 | } 508 | 509 | impl ToXml for Source { 510 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 511 | let name = "source"; 512 | writer 513 | .write_event(Event::Start(BytesStart::new(name))) 514 | .map_err(XmlError::new)?; 515 | writer.write_object_named(&self.title, "title")?; 516 | writer.write_text_element("id", &self.id)?; 517 | writer.write_text_element("updated", &self.updated.to_rfc3339())?; 518 | writer.write_objects_named(&self.authors, "author")?; 519 | writer.write_objects(&self.categories)?; 520 | writer.write_objects_named(&self.contributors, "contributor")?; 521 | 522 | if let Some(ref generator) = self.generator { 523 | writer.write_object(generator)?; 524 | } 525 | 526 | if let Some(ref icon) = self.icon { 527 | writer.write_text_element("icon", icon)?; 528 | } 529 | 530 | writer.write_objects(&self.links)?; 531 | 532 | if let Some(ref logo) = self.logo { 533 | writer.write_text_element("logo", logo)?; 534 | } 535 | 536 | if let Some(ref rights) = self.rights { 537 | writer.write_object_named(rights, "rights")?; 538 | } 539 | 540 | if let Some(ref subtitle) = self.subtitle { 541 | writer.write_object_named(subtitle, "subtitle")?; 542 | } 543 | 544 | writer 545 | .write_event(Event::End(BytesEnd::new(name))) 546 | .map_err(XmlError::new)?; 547 | 548 | Ok(()) 549 | } 550 | } 551 | 552 | impl Default for Source { 553 | fn default() -> Self { 554 | Source { 555 | title: Text::default(), 556 | id: String::new(), 557 | updated: default_fixed_datetime(), 558 | authors: Vec::new(), 559 | categories: Vec::new(), 560 | contributors: Vec::new(), 561 | generator: None, 562 | icon: None, 563 | links: Vec::new(), 564 | logo: None, 565 | rights: None, 566 | subtitle: None, 567 | } 568 | } 569 | } 570 | 571 | #[cfg(feature = "builders")] 572 | impl SourceBuilder { 573 | /// Builds a new `Source`. 574 | pub fn build(&self) -> Source { 575 | self.build_impl().unwrap() 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /src/entry.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io::{BufRead, Write}; 3 | 4 | use quick_xml::events::attributes::Attributes; 5 | use quick_xml::events::{BytesEnd, BytesStart, Event}; 6 | use quick_xml::Reader; 7 | use quick_xml::Writer; 8 | 9 | use crate::category::Category; 10 | use crate::content::Content; 11 | use crate::error::{Error, XmlError}; 12 | use crate::extension::util::{extension_name, parse_extension}; 13 | use crate::extension::ExtensionMap; 14 | use crate::fromxml::FromXml; 15 | use crate::link::Link; 16 | use crate::person::Person; 17 | use crate::source::Source; 18 | use crate::text::Text; 19 | use crate::toxml::{ToXml, WriterExt}; 20 | use crate::util::{atom_datetime, atom_text, decode, default_fixed_datetime, skip, FixedDateTime}; 21 | 22 | /// Represents an entry in an Atom feed 23 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 24 | #[derive(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 Entry { 35 | /// A human-readable title for the entry. 36 | pub title: Text, 37 | /// A universally unique and permanent URI. 38 | pub id: String, 39 | /// The last time the entry was modified. 40 | pub updated: FixedDateTime, 41 | /// The authors of the feed. 42 | #[cfg_attr(feature = "builders", builder(setter(each = "author")))] 43 | pub authors: Vec, 44 | /// The categories that the entry belongs to. 45 | #[cfg_attr(feature = "builders", builder(setter(each = "category")))] 46 | pub categories: Vec, 47 | /// The contributors to the entry. 48 | #[cfg_attr(feature = "builders", builder(setter(each = "contributor")))] 49 | pub contributors: Vec, 50 | /// The Web pages related to the entry. 51 | #[cfg_attr(feature = "builders", builder(setter(each = "link")))] 52 | pub links: Vec, 53 | /// The time of the initial creation or first availability of the entry. 54 | pub published: Option, 55 | /// Information about rights held in and over the entry. 56 | pub rights: Option, 57 | /// The source information if an entry is copied from one feed into another feed. 58 | pub source: Option, 59 | /// A short summary, abstract, or excerpt of the entry. 60 | pub summary: Option, 61 | /// Contains or links to the complete content of the entry. 62 | pub content: Option, 63 | /// The extensions for this entry. 64 | #[cfg_attr(feature = "builders", builder(setter(each = "extension")))] 65 | pub extensions: ExtensionMap, 66 | } 67 | 68 | impl Entry { 69 | /// Return the title of this entry. 70 | /// 71 | /// # Examples 72 | /// 73 | /// ``` 74 | /// use atom_syndication::Entry; 75 | /// 76 | /// let mut entry = Entry::default(); 77 | /// entry.set_title("Entry Title"); 78 | /// assert_eq!(entry.title(), "Entry Title"); 79 | /// ``` 80 | pub fn title(&self) -> &Text { 81 | &self.title 82 | } 83 | 84 | /// Set the title of this entry. 85 | /// 86 | /// # Examples 87 | /// 88 | /// ``` 89 | /// use atom_syndication::Entry; 90 | /// 91 | /// let mut entry = Entry::default(); 92 | /// entry.set_title("Entry Title"); 93 | /// ``` 94 | pub fn set_title(&mut self, title: V) 95 | where 96 | V: Into, 97 | { 98 | self.title = title.into(); 99 | } 100 | 101 | /// Return the unique URI of this entry. 102 | /// 103 | /// # Examples 104 | /// 105 | /// ``` 106 | /// use atom_syndication::Entry; 107 | /// 108 | /// let mut entry = Entry::default(); 109 | /// entry.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 110 | /// assert_eq!(entry.id(), "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 111 | /// ``` 112 | pub fn id(&self) -> &str { 113 | self.id.as_str() 114 | } 115 | 116 | /// Set the unique URI of this entry. 117 | /// 118 | /// # Examples 119 | /// 120 | /// ``` 121 | /// use atom_syndication::Entry; 122 | /// 123 | /// let mut entry = Entry::default(); 124 | /// entry.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 125 | /// ``` 126 | pub fn set_id(&mut self, id: V) 127 | where 128 | V: Into, 129 | { 130 | self.id = id.into(); 131 | } 132 | 133 | /// Return the last time that this entry was modified. 134 | /// 135 | /// # Examples 136 | /// 137 | /// ``` 138 | /// use atom_syndication::Entry; 139 | /// use atom_syndication::FixedDateTime; 140 | /// use std::str::FromStr; 141 | /// 142 | /// let mut entry = Entry::default(); 143 | /// entry.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 144 | /// assert_eq!(entry.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 145 | /// ``` 146 | pub fn updated(&self) -> &FixedDateTime { 147 | &self.updated 148 | } 149 | 150 | /// Set the last time that this entry was modified. 151 | /// 152 | /// # Examples 153 | /// 154 | /// ``` 155 | /// use atom_syndication::Entry; 156 | /// use atom_syndication::FixedDateTime; 157 | /// use std::str::FromStr; 158 | /// 159 | /// let mut entry = Entry::default(); 160 | /// entry.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 161 | /// ``` 162 | pub fn set_updated(&mut self, updated: V) 163 | where 164 | V: Into, 165 | { 166 | self.updated = updated.into(); 167 | } 168 | 169 | /// Return the authors of this entry. 170 | /// 171 | /// # Examples 172 | /// 173 | /// ``` 174 | /// use atom_syndication::{Entry, Person}; 175 | /// 176 | /// let mut entry = Entry::default(); 177 | /// entry.set_authors(vec![Person::default()]); 178 | /// assert_eq!(entry.authors().len(), 1); 179 | /// ``` 180 | pub fn authors(&self) -> &[Person] { 181 | self.authors.as_slice() 182 | } 183 | 184 | /// Set the authors of this entry. 185 | /// 186 | /// # Examples 187 | /// 188 | /// ``` 189 | /// use atom_syndication::{Entry, Person}; 190 | /// 191 | /// let mut entry = Entry::default(); 192 | /// entry.set_authors(vec![Person::default()]); 193 | /// ``` 194 | pub fn set_authors(&mut self, authors: V) 195 | where 196 | V: Into>, 197 | { 198 | self.authors = authors.into(); 199 | } 200 | 201 | /// Return the categories this entry belongs to. 202 | /// 203 | /// # Examples 204 | /// 205 | /// ``` 206 | /// use atom_syndication::{Entry, Category}; 207 | /// 208 | /// let mut entry = Entry::default(); 209 | /// entry.set_categories(vec![Category::default()]); 210 | /// assert_eq!(entry.categories().len(), 1); 211 | /// ``` 212 | pub fn categories(&self) -> &[Category] { 213 | self.categories.as_slice() 214 | } 215 | 216 | /// Set the categories this entry belongs to. 217 | /// 218 | /// # Examples 219 | /// 220 | /// ``` 221 | /// use atom_syndication::{Entry, Category}; 222 | /// 223 | /// let mut entry = Entry::default(); 224 | /// entry.set_categories(vec![Category::default()]); 225 | /// ``` 226 | pub fn set_categories(&mut self, categories: V) 227 | where 228 | V: Into>, 229 | { 230 | self.categories = categories.into(); 231 | } 232 | 233 | /// Return the contributors to this entry. 234 | /// 235 | /// # Examples 236 | /// 237 | /// ``` 238 | /// use atom_syndication::{Entry, Person}; 239 | /// 240 | /// let mut entry = Entry::default(); 241 | /// entry.set_contributors(vec![Person::default()]); 242 | /// assert_eq!(entry.contributors().len(), 1); 243 | /// ``` 244 | pub fn contributors(&self) -> &[Person] { 245 | self.contributors.as_slice() 246 | } 247 | 248 | /// Set the contributors to this entry. 249 | /// 250 | /// # Examples 251 | /// 252 | /// ``` 253 | /// use atom_syndication::{Entry, Person}; 254 | /// 255 | /// let mut entry = Entry::default(); 256 | /// entry.set_contributors(vec![Person::default()]); 257 | /// ``` 258 | pub fn set_contributors(&mut self, contributors: V) 259 | where 260 | V: Into>, 261 | { 262 | self.contributors = contributors.into(); 263 | } 264 | 265 | /// Return the links for this entry. 266 | /// 267 | /// # Examples 268 | /// 269 | /// ``` 270 | /// use atom_syndication::{Entry, Link}; 271 | /// 272 | /// let mut entry = Entry::default(); 273 | /// entry.set_links(vec![Link::default()]); 274 | /// assert_eq!(entry.links().len(), 1); 275 | /// ``` 276 | pub fn links(&self) -> &[Link] { 277 | self.links.as_slice() 278 | } 279 | 280 | /// Set the links for this entry. 281 | /// 282 | /// # Examples 283 | /// 284 | /// ``` 285 | /// use atom_syndication::{Entry, Link}; 286 | /// 287 | /// let mut entry = Entry::default(); 288 | /// entry.set_links(vec![Link::default()]); 289 | /// ``` 290 | pub fn set_links(&mut self, links: V) 291 | where 292 | V: Into>, 293 | { 294 | self.links = links.into(); 295 | } 296 | 297 | /// Return the time that this entry was initially created or first made available. 298 | /// 299 | /// # Examples 300 | /// 301 | /// ``` 302 | /// use atom_syndication::Entry; 303 | /// use atom_syndication::FixedDateTime; 304 | /// use std::str::FromStr; 305 | /// 306 | /// let mut entry = Entry::default(); 307 | /// entry.set_published(FixedDateTime::from_str("2017-06-01T15:15:44-05:00").unwrap()); 308 | /// assert_eq!(entry.published().map(|x|x.to_rfc3339()), Some("2017-06-01T15:15:44-05:00".to_string())); 309 | /// ``` 310 | pub fn published(&self) -> Option<&FixedDateTime> { 311 | self.published.as_ref() 312 | } 313 | 314 | /// Set the time that this entry was initially created or first made available. 315 | /// 316 | /// # Examples 317 | /// 318 | /// ``` 319 | /// use atom_syndication::Entry; 320 | /// use atom_syndication::FixedDateTime; 321 | /// use std::str::FromStr; 322 | /// 323 | /// let mut entry = Entry::default(); 324 | /// entry.set_published(FixedDateTime::from_str("2017-06-01T15:15:44-05:00").unwrap()); 325 | /// ``` 326 | pub fn set_published(&mut self, published: V) 327 | where 328 | V: Into>, 329 | { 330 | self.published = published.into(); 331 | } 332 | 333 | /// Return the information about the rights held in and over this entry. 334 | /// 335 | /// # Examples 336 | /// 337 | /// ``` 338 | /// use atom_syndication::{Entry, Text}; 339 | /// 340 | /// let mut entry = Entry::default(); 341 | /// entry.set_rights(Text::from("© 2017 John Doe")); 342 | /// assert_eq!(entry.rights().map(Text::as_str), Some("© 2017 John Doe")); 343 | /// ``` 344 | pub fn rights(&self) -> Option<&Text> { 345 | self.rights.as_ref() 346 | } 347 | 348 | /// Set the information about the rights held in and over this entry. 349 | /// 350 | /// # Examples 351 | /// 352 | /// ``` 353 | /// use atom_syndication::{Entry, Text}; 354 | /// 355 | /// let mut entry = Entry::default(); 356 | /// entry.set_rights(Text::from("© 2017 John Doe")); 357 | /// ``` 358 | pub fn set_rights(&mut self, rights: V) 359 | where 360 | V: Into>, 361 | { 362 | self.rights = rights.into(); 363 | } 364 | 365 | /// Return the source of this entry if it was copied from another feed. 366 | /// 367 | /// # Examples 368 | /// 369 | /// ``` 370 | /// use atom_syndication::{Entry, Source}; 371 | /// 372 | /// let mut entry = Entry::default(); 373 | /// entry.set_source(Source::default()); 374 | /// assert!(entry.source().is_some()); 375 | /// ``` 376 | pub fn source(&self) -> Option<&Source> { 377 | self.source.as_ref() 378 | } 379 | 380 | /// Set the source of this entry if it was copied from another feed. 381 | /// 382 | /// # Examples 383 | /// 384 | /// ``` 385 | /// use atom_syndication::{Entry, Source}; 386 | /// 387 | /// let mut entry = Entry::default(); 388 | /// entry.set_source(Source::default()); 389 | /// ``` 390 | pub fn set_source(&mut self, source: V) 391 | where 392 | V: Into>, 393 | { 394 | self.source = source.into() 395 | } 396 | 397 | /// Return the summary of this entry. 398 | /// 399 | /// # Examples 400 | /// 401 | /// ``` 402 | /// use atom_syndication::{Entry, Text}; 403 | /// 404 | /// let mut entry = Entry::default(); 405 | /// entry.set_summary(Text::from("Entry summary.")); 406 | /// assert_eq!(entry.summary().map(Text::as_str), Some("Entry summary.")); 407 | /// ``` 408 | pub fn summary(&self) -> Option<&Text> { 409 | self.summary.as_ref() 410 | } 411 | 412 | /// Set the summary of this entry. 413 | /// 414 | /// # Examples 415 | /// 416 | /// ``` 417 | /// use atom_syndication::{Entry, Text}; 418 | /// 419 | /// let mut entry = Entry::default(); 420 | /// entry.set_summary(Text::from("Entry summary.")); 421 | /// ``` 422 | pub fn set_summary(&mut self, summary: V) 423 | where 424 | V: Into>, 425 | { 426 | self.summary = summary.into(); 427 | } 428 | 429 | /// Return the content of this entry. 430 | /// 431 | /// # Examples 432 | /// 433 | /// ``` 434 | /// use atom_syndication::{Entry, Content}; 435 | /// 436 | /// let mut entry = Entry::default(); 437 | /// entry.set_content(Content::default()); 438 | /// assert!(entry.content().is_some()); 439 | /// ``` 440 | pub fn content(&self) -> Option<&Content> { 441 | self.content.as_ref() 442 | } 443 | 444 | /// Set the content of this entry. 445 | /// 446 | /// # Examples 447 | /// 448 | /// ``` 449 | /// use atom_syndication::{Entry, Content}; 450 | /// 451 | /// let mut entry = Entry::default(); 452 | /// entry.set_content(Content::default()); 453 | /// assert!(entry.content().is_some()); 454 | /// ``` 455 | pub fn set_content(&mut self, content: V) 456 | where 457 | V: Into>, 458 | { 459 | self.content = content.into(); 460 | } 461 | 462 | /// Return the extensions for this entry. 463 | /// 464 | /// # Examples 465 | /// 466 | /// ``` 467 | /// use std::collections::BTreeMap; 468 | /// use atom_syndication::Entry; 469 | /// use atom_syndication::extension::{ExtensionMap, Extension}; 470 | /// 471 | /// let extension = Extension::default(); 472 | /// 473 | /// let mut item_map = BTreeMap::>::new(); 474 | /// item_map.insert("ext:name".to_string(), vec![extension]); 475 | /// 476 | /// let mut extension_map = ExtensionMap::default(); 477 | /// extension_map.insert("ext".to_string(), item_map); 478 | /// 479 | /// let mut entry = Entry::default(); 480 | /// entry.set_extensions(extension_map); 481 | /// assert_eq!(entry.extensions() 482 | /// .get("ext") 483 | /// .and_then(|m| m.get("ext:name")) 484 | /// .map(|v| v.len()), 485 | /// Some(1)); 486 | /// ``` 487 | pub fn extensions(&self) -> &ExtensionMap { 488 | &self.extensions 489 | } 490 | 491 | /// Set the extensions for this entry. 492 | /// 493 | /// # Examples 494 | /// 495 | /// ``` 496 | /// use atom_syndication::Entry; 497 | /// use atom_syndication::extension::ExtensionMap; 498 | /// 499 | /// let mut entry = Entry::default(); 500 | /// entry.set_extensions(ExtensionMap::default()); 501 | /// ``` 502 | pub fn set_extensions(&mut self, extensions: V) 503 | where 504 | V: Into, 505 | { 506 | self.extensions = extensions.into() 507 | } 508 | } 509 | 510 | impl FromXml for Entry { 511 | fn from_xml(reader: &mut Reader, _: Attributes<'_>) -> Result { 512 | let mut entry = Entry::default(); 513 | let mut buf = Vec::new(); 514 | 515 | loop { 516 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 517 | Event::Start(element) => match decode(element.name().as_ref(), reader)? { 518 | Cow::Borrowed("id") => entry.id = atom_text(reader)?.unwrap_or_default(), 519 | Cow::Borrowed("title") => { 520 | entry.title = Text::from_xml(reader, element.attributes())? 521 | } 522 | Cow::Borrowed("updated") => { 523 | entry.updated = 524 | atom_datetime(reader)?.unwrap_or_else(default_fixed_datetime) 525 | } 526 | Cow::Borrowed("author") => entry 527 | .authors 528 | .push(Person::from_xml(reader, element.attributes())?), 529 | Cow::Borrowed("category") => { 530 | entry.categories.push(Category::from_xml(reader, &element)?); 531 | skip(element.name(), reader)?; 532 | } 533 | Cow::Borrowed("contributor") => entry 534 | .contributors 535 | .push(Person::from_xml(reader, element.attributes())?), 536 | Cow::Borrowed("link") => { 537 | entry.links.push(Link::from_xml(reader, &element)?); 538 | skip(element.name(), reader)?; 539 | } 540 | Cow::Borrowed("published") => entry.published = atom_datetime(reader)?, 541 | Cow::Borrowed("rights") => { 542 | entry.rights = Some(Text::from_xml(reader, element.attributes())?) 543 | } 544 | Cow::Borrowed("source") => { 545 | entry.source = Some(Source::from_xml(reader, element.attributes())?) 546 | } 547 | Cow::Borrowed("summary") => { 548 | entry.summary = Some(Text::from_xml(reader, element.attributes())?) 549 | } 550 | Cow::Borrowed("content") => { 551 | entry.content = Some(Content::from_xml(reader, element.attributes())?) 552 | } 553 | n => { 554 | if let Some((ns, name)) = extension_name(n.as_ref()) { 555 | parse_extension( 556 | reader, 557 | element.attributes(), 558 | ns, 559 | name, 560 | &mut entry.extensions, 561 | )?; 562 | } else { 563 | skip(element.name(), reader)?; 564 | } 565 | } 566 | }, 567 | Event::End(_) => break, 568 | Event::Eof => return Err(Error::Eof), 569 | _ => {} 570 | } 571 | 572 | buf.clear(); 573 | } 574 | 575 | Ok(entry) 576 | } 577 | } 578 | 579 | impl ToXml for Entry { 580 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 581 | let name = "entry"; 582 | writer 583 | .write_event(Event::Start(BytesStart::new(name))) 584 | .map_err(XmlError::new)?; 585 | writer.write_object_named(&self.title, "title")?; 586 | writer.write_text_element("id", &self.id)?; 587 | writer.write_text_element("updated", &self.updated.to_rfc3339())?; 588 | writer.write_objects_named(&self.authors, "author")?; 589 | writer.write_objects(&self.categories)?; 590 | writer.write_objects_named(&self.contributors, "contributor")?; 591 | writer.write_objects(&self.links)?; 592 | 593 | if let Some(ref published) = self.published { 594 | writer.write_text_element("published", &published.to_rfc3339())?; 595 | } 596 | 597 | if let Some(ref rights) = self.rights { 598 | writer.write_object_named(rights, "rights")?; 599 | } 600 | 601 | if let Some(ref source) = self.source { 602 | writer.write_object(source)?; 603 | } 604 | 605 | if let Some(ref summary) = self.summary { 606 | writer.write_object_named(summary, "summary")?; 607 | } 608 | 609 | if let Some(ref content) = self.content { 610 | writer.write_object(content)?; 611 | } 612 | 613 | for map in self.extensions.values() { 614 | for extensions in map.values() { 615 | writer.write_objects(extensions)?; 616 | } 617 | } 618 | 619 | writer 620 | .write_event(Event::End(BytesEnd::new(name))) 621 | .map_err(XmlError::new)?; 622 | 623 | Ok(()) 624 | } 625 | } 626 | 627 | impl Default for Entry { 628 | fn default() -> Self { 629 | Entry { 630 | title: Text::default(), 631 | id: String::new(), 632 | updated: default_fixed_datetime(), 633 | authors: Vec::new(), 634 | categories: Vec::new(), 635 | contributors: Vec::new(), 636 | links: Vec::new(), 637 | published: None, 638 | rights: None, 639 | source: None, 640 | summary: None, 641 | content: None, 642 | extensions: ExtensionMap::default(), 643 | } 644 | } 645 | } 646 | 647 | #[cfg(feature = "builders")] 648 | impl EntryBuilder { 649 | /// Builds a new `Entry`. 650 | pub fn build(&self) -> Entry { 651 | self.build_impl().unwrap() 652 | } 653 | } 654 | -------------------------------------------------------------------------------- /src/feed.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::BTreeMap; 3 | use std::io::{BufRead, Write}; 4 | use std::str::{self, FromStr}; 5 | 6 | use quick_xml::events::attributes::Attributes; 7 | use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; 8 | use quick_xml::{Reader, Writer}; 9 | 10 | use crate::category::Category; 11 | use crate::entry::Entry; 12 | use crate::error::{Error, XmlError}; 13 | use crate::extension::util::{extension_name, parse_extension}; 14 | use crate::extension::ExtensionMap; 15 | use crate::fromxml::FromXml; 16 | use crate::generator::Generator; 17 | use crate::link::Link; 18 | use crate::person::Person; 19 | use crate::text::Text; 20 | use crate::toxml::{ToXml, WriterExt}; 21 | use crate::util::{ 22 | atom_datetime, atom_text, attr_value, decode, default_fixed_datetime, skip, FixedDateTime, 23 | }; 24 | 25 | /// Various options which control XML writer 26 | #[derive(Clone, Copy)] 27 | pub struct WriteConfig { 28 | /// Write XML document declaration at the beginning of a document. Default is `true`. 29 | pub write_document_declaration: bool, 30 | /// Indent XML tags. Default is `None`. 31 | pub indent_size: Option, 32 | } 33 | 34 | impl Default for WriteConfig { 35 | fn default() -> Self { 36 | Self { 37 | write_document_declaration: true, 38 | indent_size: None, 39 | } 40 | } 41 | } 42 | 43 | /// Represents an Atom feed 44 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 45 | #[derive(Debug, Clone, PartialEq)] 46 | #[cfg_attr(feature = "builders", derive(Builder))] 47 | #[cfg_attr( 48 | feature = "builders", 49 | builder( 50 | setter(into), 51 | default, 52 | build_fn(name = "build_impl", private, error = "never::Never") 53 | ) 54 | )] 55 | pub struct Feed { 56 | /// A human-readable title for the feed. 57 | pub title: Text, 58 | /// A universally unique and permanent URI. 59 | pub id: String, 60 | /// The last time the feed was modified in a significant way. 61 | pub updated: FixedDateTime, 62 | /// The authors of the feed. 63 | #[cfg_attr(feature = "builders", builder(setter(each = "author")))] 64 | pub authors: Vec, 65 | /// The categories that the feed belongs to. 66 | #[cfg_attr(feature = "builders", builder(setter(each = "category")))] 67 | pub categories: Vec, 68 | /// The contributors to the feed. 69 | #[cfg_attr(feature = "builders", builder(setter(each = "contributor")))] 70 | pub contributors: Vec, 71 | /// The software used to generate the feed. 72 | pub generator: Option, 73 | /// A small image which provides visual identification for the feed. 74 | pub icon: Option, 75 | /// The Web pages related to the feed. 76 | #[cfg_attr(feature = "builders", builder(setter(each = "link")))] 77 | pub links: Vec, 78 | /// A larger image which provides visual identification for the feed. 79 | pub logo: Option, 80 | /// Information about rights held in and over the feed. 81 | pub rights: Option, 82 | /// A human-readable description or subtitle for the feed. 83 | pub subtitle: Option, 84 | /// The entries contained in the feed. 85 | #[cfg_attr(feature = "builders", builder(setter(each = "entry")))] 86 | pub entries: Vec, 87 | /// The extensions for the feed. 88 | #[cfg_attr(feature = "builders", builder(setter(each = "extension")))] 89 | pub extensions: ExtensionMap, 90 | /// The namespaces present in the feed tag. 91 | #[cfg_attr(feature = "builders", builder(setter(each = "namespace")))] 92 | pub namespaces: BTreeMap, 93 | /// Base URL for resolving any relative references found in the element. 94 | pub base: Option, 95 | /// Indicates the natural language for the element. 96 | pub lang: Option, 97 | } 98 | 99 | impl Feed { 100 | /// Attempt to read an Atom feed from the reader. 101 | /// 102 | /// # Examples 103 | /// 104 | /// ```no_run 105 | /// use std::io::BufReader; 106 | /// use std::fs::File; 107 | /// use atom_syndication::Feed; 108 | /// 109 | /// let file = File::open("example.xml").unwrap(); 110 | /// let feed = Feed::read_from(BufReader::new(file)).unwrap(); 111 | /// ``` 112 | pub fn read_from(reader: B) -> Result { 113 | let mut reader = Reader::from_reader(reader); 114 | reader.config_mut().expand_empty_elements = true; 115 | 116 | let mut buf = Vec::new(); 117 | 118 | loop { 119 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 120 | Event::Start(element) => { 121 | if decode(element.name().as_ref(), &reader)? == "feed" { 122 | return Feed::from_xml(&mut reader, element.attributes()); 123 | } else { 124 | return Err(Error::InvalidStartTag); 125 | } 126 | } 127 | Event::Eof => break, 128 | _ => {} 129 | } 130 | 131 | buf.clear(); 132 | } 133 | 134 | Err(Error::Eof) 135 | } 136 | 137 | /// Attempt to write this Atom feed to a writer using default `WriteConfig`. 138 | /// 139 | /// # Examples 140 | /// 141 | /// ``` 142 | /// use std::io::BufReader; 143 | /// use std::fs::File; 144 | /// use atom_syndication::Feed; 145 | /// 146 | /// # fn main() -> Result<(), Box> { 147 | /// let feed = Feed { 148 | /// title: "Feed Title".into(), 149 | /// id: "Feed ID".into(), 150 | /// ..Default::default() 151 | /// }; 152 | /// 153 | /// let out = feed.write_to(Vec::new())?; 154 | /// assert_eq!(&out, br#" 155 | /// Feed TitleFeed ID1970-01-01T00:00:00+00:00"#); 156 | /// # Ok(()) } 157 | /// ``` 158 | pub fn write_to(&self, writer: W) -> Result { 159 | self.write_with_config(writer, WriteConfig::default()) 160 | } 161 | 162 | /// Attempt to write this Atom feed to a writer. 163 | /// 164 | /// # Examples 165 | /// 166 | /// ``` 167 | /// use std::io::BufReader; 168 | /// use std::fs::File; 169 | /// use atom_syndication::{Feed, WriteConfig}; 170 | /// 171 | /// # fn main() -> Result<(), Box> { 172 | /// let feed = Feed { 173 | /// title: "Feed Title".into(), 174 | /// id: "Feed ID".into(), 175 | /// ..Default::default() 176 | /// }; 177 | /// 178 | /// let mut out = Vec::new(); 179 | /// let config = WriteConfig { 180 | /// write_document_declaration: false, 181 | /// indent_size: Some(2), 182 | /// }; 183 | /// feed.write_with_config(&mut out, config)?; 184 | /// assert_eq!(&out, br#" 185 | /// Feed Title 186 | /// Feed ID 187 | /// 1970-01-01T00:00:00+00:00 188 | /// "#); 189 | /// # Ok(()) } 190 | /// ``` 191 | pub fn write_with_config( 192 | &self, 193 | writer: W, 194 | write_config: WriteConfig, 195 | ) -> Result { 196 | let mut writer = match write_config.indent_size { 197 | Some(indent_size) => Writer::new_with_indent(writer, b' ', indent_size), 198 | None => Writer::new(writer), 199 | }; 200 | if write_config.write_document_declaration { 201 | writer 202 | .write_event(Event::Decl(BytesDecl::new("1.0", None, None))) 203 | .map_err(XmlError::new)?; 204 | writer 205 | .write_event(Event::Text(BytesText::from_escaped("\n"))) 206 | .map_err(XmlError::new)?; 207 | } 208 | self.to_xml(&mut writer)?; 209 | Ok(writer.into_inner()) 210 | } 211 | 212 | /// Return the title of this feed. 213 | /// 214 | /// # Examples 215 | /// 216 | /// ``` 217 | /// use atom_syndication::Feed; 218 | /// 219 | /// let mut feed = Feed::default(); 220 | /// feed.set_title("Feed Title"); 221 | /// assert_eq!(feed.title(), "Feed Title"); 222 | /// ``` 223 | pub fn title(&self) -> &Text { 224 | &self.title 225 | } 226 | 227 | /// Set the title of this feed. 228 | /// 229 | /// # Examples 230 | /// 231 | /// ``` 232 | /// use atom_syndication::Feed; 233 | /// 234 | /// let mut feed = Feed::default(); 235 | /// feed.set_title("Feed Title"); 236 | /// ``` 237 | pub fn set_title(&mut self, title: V) 238 | where 239 | V: Into, 240 | { 241 | self.title = title.into(); 242 | } 243 | 244 | /// Return the unique URI of this feed. 245 | /// 246 | /// # Examples 247 | /// 248 | /// ``` 249 | /// use atom_syndication::Feed; 250 | /// 251 | /// let mut feed = Feed::default(); 252 | /// feed.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 253 | /// assert_eq!(feed.id(), "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 254 | /// ``` 255 | pub fn id(&self) -> &str { 256 | self.id.as_str() 257 | } 258 | 259 | /// Set the unique URI of this feed. 260 | /// 261 | /// # Examples 262 | /// 263 | /// ``` 264 | /// use atom_syndication::Feed; 265 | /// 266 | /// let mut feed = Feed::default(); 267 | /// feed.set_id("urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6"); 268 | /// ``` 269 | pub fn set_id(&mut self, id: V) 270 | where 271 | V: Into, 272 | { 273 | self.id = id.into(); 274 | } 275 | 276 | /// Return the last time that this feed was modified. 277 | /// 278 | /// # Examples 279 | /// 280 | /// ``` 281 | /// use atom_syndication::Feed; 282 | /// use atom_syndication::FixedDateTime; 283 | /// use std::str::FromStr; 284 | /// 285 | /// let mut feed = Feed::default(); 286 | /// feed.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 287 | /// assert_eq!(feed.updated().to_rfc3339(), "2017-06-03T15:15:44-05:00"); 288 | /// ``` 289 | pub fn updated(&self) -> &FixedDateTime { 290 | &self.updated 291 | } 292 | 293 | /// Set the last time that this feed was modified. 294 | /// 295 | /// # Examples 296 | /// 297 | /// ``` 298 | /// use atom_syndication::Feed; 299 | /// use atom_syndication::FixedDateTime; 300 | /// use std::str::FromStr; 301 | /// 302 | /// let mut feed = Feed::default(); 303 | /// feed.set_updated(FixedDateTime::from_str("2017-06-03T15:15:44-05:00").unwrap()); 304 | /// ``` 305 | pub fn set_updated(&mut self, updated: V) 306 | where 307 | V: Into, 308 | { 309 | self.updated = updated.into(); 310 | } 311 | 312 | /// Return the authors of this feed. 313 | /// 314 | /// # Examples 315 | /// 316 | /// ``` 317 | /// use atom_syndication::{Feed, Person}; 318 | /// 319 | /// let mut feed = Feed::default(); 320 | /// feed.set_authors(vec![Person::default()]); 321 | /// assert_eq!(feed.authors().len(), 1); 322 | /// ``` 323 | pub fn authors(&self) -> &[Person] { 324 | self.authors.as_slice() 325 | } 326 | 327 | /// Set the authors of this feed. 328 | /// 329 | /// # Examples 330 | /// 331 | /// ``` 332 | /// use atom_syndication::{Feed, Person}; 333 | /// 334 | /// let mut feed = Feed::default(); 335 | /// feed.set_authors(vec![Person::default()]); 336 | /// ``` 337 | pub fn set_authors(&mut self, authors: V) 338 | where 339 | V: Into>, 340 | { 341 | self.authors = authors.into(); 342 | } 343 | 344 | /// Return the categories this feed belongs to. 345 | /// 346 | /// # Examples 347 | /// 348 | /// ``` 349 | /// use atom_syndication::{Feed, Category}; 350 | /// 351 | /// let mut feed = Feed::default(); 352 | /// feed.set_categories(vec![Category::default()]); 353 | /// assert_eq!(feed.categories().len(), 1); 354 | /// ``` 355 | pub fn categories(&self) -> &[Category] { 356 | self.categories.as_slice() 357 | } 358 | 359 | /// Set the categories this feed belongs to. 360 | /// 361 | /// # Examples 362 | /// 363 | /// ``` 364 | /// use atom_syndication::{Feed, Category}; 365 | /// 366 | /// let mut feed = Feed::default(); 367 | /// feed.set_categories(vec![Category::default()]); 368 | /// ``` 369 | pub fn set_categories(&mut self, categories: V) 370 | where 371 | V: Into>, 372 | { 373 | self.categories = categories.into(); 374 | } 375 | 376 | /// Return the contributors to this feed. 377 | /// 378 | /// # Examples 379 | /// 380 | /// ``` 381 | /// use atom_syndication::{Feed, Person}; 382 | /// 383 | /// let mut feed = Feed::default(); 384 | /// feed.set_contributors(vec![Person::default()]); 385 | /// assert_eq!(feed.contributors().len(), 1); 386 | /// ``` 387 | pub fn contributors(&self) -> &[Person] { 388 | self.contributors.as_slice() 389 | } 390 | 391 | /// Set the contributors to this feed. 392 | /// 393 | /// # Examples 394 | /// 395 | /// ``` 396 | /// use atom_syndication::{Feed, Person}; 397 | /// 398 | /// let mut feed = Feed::default(); 399 | /// feed.set_contributors(vec![Person::default()]); 400 | /// ``` 401 | pub fn set_contributors(&mut self, contributors: V) 402 | where 403 | V: Into>, 404 | { 405 | self.contributors = contributors.into(); 406 | } 407 | 408 | /// Return the name of the software used to generate this feed. 409 | /// 410 | /// # Examples 411 | /// 412 | /// ``` 413 | /// use atom_syndication::{Feed, Generator}; 414 | /// 415 | /// let mut feed = Feed::default(); 416 | /// feed.set_generator(Generator::default()); 417 | /// assert!(feed.generator().is_some()); 418 | /// ``` 419 | pub fn generator(&self) -> Option<&Generator> { 420 | self.generator.as_ref() 421 | } 422 | 423 | /// Set the name of the software used to generate this feed. 424 | /// 425 | /// # Examples 426 | /// 427 | /// ``` 428 | /// use atom_syndication::{Feed, Generator}; 429 | /// 430 | /// let mut feed = Feed::default(); 431 | /// feed.set_generator(Generator::default()); 432 | /// ``` 433 | pub fn set_generator(&mut self, generator: V) 434 | where 435 | V: Into>, 436 | { 437 | self.generator = generator.into() 438 | } 439 | 440 | /// Return the icon for this feed. 441 | /// 442 | /// # Examples 443 | /// 444 | /// ``` 445 | /// use atom_syndication::Feed; 446 | /// 447 | /// let mut feed = Feed::default(); 448 | /// feed.set_icon("http://example.com/icon.png".to_string()); 449 | /// assert_eq!(feed.icon(), Some("http://example.com/icon.png")); 450 | /// ``` 451 | pub fn icon(&self) -> Option<&str> { 452 | self.icon.as_deref() 453 | } 454 | 455 | /// Set the icon for this feed. 456 | /// 457 | /// # Examples 458 | /// 459 | /// ``` 460 | /// use atom_syndication::Feed; 461 | /// 462 | /// let mut feed = Feed::default(); 463 | /// feed.set_icon("http://example.com/icon.png".to_string()); 464 | /// ``` 465 | pub fn set_icon(&mut self, icon: V) 466 | where 467 | V: Into>, 468 | { 469 | self.icon = icon.into() 470 | } 471 | 472 | /// Return the Web pages related to this feed. 473 | /// 474 | /// # Examples 475 | /// 476 | /// ``` 477 | /// use atom_syndication::{Feed, Link}; 478 | /// 479 | /// let mut feed = Feed::default(); 480 | /// feed.set_links(vec![Link::default()]); 481 | /// assert_eq!(feed.links().len(), 1); 482 | /// ``` 483 | pub fn links(&self) -> &[Link] { 484 | self.links.as_slice() 485 | } 486 | 487 | /// Set the Web pages related to this feed. 488 | /// 489 | /// # Examples 490 | /// 491 | /// ``` 492 | /// use atom_syndication::{Feed, Link}; 493 | /// 494 | /// let mut feed = Feed::default(); 495 | /// feed.set_links(vec![Link::default()]); 496 | /// ``` 497 | pub fn set_links(&mut self, links: V) 498 | where 499 | V: Into>, 500 | { 501 | self.links = links.into(); 502 | } 503 | 504 | /// Return the logo for this feed. 505 | /// 506 | /// # Examples 507 | /// 508 | /// ``` 509 | /// use atom_syndication::Feed; 510 | /// 511 | /// let mut feed = Feed::default(); 512 | /// feed.set_logo("http://example.com/logo.png".to_string()); 513 | /// assert_eq!(feed.logo(), Some("http://example.com/logo.png")); 514 | /// ``` 515 | pub fn logo(&self) -> Option<&str> { 516 | self.logo.as_deref() 517 | } 518 | 519 | /// Set the logo for this feed. 520 | /// 521 | /// # Examples 522 | /// 523 | /// ``` 524 | /// use atom_syndication::Feed; 525 | /// 526 | /// let mut feed = Feed::default(); 527 | /// feed.set_logo("http://example.com/logo.png".to_string()); 528 | /// ``` 529 | pub fn set_logo(&mut self, logo: V) 530 | where 531 | V: Into>, 532 | { 533 | self.logo = logo.into() 534 | } 535 | 536 | /// Return the information about the rights held in and over this feed. 537 | /// 538 | /// # Examples 539 | /// 540 | /// ``` 541 | /// use atom_syndication::{Feed, Text}; 542 | /// 543 | /// let mut feed = Feed::default(); 544 | /// feed.set_rights(Text::from("© 2017 John Doe")); 545 | /// assert_eq!(feed.rights().map(Text::as_str), Some("© 2017 John Doe")); 546 | /// ``` 547 | pub fn rights(&self) -> Option<&Text> { 548 | self.rights.as_ref() 549 | } 550 | 551 | /// Set the information about the rights held in and over this feed. 552 | /// 553 | /// # Examples 554 | /// 555 | /// ``` 556 | /// use atom_syndication::{Feed, Text}; 557 | /// 558 | /// let mut feed = Feed::default(); 559 | /// feed.set_rights(Text::from("© 2017 John Doe")); 560 | /// ``` 561 | pub fn set_rights(&mut self, rights: V) 562 | where 563 | V: Into>, 564 | { 565 | self.rights = rights.into() 566 | } 567 | 568 | /// Return the description or subtitle of this feed. 569 | /// 570 | /// # Examples 571 | /// 572 | /// ``` 573 | /// use atom_syndication::{Feed, Text}; 574 | /// 575 | /// let mut feed = Feed::default(); 576 | /// feed.set_subtitle(Text::from("Feed subtitle")); 577 | /// assert_eq!(feed.subtitle().map(Text::as_str), Some("Feed subtitle")); 578 | /// ``` 579 | pub fn subtitle(&self) -> Option<&Text> { 580 | self.subtitle.as_ref() 581 | } 582 | 583 | /// Set the description or subtitle of this feed. 584 | /// 585 | /// # Examples 586 | /// 587 | /// ``` 588 | /// use atom_syndication::{Feed, Text}; 589 | /// 590 | /// let mut feed = Feed::default(); 591 | /// feed.set_subtitle(Text::from("Feed subtitle")); 592 | /// ``` 593 | pub fn set_subtitle(&mut self, subtitle: V) 594 | where 595 | V: Into>, 596 | { 597 | self.subtitle = subtitle.into() 598 | } 599 | 600 | /// Return the entries in this feed. 601 | /// 602 | /// # Examples 603 | /// 604 | /// ``` 605 | /// use atom_syndication::{Feed, Entry}; 606 | /// 607 | /// let mut feed = Feed::default(); 608 | /// feed.set_entries(vec![Entry::default()]); 609 | /// assert_eq!(feed.entries().len(), 1); 610 | /// ``` 611 | pub fn entries(&self) -> &[Entry] { 612 | self.entries.as_slice() 613 | } 614 | 615 | /// Set the entries in this feed. 616 | /// 617 | /// # Examples 618 | /// 619 | /// ``` 620 | /// use atom_syndication::{Feed, Entry}; 621 | /// 622 | /// let mut feed = Feed::default(); 623 | /// feed.set_entries(vec![Entry::default()]); 624 | /// ``` 625 | pub fn set_entries(&mut self, entries: V) 626 | where 627 | V: Into>, 628 | { 629 | self.entries = entries.into(); 630 | } 631 | 632 | /// Return the extensions for this feed. 633 | /// 634 | /// # Examples 635 | /// 636 | /// ``` 637 | /// use std::collections::BTreeMap; 638 | /// use atom_syndication::Feed; 639 | /// use atom_syndication::extension::{ExtensionMap, Extension}; 640 | /// 641 | /// let extension = Extension::default(); 642 | /// 643 | /// let mut item_map = BTreeMap::>::new(); 644 | /// item_map.insert("ext:name".to_string(), vec![extension]); 645 | /// 646 | /// let mut extension_map = ExtensionMap::default(); 647 | /// extension_map.insert("ext".to_string(), item_map); 648 | /// 649 | /// let mut feed = Feed::default(); 650 | /// feed.set_extensions(extension_map); 651 | /// assert_eq!(feed.extensions() 652 | /// .get("ext") 653 | /// .and_then(|m| m.get("ext:name")) 654 | /// .map(|v| v.len()), 655 | /// Some(1)); 656 | /// ``` 657 | pub fn extensions(&self) -> &ExtensionMap { 658 | &self.extensions 659 | } 660 | 661 | /// Set the extensions for this feed. 662 | /// 663 | /// # Examples 664 | /// 665 | /// ``` 666 | /// use atom_syndication::Feed; 667 | /// use atom_syndication::extension::ExtensionMap; 668 | /// 669 | /// let mut feed = Feed::default(); 670 | /// feed.set_extensions(ExtensionMap::default()); 671 | /// ``` 672 | pub fn set_extensions(&mut self, extensions: V) 673 | where 674 | V: Into, 675 | { 676 | self.extensions = extensions.into() 677 | } 678 | 679 | /// Return the namespaces for this feed. 680 | /// 681 | /// # Examples 682 | /// 683 | /// ``` 684 | /// use std::collections::BTreeMap; 685 | /// use atom_syndication::Feed; 686 | /// 687 | /// let mut namespaces = BTreeMap::new(); 688 | /// namespaces.insert("ext".to_string(), "http://example.com".to_string()); 689 | /// 690 | /// let mut feed = Feed::default(); 691 | /// feed.set_namespaces(namespaces); 692 | /// assert_eq!(feed.namespaces().get("ext").map(|s| s.as_str()), Some("http://example.com")); 693 | /// ``` 694 | pub fn namespaces(&self) -> &BTreeMap { 695 | &self.namespaces 696 | } 697 | 698 | /// Set the namespaces for this feed. 699 | /// 700 | /// # Examples 701 | /// 702 | /// ``` 703 | /// use std::collections::BTreeMap; 704 | /// use atom_syndication::Feed; 705 | /// 706 | /// let mut feed = Feed::default(); 707 | /// feed.set_namespaces(BTreeMap::new()); 708 | /// ``` 709 | pub fn set_namespaces(&mut self, namespaces: V) 710 | where 711 | V: Into>, 712 | { 713 | self.namespaces = namespaces.into() 714 | } 715 | 716 | /// Return base URL of the feed. 717 | pub fn base(&self) -> Option<&str> { 718 | self.base.as_deref() 719 | } 720 | 721 | /// Set base URL of the feed. 722 | pub fn set_base(&mut self, base: V) 723 | where 724 | V: Into>, 725 | { 726 | self.base = base.into(); 727 | } 728 | 729 | /// Return natural language of the feed. 730 | pub fn lang(&self) -> Option<&str> { 731 | self.lang.as_deref() 732 | } 733 | 734 | /// Set the base URL of the feed. 735 | pub fn set_lang(&mut self, lang: V) 736 | where 737 | V: Into>, 738 | { 739 | self.lang = lang.into(); 740 | } 741 | } 742 | 743 | impl FromXml for Feed { 744 | fn from_xml( 745 | reader: &mut Reader, 746 | mut atts: Attributes<'_>, 747 | ) -> Result { 748 | let mut feed = Feed::default(); 749 | let mut buf = Vec::new(); 750 | 751 | for att in atts.with_checks(false).flatten() { 752 | match decode(att.key.as_ref(), reader)? { 753 | Cow::Borrowed("xml:base") => { 754 | feed.base = Some(attr_value(&att, reader)?.to_string()) 755 | } 756 | Cow::Borrowed("xml:lang") => { 757 | feed.lang = Some(attr_value(&att, reader)?.to_string()) 758 | } 759 | Cow::Borrowed("xmlns:dc") => {} 760 | key => { 761 | if let Some(ns) = key.strip_prefix("xmlns:") { 762 | feed.namespaces 763 | .insert(ns.to_string(), attr_value(&att, reader)?.to_string()); 764 | } 765 | } 766 | } 767 | } 768 | 769 | loop { 770 | match reader.read_event_into(&mut buf).map_err(XmlError::new)? { 771 | Event::Start(element) => match decode(element.name().as_ref(), reader)? { 772 | Cow::Borrowed("title") => { 773 | feed.title = Text::from_xml(reader, element.attributes())? 774 | } 775 | Cow::Borrowed("id") => feed.id = atom_text(reader)?.unwrap_or_default(), 776 | Cow::Borrowed("updated") => { 777 | feed.updated = atom_datetime(reader)?.unwrap_or_else(default_fixed_datetime) 778 | } 779 | Cow::Borrowed("author") => feed 780 | .authors 781 | .push(Person::from_xml(reader, element.attributes())?), 782 | Cow::Borrowed("category") => { 783 | feed.categories.push(Category::from_xml(reader, &element)?); 784 | skip(element.name(), reader)?; 785 | } 786 | Cow::Borrowed("contributor") => feed 787 | .contributors 788 | .push(Person::from_xml(reader, element.attributes())?), 789 | Cow::Borrowed("generator") => { 790 | feed.generator = Some(Generator::from_xml(reader, element.attributes())?) 791 | } 792 | Cow::Borrowed("icon") => feed.icon = atom_text(reader)?, 793 | Cow::Borrowed("link") => { 794 | feed.links.push(Link::from_xml(reader, &element)?); 795 | skip(element.name(), reader)?; 796 | } 797 | Cow::Borrowed("logo") => feed.logo = atom_text(reader)?, 798 | Cow::Borrowed("rights") => { 799 | feed.rights = Some(Text::from_xml(reader, element.attributes())?) 800 | } 801 | Cow::Borrowed("subtitle") => { 802 | feed.subtitle = Some(Text::from_xml(reader, element.attributes())?) 803 | } 804 | Cow::Borrowed("entry") => feed 805 | .entries 806 | .push(Entry::from_xml(reader, element.attributes())?), 807 | n => { 808 | if let Some((ns, name)) = extension_name(n.as_ref()) { 809 | parse_extension( 810 | reader, 811 | element.attributes(), 812 | ns, 813 | name, 814 | &mut feed.extensions, 815 | )?; 816 | } else { 817 | skip(element.name(), reader)?; 818 | } 819 | } 820 | }, 821 | Event::End(_) => break, 822 | Event::Eof => return Err(Error::Eof), 823 | _ => {} 824 | } 825 | 826 | buf.clear(); 827 | } 828 | 829 | Ok(feed) 830 | } 831 | } 832 | 833 | impl ToXml for Feed { 834 | fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { 835 | let name = "feed"; 836 | let mut element = BytesStart::new(name); 837 | element.push_attribute(("xmlns", "http://www.w3.org/2005/Atom")); 838 | 839 | for (ns, uri) in &self.namespaces { 840 | element.push_attribute((format!("xmlns:{}", ns).as_bytes(), uri.as_bytes())); 841 | } 842 | 843 | if let Some(ref base) = self.base { 844 | element.push_attribute(("xml:base", base.as_str())); 845 | } 846 | 847 | if let Some(ref lang) = self.lang { 848 | element.push_attribute(("xml:lang", lang.as_str())); 849 | } 850 | 851 | writer 852 | .write_event(Event::Start(element)) 853 | .map_err(XmlError::new)?; 854 | writer.write_object_named(&self.title, "title")?; 855 | writer.write_text_element("id", &self.id)?; 856 | writer.write_text_element("updated", &self.updated.to_rfc3339())?; 857 | writer.write_objects_named(&self.authors, "author")?; 858 | writer.write_objects(&self.categories)?; 859 | writer.write_objects_named(&self.contributors, "contributor")?; 860 | 861 | if let Some(ref generator) = self.generator { 862 | writer.write_object(generator)?; 863 | } 864 | 865 | if let Some(ref icon) = self.icon { 866 | writer.write_text_element("icon", icon)?; 867 | } 868 | 869 | writer.write_objects(&self.links)?; 870 | 871 | if let Some(ref logo) = self.logo { 872 | writer.write_text_element("logo", logo)?; 873 | } 874 | 875 | if let Some(ref rights) = self.rights { 876 | writer.write_object_named(rights, "rights")?; 877 | } 878 | 879 | if let Some(ref subtitle) = self.subtitle { 880 | writer.write_object_named(subtitle, "subtitle")?; 881 | } 882 | 883 | writer.write_objects(&self.entries)?; 884 | 885 | for map in self.extensions.values() { 886 | for extensions in map.values() { 887 | writer.write_objects(extensions)?; 888 | } 889 | } 890 | 891 | writer 892 | .write_event(Event::End(BytesEnd::new(name))) 893 | .map_err(XmlError::new)?; 894 | 895 | Ok(()) 896 | } 897 | } 898 | 899 | impl FromStr for Feed { 900 | type Err = Error; 901 | 902 | fn from_str(s: &str) -> Result { 903 | Feed::read_from(s.as_bytes()) 904 | } 905 | } 906 | 907 | impl ToString for Feed { 908 | fn to_string(&self) -> String { 909 | let buf = self.write_to(Vec::new()).unwrap_or_default(); 910 | // this unwrap should be safe since the bytes written from the Feed are all valid utf8 911 | String::from_utf8(buf).unwrap() 912 | } 913 | } 914 | 915 | impl Default for Feed { 916 | fn default() -> Self { 917 | Feed { 918 | title: Text::default(), 919 | id: String::new(), 920 | updated: default_fixed_datetime(), 921 | authors: Vec::new(), 922 | categories: Vec::new(), 923 | contributors: Vec::new(), 924 | generator: None, 925 | icon: None, 926 | links: Vec::new(), 927 | logo: None, 928 | rights: None, 929 | subtitle: None, 930 | entries: Vec::new(), 931 | extensions: ExtensionMap::default(), 932 | namespaces: BTreeMap::default(), 933 | base: None, 934 | lang: None, 935 | } 936 | } 937 | } 938 | 939 | #[cfg(feature = "builders")] 940 | impl FeedBuilder { 941 | /// Builds a new `Feed`. 942 | pub fn build(&self) -> Feed { 943 | self.build_impl().unwrap() 944 | } 945 | } 946 | 947 | #[cfg(test)] 948 | mod test { 949 | use super::*; 950 | 951 | #[test] 952 | fn test_default() { 953 | let feed = Feed::default(); 954 | let xml_fragment = r#" 955 | 1970-01-01T00:00:00+00:00"#; 956 | assert_eq!(feed.to_string(), xml_fragment); 957 | let loaded_feed = Feed::read_from(xml_fragment.as_bytes()).unwrap(); 958 | assert_eq!(loaded_feed, feed); 959 | assert_eq!(loaded_feed.base(), None); 960 | assert_eq!(loaded_feed.lang(), None); 961 | } 962 | 963 | #[test] 964 | fn test_base_and_lang() { 965 | let mut feed = Feed::default(); 966 | feed.set_base(Some("http://example.com/blog/".into())); 967 | feed.set_lang(Some("fr_FR".into())); 968 | let xml_fragment = r#" 969 | 1970-01-01T00:00:00+00:00"#; 970 | assert_eq!(feed.to_string(), xml_fragment); 971 | let loaded_feed = Feed::read_from(xml_fragment.as_bytes()).unwrap(); 972 | assert_eq!(loaded_feed, feed); 973 | assert_eq!(loaded_feed.base(), Some("http://example.com/blog/")); 974 | assert_eq!(loaded_feed.lang(), Some("fr_FR")); 975 | } 976 | 977 | #[test] 978 | fn test_write_no_decl() { 979 | let feed = Feed::default(); 980 | let xml = feed 981 | .write_with_config( 982 | Vec::new(), 983 | WriteConfig { 984 | write_document_declaration: false, 985 | indent_size: None, 986 | }, 987 | ) 988 | .unwrap(); 989 | assert_eq!( 990 | String::from_utf8_lossy(&xml), 991 | r#"1970-01-01T00:00:00+00:00"# 992 | ); 993 | } 994 | 995 | #[test] 996 | fn test_write_indented() { 997 | let feed = Feed::default(); 998 | let xml = feed 999 | .write_with_config( 1000 | Vec::new(), 1001 | WriteConfig { 1002 | write_document_declaration: true, 1003 | indent_size: Some(4), 1004 | }, 1005 | ) 1006 | .unwrap(); 1007 | assert_eq!( 1008 | String::from_utf8_lossy(&xml), 1009 | r#" 1010 | 1011 | 1012 | 1013 | 1970-01-01T00:00:00+00:00 1014 | "# 1015 | ); 1016 | } 1017 | 1018 | #[test] 1019 | fn test_write_no_decl_indented() { 1020 | let feed = Feed::default(); 1021 | let xml = feed 1022 | .write_with_config( 1023 | Vec::new(), 1024 | WriteConfig { 1025 | write_document_declaration: false, 1026 | indent_size: Some(4), 1027 | }, 1028 | ) 1029 | .unwrap(); 1030 | assert_eq!( 1031 | String::from_utf8_lossy(&xml), 1032 | r#" 1033 | 1034 | 1035 | 1970-01-01T00:00:00+00:00 1036 | "# 1037 | ); 1038 | } 1039 | } 1040 | --------------------------------------------------------------------------------