diff --git a/Cargo.toml b/Cargo.toml index f6c79f4fd2..e0cb2624c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,10 @@ all-features = false [features] default = ["builders"] -builders = ["derive_builder"] +builders = ["derive_builder", "atom_syndication/builders"] validation = ["chrono", "url", "mime"] +with-serde = ["serde", "atom_syndication/with-serde"] +atom = ["atom_syndication"] [dependencies] quick-xml = { version = "0.20", features = ["encoding"] } @@ -26,6 +28,7 @@ chrono = {version = "0.4", optional = true } url = { version = "2.1", optional = true } mime = { version = "0.3", optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } +atom_syndication = { version = "0.9", optional = true } [dev-dependencies] bencher = "0.1" diff --git a/src/channel.rs b/src/channel.rs index 0861507782..705dfd3788 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -18,6 +18,8 @@ use quick_xml::Writer; use crate::category::Category; use crate::cloud::Cloud; use crate::error::Error; +#[cfg(feature = "atom")] +use crate::extension::atom; use crate::extension::dublincore; use crate::extension::itunes; use crate::extension::syndication; @@ -77,6 +79,9 @@ pub struct Channel { pub items: Vec, /// The extensions for the channel. pub extensions: ExtensionMap, + /// The Atom extension for the channel. + #[cfg(feature = "atom")] + pub atom_ext: Option, /// The iTunes extension for the channel. pub itunes_ext: Option, /// The Dublin Core extension for the channel. @@ -779,6 +784,42 @@ impl Channel { self.items = items.into(); } + /// Return the Atom extension for this channel. + /// + /// # Examples + /// + /// ``` + /// use rss::Channel; + /// use rss::extension::atom::AtomExtension; + /// + /// let mut channel = Channel::default(); + /// channel.set_atom_ext(AtomExtension::default()); + /// assert!(channel.atom_ext().is_some()); + /// ``` + #[cfg(feature = "atom")] + pub fn atom_ext(&self) -> Option<&atom::AtomExtension> { + self.atom_ext.as_ref() + } + + /// Set the Atom extension for this channel. + /// + /// # Examples + /// + /// ``` + /// use rss::Channel; + /// use rss::extension::atom::AtomExtension; + /// + /// let mut channel = Channel::default(); + /// channel.set_atom_ext(AtomExtension::default()); + /// ``` + #[cfg(feature = "atom")] + pub fn set_atom_ext(&mut self, atom_ext: V) + where + V: Into>, + { + self.atom_ext = atom_ext.into(); + } + /// Return the iTunes extension for this channel. /// /// # Examples @@ -1255,6 +1296,11 @@ impl Channel { // Process each of the namespaces we know (note that the values are not removed prior and reused to support pass-through of unknown extensions) for (prefix, namespace) in namespaces { match namespace.as_ref() { + #[cfg(feature = "atom")] + atom::NAMESPACE => channel + .extensions + .remove(prefix) + .map(|v| channel.atom_ext = Some(atom::AtomExtension::from_map(v))), itunes::NAMESPACE => channel.extensions.remove(prefix).map(|v| { channel.itunes_ext = Some(itunes::ITunesChannelExtension::from_map(v)) }), @@ -1364,6 +1410,11 @@ impl ToXml for Channel { } } + #[cfg(feature = "atom")] + if let Some(ext) = &self.atom_ext { + ext.to_xml(writer)?; + } + if let Some(ext) = &self.itunes_ext { ext.to_xml(writer)?; } diff --git a/src/extension/atom.rs b/src/extension/atom.rs new file mode 100644 index 0000000000..d19885ee80 --- /dev/null +++ b/src/extension/atom.rs @@ -0,0 +1,104 @@ +// This file is part of rss. +// +// Copyright © 2015-2020 The rust-syndication Developers +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the MIT License and/or Apache 2.0 License. + +use std::collections::HashMap; +use std::io::Write; + +pub use atom_syndication::Link; +use quick_xml::events::{BytesStart, Event}; +use quick_xml::Error as XmlError; +use quick_xml::Writer; + +use crate::extension::Extension; +use crate::toxml::ToXml; + +/// The Atom XML namespace. +pub const NAMESPACE: &str = "http://www.w3.org/2005/Atom"; + +/// An Atom element extension. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "builders", derive(Builder))] +#[cfg_attr(feature = "builders", builder(setter(into), default))] +pub struct AtomExtension { + /// Links + pub links: Vec, +} + +impl AtomExtension { + /// Retrieve links + pub fn links(&self) -> &[Link] { + &self.links + } + + /// Set links + pub fn set_links(&mut self, links: V) + where + V: Into>, + { + self.links = links.into(); + } +} + +impl AtomExtension { + /// Creates an `AtomExtension` using the specified `HashMap`. + pub fn from_map(mut map: HashMap>) -> Self { + let mut ext = Self::default(); + + ext.links = map + .remove("link") + .unwrap_or_default() + .into_iter() + .filter_map(|mut link_ext| { + let href = link_ext.attrs.remove("href")?; + + let mut link = Link::default(); + link.href = href; + if let Some(rel) = link_ext.attrs.remove("rel") { + link.rel = rel; + } + link.hreflang = link_ext.attrs.remove("hreflang"); + link.mime_type = link_ext.attrs.remove("type"); + link.title = link_ext.attrs.remove("title"); + link.length = link_ext.attrs.remove("length"); + Some(link) + }) + .collect(); + + ext + } +} + +impl ToXml for AtomExtension { + fn to_xml(&self, writer: &mut Writer) -> Result<(), XmlError> { + for link in &self.links { + let name = b"link"; + let mut element = BytesStart::borrowed(name, name.len()); + element.push_attribute(("href", &*link.href)); + element.push_attribute(("rel", &*link.rel)); + + if let Some(ref hreflang) = link.hreflang { + element.push_attribute(("hreflang", &**hreflang)); + } + + if let Some(ref mime_type) = link.mime_type { + element.push_attribute(("type", &**mime_type)); + } + + if let Some(ref title) = link.title { + element.push_attribute(("title", &**title)); + } + + if let Some(ref length) = link.length { + element.push_attribute(("length", &**length)); + } + + writer.write_event(Event::Empty(element))?; + } + Ok(()) + } +} diff --git a/src/extension/mod.rs b/src/extension/mod.rs index 403ab71e76..4f0c8d35d5 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -15,6 +15,10 @@ use quick_xml::Writer; use crate::toxml::ToXml; +/// Types and methods for [Atom](https://www.rssboard.org/rss-profile#namespace-elements-atom) extensions. +#[cfg(feature = "atom")] +pub mod atom; + /// Types and methods for /// [iTunes](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) extensions. pub mod itunes; diff --git a/src/item.rs b/src/item.rs index e53b04c99b..344ebbc271 100644 --- a/src/item.rs +++ b/src/item.rs @@ -16,6 +16,8 @@ use quick_xml::Writer; use crate::category::Category; use crate::enclosure::Enclosure; use crate::error::Error; +#[cfg(feature = "atom")] +use crate::extension::atom; use crate::extension::dublincore; use crate::extension::itunes; use crate::extension::util::{extension_name, parse_extension}; @@ -56,6 +58,9 @@ pub struct Item { pub content: Option, /// The extensions for the item. pub extensions: ExtensionMap, + /// The Atom extension for the channel. + #[cfg(feature = "atom")] + pub atom_ext: Option, /// The iTunes extension for the item. pub itunes_ext: Option, /// The Dublin Core extension for the item. @@ -438,6 +443,42 @@ impl Item { self.content = content.into(); } + /// Return the Atom extension for this item. + /// + /// # Examples + /// + /// ``` + /// use rss::Item; + /// use rss::extension::atom::AtomExtension; + /// + /// let mut item = Item::default(); + /// item.set_atom_ext(AtomExtension::default()); + /// assert!(item.atom_ext().is_some()); + /// ``` + #[cfg(feature = "atom")] + pub fn atom_ext(&self) -> Option<&atom::AtomExtension> { + self.atom_ext.as_ref() + } + + /// Set the Atom extension for this item. + /// + /// # Examples + /// + /// ``` + /// use rss::Item; + /// use rss::extension::atom::AtomExtension; + /// + /// let mut item = Item::default(); + /// item.set_atom_ext(AtomExtension::default()); + /// ``` + #[cfg(feature = "atom")] + pub fn set_atom_ext(&mut self, atom_ext: V) + where + V: Into>, + { + self.atom_ext = atom_ext.into(); + } + /// Return the iTunes extension for this item. /// /// # Examples @@ -615,6 +656,11 @@ impl Item { // Process each of the namespaces we know (note that the values are not removed prior and reused to support pass-through of unknown extensions) for (prefix, namespace) in namespaces { match namespace.as_ref() { + #[cfg(feature = "atom")] + atom::NAMESPACE => item + .extensions + .remove(prefix) + .map(|v| item.atom_ext = Some(atom::AtomExtension::from_map(v))), itunes::NAMESPACE => item .extensions .remove(prefix) @@ -687,6 +733,11 @@ impl ToXml for Item { } } + #[cfg(feature = "atom")] + if let Some(ext) = self.atom_ext.as_ref() { + ext.to_xml(writer)?; + } + if let Some(ext) = self.itunes_ext.as_ref() { ext.to_xml(writer)?; } diff --git a/tests/data/rss2_with_atom.xml b/tests/data/rss2_with_atom.xml new file mode 100644 index 0000000000..ec7c56cac8 --- /dev/null +++ b/tests/data/rss2_with_atom.xml @@ -0,0 +1,38 @@ + + + + Liftoff News + http://liftoff.msfc.nasa.gov/ + Liftoff to Space Exploration. + en-us + Tue, 10 Jun 2003 04:00:00 GMT + Tue, 10 Jun 2003 09:41:01 GMT + http://blogs.law.harvard.edu/tech/rss + Weblog Editor 2.0 + editor@example.com + webmaster@example.com + + + Star City + http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp + + How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>. + Tue, 03 Jun 2003 09:39:21 GMT + http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 + + + + Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st. + Fri, 30 May 2003 11:06:42 GMT + http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 + + + The Engine That Does More + http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp + + Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that. + Tue, 27 May 2003 08:37:32 GMT + http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 + + + diff --git a/tests/read.rs b/tests/read.rs index a1e3609380..b0f6ded3ef 100644 --- a/tests/read.rs +++ b/tests/read.rs @@ -538,6 +538,48 @@ fn read_extension() { ); } +#[cfg(feature = "atom")] +#[test] +fn read_atom() { + let input = include_str!("data/rss2_with_atom.xml"); + let channel = input.parse::().expect("failed to parse xml"); + + assert_eq!( + channel.atom_ext().unwrap().links(), + &[rss::extension::atom::Link { + href: "http://liftoff.msfc.nasa.gov/rss".into(), + rel: "self".into(), + mime_type: Some("application/rss+xml".into()), + ..Default::default() + },] + ); + + assert_eq!( + channel.items[0].atom_ext().unwrap().links(), + &[ + rss::extension::atom::Link { + href: "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp".into(), + ..Default::default() + }, + rss::extension::atom::Link { + href: "http://liftoff.msfc.nasa.gov/2003/05/30.html#item572".into(), + rel: "related".into(), + ..Default::default() + }, + ] + ); + + assert!(channel.items[1].atom_ext().is_none()); + + assert_eq!( + channel.items[2].atom_ext().unwrap().links(), + &[rss::extension::atom::Link { + href: "http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp".into(), + ..Default::default() + }] + ); +} + #[test] fn read_itunes() { let input = include_str!("data/itunes.xml");