├── 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 |
4 |
--------------------------------------------------------------------------------
/tests/data/text_plain.xml:
--------------------------------------------------------------------------------
1 |
2 | Feed Title
3 |
4 |
--------------------------------------------------------------------------------
/tests/data/text_cdata.xml:
--------------------------------------------------------------------------------
1 |
2 | Feed Title
]]>
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 |
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>Feed Title]]>
3 |
4 |
--------------------------------------------------------------------------------
/tests/data/link.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tests/data/text_xhtml_escaped.xml:
--------------------------------------------------------------------------------
1 |
2 | <p>Feed Title</p>
3 |
4 |
--------------------------------------------------------------------------------
/tests/data/content_text_cdata_escaped.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/data/text_xhtml.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tests/data/content_text_xhtml.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | [](https://github.com/rust-syndication/atom/actions)
4 | [](https://crates.io/crates/atom_syndication)
5 | [](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#""#
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#"Feed Title
"#
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 | ""
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 ".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 |
--------------------------------------------------------------------------------