diff --git a/.travis.yml b/.travis.yml index 06b45ced20..01fdba5906 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ rust: - nightly - beta - stable +script: + - cargo test --verbose + - cargo test --verbose --features rss_loose after_success: | cargo doc && \ echo '' > target/doc/index.html && \ diff --git a/Cargo.toml b/Cargo.toml index a1757adf44..d0e3f7ad1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,4 @@ [package] - name = "rss" version = "0.3.1" authors = ["Corey Farwell "] @@ -10,6 +9,9 @@ description = "Library for serializing the RSS web content syndication format" keywords = ["rss", "feed", "blog", "web", "news"] exclude = ["test-data/"] - [dependencies] RustyXML = "0.1" + +[features] +rss_loose = [] + diff --git a/README.md b/README.md index 622d427e05..680737ed79 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,24 @@ let rss_str = r#" let rss = rss_str.parse::().unwrap(); ``` + +### Partial Feeds + +In some cases, the RSS source may not return a standards-compliant RSS such as a missing description tag. The library +is designed to return an error in such cases, however this behaviour can be loosened by using the feature +flag `rss_loose`. + +Using this flag changes what would normally be a `String` type to a `Option`, just like other fields. + +In your `Cargo.toml`, add the following: +```toml +[dependencies.rss] +features = ["rss_loose"] +``` + +## Contributors & License + +- Michael Yoo [GitHub](https://github.com/sekjun9878) [Web](https://www.michael.yoo.id.au/) + +Released under The Apache License 2.0 + diff --git a/src/category.rs b/src/category.rs index 35242de96c..895bd62e63 100644 --- a/src/category.rs +++ b/src/category.rs @@ -45,3 +45,4 @@ impl ViaXml for Category { }) } } + diff --git a/src/channel.rs b/src/channel.rs index 83960be907..0ebcf1a52a 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -23,8 +23,10 @@ use ::{Category, ElementUtils, Item, Image, ReadError, TextInput, ViaXml}; /// # Examples /// /// ``` +///# #![feature(stmt_expr_attributes)] /// use rss::Channel; /// +///# #[cfg(not(feature = "rss_loose"))] /// let channel = Channel { /// title: String::from("My Blog"), /// link: String::from("http://myblog.com"), @@ -32,12 +34,31 @@ use ::{Category, ElementUtils, Item, Image, ReadError, TextInput, ViaXml}; /// items: vec![], /// ..Default::default() /// }; +///# #[cfg(feature = "rss_loose")] +///# let channel = Channel { +///# title: Some(String::from("My Blog")), +///# link: Some(String::from("http://myblog.com")), +///# description: Some(String::from("My thoughts on life, the universe, and everything")), +///# items: vec![], +///# ..Default::default() +///# }; /// ``` #[derive(Default, Debug, Clone)] pub struct Channel { + #[cfg(not(feature = "rss_loose"))] pub title: String, + #[cfg(not(feature = "rss_loose"))] pub link: String, + #[cfg(not(feature = "rss_loose"))] pub description: String, + + #[cfg(feature = "rss_loose")] + pub title: Option, + #[cfg(feature = "rss_loose")] + pub link: Option, + #[cfg(feature = "rss_loose")] + pub description: Option, + pub items: Vec, pub language: Option, pub copyright: Option, @@ -61,10 +82,20 @@ impl ViaXml for Channel { fn to_xml(&self) -> Element { let mut channel = Element::new("channel".to_owned(), None, vec![]); + #[cfg(not(feature = "rss_loose"))] channel.tag_with_text("title", self.title.clone()); + #[cfg(not(feature = "rss_loose"))] channel.tag_with_text("link", self.link.clone()); + #[cfg(not(feature = "rss_loose"))] channel.tag_with_text("description", self.description.clone()); + #[cfg(feature = "rss_loose")] + channel.tag_with_optional_text("title", self.title.clone()); + #[cfg(feature = "rss_loose")] + channel.tag_with_optional_text("link", self.link.clone()); + #[cfg(feature = "rss_loose")] + channel.tag_with_optional_text("description", self.description.clone()); + for item in &self.items { channel.tag(item.to_xml()); } @@ -95,21 +126,29 @@ impl ViaXml for Channel { } fn from_xml(elem: Element) -> Result { + #[cfg(not(feature = "rss_loose"))] let title = match elem.get_child("title", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ChannelMissingTitle), }; - + #[cfg(not(feature = "rss_loose"))] let link = match elem.get_child("link", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ChannelMissingLink), }; - + #[cfg(not(feature = "rss_loose"))] let description = match elem.get_child("description", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ChannelMissingDescription), }; + #[cfg(feature = "rss_loose")] + let title = elem.get_child("title", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let link = elem.get_child("link", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let description = elem.get_child("description", None).map(Element::content_str); + let items = match elem.get_children("item", None) .map(|e| ViaXml::from_xml(e.clone())) .collect::, _>>() diff --git a/src/image.rs b/src/image.rs index f784b59e63..1eedbfbd39 100644 --- a/src/image.rs +++ b/src/image.rs @@ -8,9 +8,20 @@ use ::{ElementUtils, ReadError, ViaXml}; /// (http://cyber.law.harvard.edu/rss/rss.html#ltimagegtSubelementOfLtchannelgt) #[derive(Default, Debug, Clone)] pub struct Image { + #[cfg(not(feature = "rss_loose"))] pub url: String, + #[cfg(not(feature = "rss_loose"))] pub title: String, + #[cfg(not(feature = "rss_loose"))] pub link: String, + + #[cfg(feature = "rss_loose")] + pub url: Option, + #[cfg(feature = "rss_loose")] + pub title: Option, + #[cfg(feature = "rss_loose")] + pub link: Option, + pub width: Option, pub height: Option, } @@ -18,9 +29,21 @@ pub struct Image { impl ViaXml for Image { fn to_xml(&self) -> Element { let mut elem = Element::new("image".to_owned(), None, vec![]); + + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("url", self.url.clone()); + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("title", self.title.clone()); + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("link", self.link.clone()); + + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("url", self.url.clone()); + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("title", self.title.clone()); + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("link", self.link.clone()); + if let Some(ref n) = self.width { elem.tag_with_text("width", n.to_string()); } @@ -31,21 +54,31 @@ impl ViaXml for Image { } fn from_xml(elem: Element) -> Result { + #[cfg(not(feature = "rss_loose"))] let url = match elem.get_child("url", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ImageMissingUrl), }; + #[cfg(not(feature = "rss_loose"))] let title = match elem.get_child("title", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ImageMissingTitle), }; + #[cfg(not(feature = "rss_loose"))] let link = match elem.get_child("link", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::ImageMissingLink), }; + #[cfg(feature = "rss_loose")] + let url = elem.get_child("url", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let title = elem.get_child("title", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let link = elem.get_child("link", None).map(Element::content_str); + let height = match elem.get_child("height", None) .map(|h| u32::from_str(&h.content_str())) { diff --git a/src/lib.rs b/src/lib.rs index 9b2f83af23..cd6db83258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ //! ## Writing //! //! ``` +//!# #![feature(stmt_expr_attributes)] //! use rss::{Channel, Item, Rss}; //! //! let item = Item { @@ -28,6 +29,7 @@ //! ..Default::default() //! }; //! +//!# #[cfg(not(feature = "rss_loose"))] //! let channel = Channel { //! title: String::from("TechCrunch"), //! link: String::from("http://techcrunch.com"), @@ -35,6 +37,15 @@ //! items: vec![item], //! ..Default::default() //! }; +//!# +//!# #[cfg(feature = "rss_loose")] +//!# let channel = Channel { +//!# title: Some(String::from("TechCrunch")), +//!# link: Some(String::from("http://techcrunch.com")), +//!# description: Some(String::from("The latest technology news and information on startups")), +//!# items: vec![item], +//!# ..Default::default() +//!# }; //! //! let rss = Rss(channel); //! @@ -64,6 +75,22 @@ //! //! let rss = rss_str.parse::().unwrap(); //! ``` +//! +//! ### Partial Feeds +//! +//! In some cases, the RSS source may not return a standards-compliant RSS such as a missing description tag. The library +//! is designed to return an error in such cases, however this behaviour can be loosened by using the feature +//! flag `rss_loose`. +//! +//! Using this flag changes what would normally be a `String` type to a `Option`, just like other fields. +//! +//! In your `Cargo.toml`, add the following: +//! ```toml +//! [dependencies.rss] +//! features = ["rss_loose"] +//! ``` + +#![feature(stmt_expr_attributes)] mod category; mod guid; @@ -246,6 +273,7 @@ mod test { ..Default::default() }; + #[cfg(not(feature = "rss_loose"))] let channel = Channel { title: "My Blog".to_owned(), link: "http://myblog.com".to_owned(), @@ -254,6 +282,15 @@ mod test { ..Default::default() }; + #[cfg(feature = "rss_loose")] + let channel = Channel { + title: Some("My Blog".to_owned()), + link: Some("http://myblog.com".to_owned()), + description: Some("Where I write stuff".to_owned()), + items: vec![item], + ..Default::default() + }; + let rss = Rss(channel); assert_eq!(rss.to_string(), "My Bloghttp://myblog.comWhere I write stuffMy first post!http://myblog.com/post1This is my first post"); } @@ -274,6 +311,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_one_channel_no_properties() { let rss_str = "\ \ @@ -294,7 +332,11 @@ mod test { \ "; let Rss(channel) = Rss::from_str(rss_str).unwrap(); + + #[cfg(not(feature = "rss_loose"))] assert_eq!("Hello world!", channel.title); + #[cfg(feature = "rss_loose")] + assert_eq!(Some("Hello world!".to_owned()), channel.title); // How come &str is dereferenced but Some(&str) is not? } #[test] @@ -340,7 +382,11 @@ mod test { \ "; let Rss(channel) = Rss::from_str(rss_str).unwrap(); + + #[cfg(not(feature = "rss_loose"))] assert_eq!("Foobar", channel.text_input.unwrap().title); + #[cfg(feature = "rss_loose")] + assert_eq!(Some("Foobar".to_owned()), channel.text_input.unwrap().title); } // Ensure reader ignores the PI XML node and continues to parse the RSS @@ -356,7 +402,11 @@ mod test { \ "; let Rss(channel) = Rss::from_str(rss_str).unwrap(); + + #[cfg(not(feature = "rss_loose"))] assert_eq!("Title", channel.title); + #[cfg(feature = "rss_loose")] + assert_eq!(Some("Title".to_owned()), channel.title); } #[test] @@ -379,14 +429,27 @@ mod test { "; let rss = Rss::from_str(rss_str).unwrap(); let image = rss.0.image.unwrap(); - assert_eq!(image.url, "a url"); - assert_eq!(image.title, "a title"); - assert_eq!(image.link, "a link"); + + #[cfg(not(feature = "rss_loose"))] + assert_eq!(image.url, "a url".to_owned()); + #[cfg(not(feature = "rss_loose"))] + assert_eq!(image.title, "a title".to_owned()); + #[cfg(not(feature = "rss_loose"))] + assert_eq!(image.link, "a link".to_owned()); + + #[cfg(feature = "rss_loose")] + assert_eq!(image.url, Some("a url".to_owned())); + #[cfg(feature = "rss_loose")] + assert_eq!(image.title, Some("a title".to_owned())); + #[cfg(feature = "rss_loose")] + assert_eq!(image.link, Some("a link".to_owned())); + assert_eq!(image.height, Some(140)); assert_eq!(image.width, Some(280)); } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_image_no_url() { let rss_str = "\ \ @@ -405,6 +468,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_image_no_title() { let rss_str = "\ \ @@ -423,6 +487,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_image_no_link() { let rss_str = "\ \ diff --git a/src/text_input.rs b/src/text_input.rs index 69a6884a3f..d582dd4be0 100644 --- a/src/text_input.rs +++ b/src/text_input.rs @@ -21,43 +21,84 @@ use ::{ElementUtils, ReadError, ViaXml}; /// (http://cyber.law.harvard.edu/rss/rss.html#lttextinputgtSubelementOfLtchannelgt) #[derive(Debug, Clone)] pub struct TextInput { + #[cfg(not(feature = "rss_loose"))] pub title: String, + #[cfg(not(feature = "rss_loose"))] pub description: String, + #[cfg(not(feature = "rss_loose"))] pub name: String, + #[cfg(not(feature = "rss_loose"))] pub link: String, + + #[cfg(feature = "rss_loose")] + pub title: Option, + #[cfg(feature = "rss_loose")] + pub description: Option, + #[cfg(feature = "rss_loose")] + pub name: Option, + #[cfg(feature = "rss_loose")] + pub link: Option, } impl ViaXml for TextInput { fn to_xml(&self) -> Element { let mut elem = Element::new("textInput".to_owned(), None, vec![]); + + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("title", self.title.clone()); + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("description", self.description.clone()); + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("name", self.name.clone()); + #[cfg(not(feature = "rss_loose"))] elem.tag_with_text("link", self.link.clone()); + + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("title", self.title.clone()); + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("description", self.description.clone()); + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("name", self.name.clone()); + #[cfg(feature = "rss_loose")] + elem.tag_with_optional_text("link", self.link.clone()); + elem } fn from_xml(elem: Element) -> Result { + #[cfg(not(feature = "rss_loose"))] let title = match elem.get_child("title", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::TextInputMissingTitle), }; + #[cfg(not(feature = "rss_loose"))] let description = match elem.get_child("description", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::TextInputMissingDescription), }; + #[cfg(not(feature = "rss_loose"))] let name = match elem.get_child("name", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::TextInputMissingName), }; + #[cfg(not(feature = "rss_loose"))] let link = match elem.get_child("link", None) { Some(elem) => elem.content_str(), None => return Err(ReadError::TextInputMissingLink), }; + #[cfg(feature = "rss_loose")] + let title = elem.get_child("title", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let description = elem.get_child("description", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let name = elem.get_child("name", None).map(Element::content_str); + #[cfg(feature = "rss_loose")] + let link = elem.get_child("link", None).map(Element::content_str); + Ok(TextInput { title: title, description: description,