Skip to content

Return partial RSS feeds, rss_loose feature, resolves #22 #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ rust:
- nightly
- beta
- stable
script:
- cargo test --verbose
- cargo test --verbose --features rss_loose
after_success: |
cargo doc && \
echo '<meta http-equiv=refresh content=0;url=rss/index.html>' > target/doc/index.html && \
Expand Down
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[package]

name = "rss"
version = "0.3.1"
authors = ["Corey Farwell <[email protected]>"]
Expand All @@ -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 = []

21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,24 @@ let rss_str = r#"

let rss = rss_str.parse::<Rss>().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<String>`, 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

1 change: 1 addition & 0 deletions src/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ impl ViaXml for Category {
})
}
}

43 changes: 41 additions & 2 deletions src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,42 @@ 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"),
/// description: String::from("My thoughts on life, the universe, and everything"),
/// 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<String>,
#[cfg(feature = "rss_loose")]
pub link: Option<String>,
#[cfg(feature = "rss_loose")]
pub description: Option<String>,

pub items: Vec<Item>,
pub language: Option<String>,
pub copyright: Option<String>,
Expand All @@ -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());
}
Expand Down Expand Up @@ -95,21 +126,29 @@ impl ViaXml for Channel {
}

fn from_xml(elem: Element) -> Result<Self, ReadError> {
#[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::<Result<Vec<_>, _>>()
Expand Down
33 changes: 33 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,42 @@ 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<String>,
#[cfg(feature = "rss_loose")]
pub title: Option<String>,
#[cfg(feature = "rss_loose")]
pub link: Option<String>,

pub width: Option<u32>,
pub height: Option<u32>,
}

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());
}
Expand All @@ -31,21 +54,31 @@ impl ViaXml for Image {
}

fn from_xml(elem: Element) -> Result<Self, ReadError> {
#[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()))
{
Expand Down
71 changes: 68 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
//! ## Writing
//!
//! ```
//!# #![feature(stmt_expr_attributes)]
//! use rss::{Channel, Item, Rss};
//!
//! let item = Item {
Expand All @@ -28,13 +29,23 @@
//! ..Default::default()
//! };
//!
//!# #[cfg(not(feature = "rss_loose"))]
//! let channel = Channel {
//! title: String::from("TechCrunch"),
//! link: String::from("http://techcrunch.com"),
//! description: String::from("The latest technology news and information on startups"),
//! 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);
//!
Expand Down Expand Up @@ -64,6 +75,22 @@
//!
//! let rss = rss_str.parse::<Rss>().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<String>`, 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;
Expand Down Expand Up @@ -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(),
Expand All @@ -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(), "<?xml version=\'1.0\' encoding=\'UTF-8\'?><rss version=\'2.0\'><channel><title>My Blog</title><link>http://myblog.com</link><description>Where I write stuff</description><item><title>My first post!</title><link>http://myblog.com/post1</link><description>This is my first post</description></item></channel></rss>");
}
Expand All @@ -274,6 +311,7 @@ mod test {
}

#[test]
#[cfg_attr(feature = "rss_loose", ignore)]
fn test_read_one_channel_no_properties() {
let rss_str = "\
<rss>\
Expand All @@ -294,7 +332,11 @@ mod test {
</channel>\
</rss>";
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]
Expand Down Expand Up @@ -340,7 +382,11 @@ mod test {
</channel>\
</rss>";
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
Expand All @@ -356,7 +402,11 @@ mod test {
</channel>\
</rss>";
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]
Expand All @@ -379,14 +429,27 @@ mod test {
</rss>";
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 = "\
<?xml version=\'1.0\' encoding=\'UTF-8\'?>\
Expand All @@ -405,6 +468,7 @@ mod test {
}

#[test]
#[cfg_attr(feature = "rss_loose", ignore)]
fn test_read_image_no_title() {
let rss_str = "\
<?xml version=\'1.0\' encoding=\'UTF-8\'?>\
Expand All @@ -423,6 +487,7 @@ mod test {
}

#[test]
#[cfg_attr(feature = "rss_loose", ignore)]
fn test_read_image_no_link() {
let rss_str = "\
<?xml version=\'1.0\' encoding=\'UTF-8\'?>\
Expand Down
Loading