From 20e1e470b5d4e05d11ed1b22363d93308a923a0f Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Fri, 8 Jul 2016 20:45:39 +0930 Subject: [PATCH 1/6] Preliminary work on type looseness flag --- Cargo.toml | 6 ++++-- src/category.rs | 1 + src/channel.rs | 33 +++++++++++++++++++++++++++++++-- src/image.rs | 23 +++++++++++++++++++++++ src/lib.rs | 2 ++ src/text_input.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 4 deletions(-) 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/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..21c0459ce1 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -35,9 +35,20 @@ use ::{Category, ElementUtils, Item, Image, ReadError, TextInput, ViaXml}; /// ``` #[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 +72,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 +116,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..f39dd2e703 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()); } diff --git a/src/lib.rs b/src/lib.rs index 9b2f83af23..1edd968df2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,8 @@ //! let rss = rss_str.parse::().unwrap(); //! ``` +#![feature(stmt_expr_attributes)] + mod category; mod guid; mod channel; 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, From 72fcf496ff6d856f0352c106828c23f31a8373c6 Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Sun, 10 Jul 2016 23:23:34 +0930 Subject: [PATCH 2/6] Modify tests to account for the type changes, update image.rs --- src/image.rs | 10 ++++++++++ src/lib.rs | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/image.rs b/src/image.rs index f39dd2e703..1eedbfbd39 100644 --- a/src/image.rs +++ b/src/image.rs @@ -54,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 1edd968df2..181ef55f69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,6 +248,7 @@ mod test { ..Default::default() }; + #[cfg(not(feature = "rss_loose"))] let channel = Channel { title: "My Blog".to_owned(), link: "http://myblog.com".to_owned(), @@ -256,6 +257,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"); } @@ -276,6 +286,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_one_channel_no_properties() { let rss_str = "\ \ @@ -296,7 +307,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] @@ -342,7 +357,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 @@ -358,7 +377,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] @@ -381,14 +404,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 = "\ \ @@ -407,6 +443,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_image_no_title() { let rss_str = "\ \ @@ -425,6 +462,7 @@ mod test { } #[test] + #[cfg_attr(feature = "rss_loose", ignore)] fn test_read_image_no_link() { let rss_str = "\ \ From f0c90ca41d5b0d702457a3f2f551e066294fe194 Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Sun, 10 Jul 2016 23:28:03 +0930 Subject: [PATCH 3/6] Add rss_loose feature to Travis configuration --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) 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 && \ From 64170439b3db217d23e20e789f996886773536c0 Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Mon, 11 Jul 2016 00:16:53 +0930 Subject: [PATCH 4/6] Fix doctests to account for conditional compilation --- src/channel.rs | 10 ++++++++++ src/lib.rs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/channel.rs b/src/channel.rs index 21c0459ce1..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,6 +34,14 @@ 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 { diff --git a/src/lib.rs b/src/lib.rs index 181ef55f69..1d54889cfd 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); //! From d5be8e5ec7dbc450d9f00307d611b40c4133d929 Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Mon, 11 Jul 2016 00:29:16 +0930 Subject: [PATCH 5/6] Add contributors section --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 622d427e05..0719ca1895 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,10 @@ let rss_str = r#" let rss = rss_str.parse::().unwrap(); ``` + +## Contributors & License + +- Michael Yoo [GitHub](https://github.com/sekjun9878) [Web](https://www.michael.yoo.id.au/) + +Released under The Apache License 2.0 + From 1c188fac25a6d59a334c2be9e83bfdd18937788a Mon Sep 17 00:00:00 2001 From: Michael Yoo Date: Mon, 11 Jul 2016 00:38:10 +0930 Subject: [PATCH 6/6] Add documentation about feature flag rss_loose --- README.md | 14 ++++++++++++++ src/lib.rs | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 0719ca1895..680737ed79 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ 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/) diff --git a/src/lib.rs b/src/lib.rs index 1d54889cfd..cd6db83258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,20 @@ //! //! 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)]