From 3680f0ba72e7399dafbc79c57b5c671560ec9f62 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sat, 14 May 2016 23:59:57 -0400 Subject: [PATCH 01/16] Add struct definition for common feed format --- Cargo.toml | 3 ++- src/lib.rs | 57 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad54455..cb5dc09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "syndication" -version = "0.2.0" +version = "0.3.0" authors = ["Tom Shen "] license = "MIT OR Apache-2.0" repository = "https://github.com/tomshen/rust-syndication" @@ -11,3 +11,4 @@ exclude = ["test-data/*"] [dependencies] atom_syndication = "0.1" rss = "0.3" +chrono = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 3e31958..cc0b185 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,32 +1,65 @@ extern crate atom_syndication; extern crate rss; +extern crate chrono; use std::str::FromStr; +use chrono::{DateTime, UTC}; + +pub struct Entry { } +pub struct Category { } +pub struct Feed { + // If created from an RSS or Atom feed, this is the original contents + source_data: Option, + // `id` in Atom, not present in RSS + pub id: Option, + // `title` in both Atom and RSS + pub title: String, + // `subtitle` in Atom, and `description` in RSS (required) + pub description: Option, + // `updated` in Atom (required), and `pub_date` or `last_build_date` in RSS + // TODO: Document which RSS field is preferred + // This field is required in Atom, but optional in RSS + pub updated: Option>, + // `rights` in Atom, and `copyright` in RSS + pub copyright: Option, + // `icon` in Atom, and `image` in Atom + pub icon: Option, + // `links` in Atom, and `link` in RSS (will produce a Vec of 1 item) + pub links: Vec, + // `categories` in both Atom and RSS + pub categories: Vec, + // `authors` in Atom, not present in RSS (RSS will produce an empty Vec) + pub authors: Vec, + // `contributors` in Atom, not present in RSS (produces an empty Vec) + pub contributors: Vec, + // `entries` in Atom, and `items` in RSS + pub entries: Vec, +} -pub enum Feed { +enum FeedData { Atom(atom_syndication::Feed), RSS(rss::Channel), } -impl FromStr for Feed { +impl FromStr for FeedData { type Err = &'static str; fn from_str(s: &str) -> Result { match s.parse::() { - Ok (feed) => Ok (Feed::Atom(feed)), + Ok (feed) => Ok (FeedData::Atom(feed)), _ => match s.parse::() { - Ok (rss::Rss(channel)) => Ok (Feed::RSS(channel)), + Ok (rss::Rss(channel)) => Ok (FeedData::RSS(channel)), _ => Err ("Could not parse XML as Atom or RSS from input") } } } } -impl ToString for Feed { +impl ToString for FeedData { fn to_string(&self) -> String { match self { - &Feed::Atom(ref atom_feed) => atom_feed.to_string(), - &Feed::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), + &FeedData::Atom(ref atom_feed) => atom_feed.to_string(), + &FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), } } } @@ -40,7 +73,7 @@ mod test { use std::io::Read; use std::str::FromStr; - use super::Feed; + use super::FeedData; // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs #[test] @@ -48,7 +81,7 @@ mod test { let mut file = File::open("test-data/atom.xml").unwrap(); let mut atom_string = String::new(); file.read_to_string(&mut atom_string).unwrap(); - let feed = Feed::from_str(&atom_string).unwrap(); + let feed = FeedData::from_str(&atom_string).unwrap(); assert!(feed.to_string().len() > 0); } @@ -58,7 +91,7 @@ mod test { let mut file = File::open("test-data/rss.xml").unwrap(); let mut rss_string = String::new(); file.read_to_string(&mut rss_string).unwrap(); - let rss = Feed::from_str(&rss_string).unwrap(); + let rss = FeedData::from_str(&rss_string).unwrap(); assert!(rss.to_string().len() > 0); } @@ -76,7 +109,7 @@ mod test { ..Default::default() }; - let feed = Feed::Atom(atom_syndication::Feed { + let feed = FeedData::Atom(atom_syndication::Feed { title: "My Blog".to_string(), authors: vec![author], entries: vec![entry], @@ -104,7 +137,7 @@ mod test { ..Default::default() }; - let rss = Feed::RSS(channel); + let rss = FeedData::RSS(channel); assert_eq!(rss.to_string(), "My Bloghttp://myblog.comWhere I write stuffMy first post!http://myblog.com/post1This is my first post"); } } From 9bd1a24fb2fdfdb481a2cffe045216ab596798f8 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sun, 15 May 2016 00:26:56 -0400 Subject: [PATCH 02/16] Add partial support for converting from/to Atom --- src/lib.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index cc0b185..96ce7b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,13 +22,19 @@ pub struct Feed { pub updated: Option>, // `rights` in Atom, and `copyright` in RSS pub copyright: Option, - // `icon` in Atom, and `image` in Atom + // `icon` in Atom, pub icon: Option, + // NOTE: Throwing away the `image` field in Atom + // `generator` in both Atom and RSS + // TODO: Add a Generator type so this can be implemented + // pub generator: Option, // `links` in Atom, and `link` in RSS (will produce a Vec of 1 item) + // TODO: Change this to a Link type instead of just a String pub links: Vec, // `categories` in both Atom and RSS pub categories: Vec, // `authors` in Atom, not present in RSS (RSS will produce an empty Vec) + // TODO: Define our own Person type for API stability reasons pub authors: Vec, // `contributors` in Atom, not present in RSS (produces an empty Vec) pub contributors: Vec, @@ -36,6 +42,63 @@ pub struct Feed { pub entries: Vec, } +impl From for Feed { + fn from(feed: atom_syndication::Feed) -> Self { + Feed { + // TODO: We can't move the feed, because we need its contents... + source_data: None, // Some(FeedData::Atom(feed)), + id: Some(feed.id), + title: feed.title, + description: feed.subtitle, + updated: feed.updated.parse::>().ok(), + copyright: feed.rights, + icon: feed.icon, + // NOTE: Throwing away the `image` field + // NOTE: We throw away the generator field + // TODO: Define a Link type + links: feed.links.into_iter().map(|link| link.href).collect::>(), + // TODO: Handle this once the Category type is defined + categories: vec![], + authors: feed.authors, + contributors: feed.contributors, + // TODO: Handle this once the Entry type is defined + entries: vec![], + } + } +} + +impl From for atom_syndication::Feed { + fn from(feed: Feed) -> Self { + if let Some(FeedData::Atom(feed)) = feed.source_data { + feed + } else { + atom_syndication::Feed { + // TODO: Producing an empty string is probably very very bad + // is there anything better that can be done...? + id: feed.id.unwrap_or(String::from("")), + title: feed.title, + subtitle: feed.description, + // TODO: Is there a better way to handle a missing date here? + updated: feed.updated.unwrap_or(UTC::now()).to_rfc3339(), + rights: feed.copyright, + icon: feed.icon.clone(), + logo: feed.icon, + generator: None, + links: feed.links.into_iter() + .map(|href| atom_syndication::Link { + href: href, ..Default::default() + }).collect::>(), + // TODO: Convert from our Category type instead of throwing them away + categories: vec![], + authors: feed.authors, + contributors: feed.contributors, + // TODO: Convert from our Entry type instead of throwing them away + entries: vec![], + } + } + } +} + enum FeedData { Atom(atom_syndication::Feed), RSS(rss::Channel), From 430b25b9178eef532a234bbd2d53a3a523924e0f Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sun, 15 May 2016 00:57:24 -0400 Subject: [PATCH 03/16] Add the Entry type --- src/lib.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 96ce7b3..afef4c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,58 @@ extern crate chrono; use std::str::FromStr; use chrono::{DateTime, UTC}; -pub struct Entry { } +enum EntryData { + Atom(atom_syndication::Feed), + RSS(rss::Item), +} + pub struct Category { } +pub struct Link { href: String } +pub struct Person { } + +pub struct Entry { + // If created from an Atom or RSS entry, this is the original contents + source_data: Option, + + // `id` in Atom (required), and `guid` in RSS + pub id: Option, + // `title` in Atom and RSS, optional only in RSS + pub title: Option, + // `updated` in Atom (required), not present in RSS + pub updated: DateTime, + // `published` in Atom, and `pub_date` in RSS + pub published: Option>, + // `summary` in Atom, and `description` in RSS + pub summary: Option, + // `content` in Atom, not present in RSS + pub content: Option, + + // TODO: Figure out the `source` field in the Atom Entry type (It refers to + // the atom Feed type, which owns the Entry, is it a copy of the Feed with + // no entries?) How do we include this? + + // `links` in Atom, and `link` in RSS (produces a Vec with 0 or 1 items) + pub links: Vec, + // `categories` in both Atom and RSS + pub categories: Vec, + // `authors` in Atom, `author` in RSS (produces a Vec with 0 or 1 items) + // TODO: Define our own Person type for API stability reasons + pub authors: Vec, + // `contributors` in Atom, not present in RSS (produces an empty Vec) + pub contributors: Vec, + + // TODO: What is the RSS `comments` field used for? +} + +enum FeedData { + Atom(atom_syndication::Feed), + RSS(rss::Channel), +} + pub struct Feed { // If created from an RSS or Atom feed, this is the original contents source_data: Option, + // `id` in Atom, not present in RSS pub id: Option, // `title` in both Atom and RSS @@ -24,17 +71,18 @@ pub struct Feed { pub copyright: Option, // `icon` in Atom, pub icon: Option, + // NOTE: Throwing away the `image` field in Atom // `generator` in both Atom and RSS // TODO: Add a Generator type so this can be implemented // pub generator: Option, + // `links` in Atom, and `link` in RSS (will produce a Vec of 1 item) - // TODO: Change this to a Link type instead of just a String - pub links: Vec, + pub links: Vec, // `categories` in both Atom and RSS pub categories: Vec, - // `authors` in Atom, not present in RSS (RSS will produce an empty Vec) // TODO: Define our own Person type for API stability reasons + // `authors` in Atom, not present in RSS (RSS will produce an empty Vec) pub authors: Vec, // `contributors` in Atom, not present in RSS (produces an empty Vec) pub contributors: Vec, @@ -53,10 +101,12 @@ impl From for Feed { updated: feed.updated.parse::>().ok(), copyright: feed.rights, icon: feed.icon, - // NOTE: Throwing away the `image` field + // NOTE: We throw away the `image` field // NOTE: We throw away the generator field - // TODO: Define a Link type - links: feed.links.into_iter().map(|link| link.href).collect::>(), + // TODO: Add more fields to the link type + links: feed.links.into_iter() + .map(|link| Link { href: link.href }) + .collect::>(), // TODO: Handle this once the Category type is defined categories: vec![], authors: feed.authors, @@ -69,6 +119,7 @@ impl From for Feed { impl From for atom_syndication::Feed { fn from(feed: Feed) -> Self { + // Performing no translation at all is both faster, and won't lose any data! if let Some(FeedData::Atom(feed)) = feed.source_data { feed } else { @@ -85,8 +136,8 @@ impl From for atom_syndication::Feed { logo: feed.icon, generator: None, links: feed.links.into_iter() - .map(|href| atom_syndication::Link { - href: href, ..Default::default() + .map(|link| atom_syndication::Link { + href: link.href, ..Default::default() }).collect::>(), // TODO: Convert from our Category type instead of throwing them away categories: vec![], @@ -99,10 +150,6 @@ impl From for atom_syndication::Feed { } } -enum FeedData { - Atom(atom_syndication::Feed), - RSS(rss::Channel), -} impl FromStr for FeedData { type Err = &'static str; From d61ea413db8d26db75b05d24cff17cbd0081509b Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sun, 15 May 2016 01:15:42 -0400 Subject: [PATCH 04/16] Add conversions between our Entry and Atom's Entry Use the Entry conversions in the Atom Feed conversions --- src/lib.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index afef4c9..e16d8d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use chrono::{DateTime, UTC}; enum EntryData { - Atom(atom_syndication::Feed), + Atom(atom_syndication::Entry), RSS(rss::Item), } @@ -48,6 +48,57 @@ pub struct Entry { // TODO: What is the RSS `comments` field used for? } +impl From for Entry { + fn from(entry: atom_syndication::Entry) -> Self { + Entry { + // TODO: We can't move the entry, because we need it's contents + // and none of the atom_syndication types support .clone() ... + source_data: None, + id: Some(entry.id), + title: Some(entry.title), + updated: entry.updated.parse::>().unwrap_or(UTC::now()), + published: entry.published.and_then(|date| date.parse::>().ok()), + summary: entry.summary, + content: entry.content, + links: entry.links.into_iter() + .map(|link| Link { href: link.href }) + .collect::>(), + // TODO: Implement the Category type for converting this + categories: vec![], + authors: entry.authors, + contributors: entry.contributors, + } + } +} + +impl From for atom_syndication::Entry { + fn from(entry: Entry) -> Self { + if let Some(EntryData::Atom(entry)) = entry.source_data { + entry + } else { + atom_syndication::Entry { + // TODO: How should we handle a missing id? + id: entry.id.unwrap_or(String::from("")), + // TODO: How should we handle a missing title? + title: entry.title.unwrap_or(String::from("")), + updated: entry.updated.to_rfc3339(), + published: entry.published.map(|date| date.to_rfc3339()), + source: None, + summary: entry.summary, + content: entry.content, + links: entry.links.into_iter() + .map(|link| atom_syndication::Link { + href: link.href, ..Default::default() + }).collect::>(), + // TODO: Convert from the category type + categories: vec![], + authors: entry.authors, + contributors: entry.contributors, + } + } + } +} + enum FeedData { Atom(atom_syndication::Feed), RSS(rss::Channel), @@ -93,8 +144,9 @@ pub struct Feed { impl From for Feed { fn from(feed: atom_syndication::Feed) -> Self { Feed { - // TODO: We can't move the feed, because we need its contents... - source_data: None, // Some(FeedData::Atom(feed)), + // TODO: We can't move the feed, because we need its contents + // and none of the atom_syndication types support .clone() ... + source_data: None, // Some(FeedData::Atom(feed.clone())), id: Some(feed.id), title: feed.title, description: feed.subtitle, @@ -111,8 +163,8 @@ impl From for Feed { categories: vec![], authors: feed.authors, contributors: feed.contributors, - // TODO: Handle this once the Entry type is defined - entries: vec![], + entries: feed.entries.into_iter().map(|entry| entry.into()) + .collect::>(), } } } @@ -143,8 +195,8 @@ impl From for atom_syndication::Feed { categories: vec![], authors: feed.authors, contributors: feed.contributors, - // TODO: Convert from our Entry type instead of throwing them away - entries: vec![], + entries: feed.entries.into_iter().map(|entry| entry.into()) + .collect::>(), } } } From 541e8982079ebeae047b6be0468c7aa240fb8197 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sun, 15 May 2016 01:58:26 -0400 Subject: [PATCH 05/16] Fix date parsing to specifically parse RFC 3339 --- src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e16d8d3..8071930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,11 @@ impl From for Entry { source_data: None, id: Some(entry.id), title: Some(entry.title), - updated: entry.updated.parse::>().unwrap_or(UTC::now()), - published: entry.published.and_then(|date| date.parse::>().ok()), + updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) + .map(|date| date.with_timezone(&UTC)).unwrap_or(UTC::now()), + published: entry.published + .and_then(|d| DateTime::parse_from_rfc3339(d.as_str()).ok()) + .map(|date| date.with_timezone(&UTC)), summary: entry.summary, content: entry.content, links: entry.links.into_iter() @@ -150,7 +153,8 @@ impl From for Feed { id: Some(feed.id), title: feed.title, description: feed.subtitle, - updated: feed.updated.parse::>().ok(), + updated: DateTime::parse_from_rfc3339(feed.updated.as_str()).ok() + .map(|date| date.with_timezone(&UTC)), copyright: feed.rights, icon: feed.icon, // NOTE: We throw away the `image` field From aa946de45d93c7c3f801888d4353f96a2177a672 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Sun, 15 May 2016 01:59:07 -0400 Subject: [PATCH 06/16] Image field and comment fixes --- src/lib.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8071930..ea5b563 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,9 +26,9 @@ pub struct Entry { pub updated: DateTime, // `published` in Atom, and `pub_date` in RSS pub published: Option>, - // `summary` in Atom, and `description` in RSS + // `summary` in Atom pub summary: Option, - // `content` in Atom, not present in RSS + // `content` in Atom, `description` in RSS pub content: Option, // TODO: Figure out the `source` field in the Atom Entry type (It refers to @@ -44,8 +44,6 @@ pub struct Entry { pub authors: Vec, // `contributors` in Atom, not present in RSS (produces an empty Vec) pub contributors: Vec, - - // TODO: What is the RSS `comments` field used for? } impl From for Entry { @@ -82,7 +80,6 @@ impl From for atom_syndication::Entry { atom_syndication::Entry { // TODO: How should we handle a missing id? id: entry.id.unwrap_or(String::from("")), - // TODO: How should we handle a missing title? title: entry.title.unwrap_or(String::from("")), updated: entry.updated.to_rfc3339(), published: entry.published.map(|date| date.to_rfc3339()), @@ -107,6 +104,8 @@ enum FeedData { RSS(rss::Channel), } +// A helpful table of approximately equivalent elements can be found here: +// http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#head-018c297098e131956bf394c0f7c8b6dd60f5cf78 pub struct Feed { // If created from an RSS or Atom feed, this is the original contents source_data: Option, @@ -123,25 +122,30 @@ pub struct Feed { pub updated: Option>, // `rights` in Atom, and `copyright` in RSS pub copyright: Option, - // `icon` in Atom, + // `icon` in Atom, not present in RSS pub icon: Option, + // `logo` in Atom, and `image` in RSS + pub image: Option, - // NOTE: Throwing away the `image` field in Atom // `generator` in both Atom and RSS // TODO: Add a Generator type so this can be implemented // pub generator: Option, - // `links` in Atom, and `link` in RSS (will produce a Vec of 1 item) + // `links` in Atom, and `link` in RSS (produces a 1 item Vec) pub links: Vec, // `categories` in both Atom and RSS pub categories: Vec, // TODO: Define our own Person type for API stability reasons - // `authors` in Atom, not present in RSS (RSS will produce an empty Vec) + // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? + // `authors` in Atom, `managing_editor` in RSS (produces 1 item Vec) pub authors: Vec, - // `contributors` in Atom, not present in RSS (produces an empty Vec) + // `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec) pub contributors: Vec, // `entries` in Atom, and `items` in RSS pub entries: Vec, + + // TODO: Add more fields that are necessary for RSS + // TODO: Fancy translation, e.g. Atom = RSS `source`, etc } impl From for Feed { @@ -157,7 +161,7 @@ impl From for Feed { .map(|date| date.with_timezone(&UTC)), copyright: feed.rights, icon: feed.icon, - // NOTE: We throw away the `image` field + image: feed.logo, // NOTE: We throw away the generator field // TODO: Add more fields to the link type links: feed.links.into_iter() @@ -188,8 +192,8 @@ impl From for atom_syndication::Feed { // TODO: Is there a better way to handle a missing date here? updated: feed.updated.unwrap_or(UTC::now()).to_rfc3339(), rights: feed.copyright, - icon: feed.icon.clone(), - logo: feed.icon, + icon: feed.icon, + logo: feed.image, generator: None, links: feed.links.into_iter() .map(|link| atom_syndication::Link { From b0afc7f0ef85bd1e5747e94ba545116ad078a668 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Mon, 16 May 2016 17:22:39 -0400 Subject: [PATCH 07/16] Keep a clone of the original data in the Feed --- Cargo.toml | 2 +- src/lib.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb5dc09..961f873 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,6 @@ keywords = ["atom", "blog", "feed", "rss", "syndication"] exclude = ["test-data/*"] [dependencies] -atom_syndication = "0.1" +atom_syndication = "^0.1.4" rss = "0.3" chrono = "0.2" diff --git a/src/lib.rs b/src/lib.rs index ea5b563..91af7ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,9 +49,7 @@ pub struct Entry { impl From for Entry { fn from(entry: atom_syndication::Entry) -> Self { Entry { - // TODO: We can't move the entry, because we need it's contents - // and none of the atom_syndication types support .clone() ... - source_data: None, + source_data: Some(EntryData::Atom(entry.clone())), id: Some(entry.id), title: Some(entry.title), updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) @@ -151,9 +149,7 @@ pub struct Feed { impl From for Feed { fn from(feed: atom_syndication::Feed) -> Self { Feed { - // TODO: We can't move the feed, because we need its contents - // and none of the atom_syndication types support .clone() ... - source_data: None, // Some(FeedData::Atom(feed.clone())), + source_data: Some(FeedData::Atom(feed.clone())), id: Some(feed.id), title: feed.title, description: feed.subtitle, From ebccc9742865d04c691e5d61987d0f3d82983ae2 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Mon, 16 May 2016 17:24:18 -0400 Subject: [PATCH 08/16] Add FromStr for Feed --- examples/read.rs | 11 ++--------- src/lib.rs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/read.rs b/examples/read.rs index a1f13c8..e47f1a2 100644 --- a/examples/read.rs +++ b/examples/read.rs @@ -17,10 +17,7 @@ fn main() { "#; - match atom_str.parse::().unwrap() { - Feed::Atom(atom_feed) => println!("Atom feed first entry: {:?}", atom_feed.entries[0].title), - _ => {} - }; + println!("Atom feed first entry: {:?}", atom_str.parse::().unwrap().entries[0].title); let rss_str = r#" @@ -38,9 +35,5 @@ fn main() { "#; - match rss_str.parse::().unwrap() { - Feed::RSS(rss_feed) => println!("RSS feed first entry: {:?}", - rss_feed.items[0].title), - _ => {} - }; + println!("RSS feed first entry: {:?}", rss_str.parse::().unwrap().entries[0].title); } diff --git a/src/lib.rs b/src/lib.rs index 91af7ef..0cbd19c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -206,6 +206,19 @@ impl From for atom_syndication::Feed { } } +impl FromStr for Feed { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(FeedData::Atom(feed)) => Ok(feed.into()), + // TODO: Implement the RSS conversions + Ok(FeedData::RSS(_)) => Err("RSS Unimplemented"), + Err(e) => Err(e), + } + } +} + impl FromStr for FeedData { type Err = &'static str; From a5e112a8fc673486b61782d197a423aa122c0258 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Tue, 17 May 2016 01:06:43 -0400 Subject: [PATCH 09/16] Split the library into modules --- src/category.rs | 1 + src/entry.rs | 97 ++++++++++++++ src/feed.rs | 250 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 329 ++---------------------------------------------- src/link.rs | 3 + src/person.rs | 1 + 6 files changed, 363 insertions(+), 318 deletions(-) create mode 100644 src/category.rs create mode 100644 src/entry.rs create mode 100644 src/feed.rs create mode 100644 src/link.rs create mode 100644 src/person.rs diff --git a/src/category.rs b/src/category.rs new file mode 100644 index 0000000..2f08401 --- /dev/null +++ b/src/category.rs @@ -0,0 +1 @@ +pub struct Category { } diff --git a/src/entry.rs b/src/entry.rs new file mode 100644 index 0000000..4ec8742 --- /dev/null +++ b/src/entry.rs @@ -0,0 +1,97 @@ +extern crate atom_syndication; +extern crate rss; +extern crate chrono; + +use chrono::{DateTime, UTC}; +use category::Category; +use link::Link; + +enum EntryData { + Atom(atom_syndication::Entry), + RSS(rss::Item), +} + +pub struct Entry { + // If created from an Atom or RSS entry, this is the original contents + source_data: Option, + + // `id` in Atom (required), and `guid` in RSS + pub id: Option, + // `title` in Atom and RSS, optional only in RSS + pub title: Option, + // `updated` in Atom (required), not present in RSS + pub updated: DateTime, + // `published` in Atom, and `pub_date` in RSS + pub published: Option>, + // `summary` in Atom + pub summary: Option, + // `content` in Atom, `description` in RSS + pub content: Option, + + // TODO: Figure out the `source` field in the Atom Entry type (It refers to + // the atom Feed type, which owns the Entry, is it a copy of the Feed with + // no entries?) How do we include this? + // + // `links` in Atom, and `link` in RSS (produces a Vec with 0 or 1 items) + pub links: Vec, + // `categories` in both Atom and RSS + pub categories: Vec, + // `authors` in Atom, `author` in RSS (produces a Vec with 0 or 1 items) + // TODO: Define our own Person type for API stability reasons + pub authors: Vec, + // `contributors` in Atom, not present in RSS (produces an empty Vec) + pub contributors: Vec, +} + +impl From for Entry { + fn from(entry: atom_syndication::Entry) -> Self { + Entry { + source_data: Some(EntryData::Atom(entry.clone())), + id: Some(entry.id), + title: Some(entry.title), + updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) + .map(|date| date.with_timezone(&UTC)) + .unwrap_or(UTC::now()), + published: entry.published + .and_then(|d| DateTime::parse_from_rfc3339(d.as_str()).ok()) + .map(|date| date.with_timezone(&UTC)), + summary: entry.summary, + content: entry.content, + links: entry.links + .into_iter() + .map(|link| Link { href: link.href }) + .collect::>(), + // TODO: Implement the Category type for converting this + categories: vec![], + authors: entry.authors, + contributors: entry.contributors, + } + } +} + +impl From for atom_syndication::Entry { + fn from(entry: Entry) -> Self { + if let Some(EntryData::Atom(entry)) = entry.source_data { + entry + } else { + atom_syndication::Entry { + // TODO: How should we handle a missing id? + id: entry.id.unwrap_or(String::from("")), + title: entry.title.unwrap_or(String::from("")), + updated: entry.updated.to_rfc3339(), + published: entry.published.map(|date| date.to_rfc3339()), + source: None, + summary: entry.summary, + content: entry.content, + links: entry.links + .into_iter() + .map(|link| atom_syndication::Link { href: link.href, ..Default::default() }) + .collect::>(), + // TODO: Convert from the category type + categories: vec![], + authors: entry.authors, + contributors: entry.contributors, + } + } + } +} diff --git a/src/feed.rs b/src/feed.rs new file mode 100644 index 0000000..290943c --- /dev/null +++ b/src/feed.rs @@ -0,0 +1,250 @@ +extern crate atom_syndication; +extern crate rss; +extern crate chrono; + +use std::str::FromStr; +use chrono::{DateTime, UTC}; + +use category::Category; +use link::Link; +use entry::Entry; + +enum FeedData { + Atom(atom_syndication::Feed), + RSS(rss::Channel), +} + +// A helpful table of approximately equivalent elements can be found here: +// http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#head-018c297098e131956bf394c0f7c8b6dd60f5cf78 +pub struct Feed { + // If created from an RSS or Atom feed, this is the original contents + source_data: Option, + + // `id` in Atom, not present in RSS + pub id: Option, + // `title` in both Atom and RSS + pub title: String, + // `subtitle` in Atom, and `description` in RSS (required) + pub description: Option, + // `updated` in Atom (required), and `pub_date` or `last_build_date` in RSS + // TODO: Document which RSS field is preferred + // This field is required in Atom, but optional in RSS + pub updated: Option>, + // `rights` in Atom, and `copyright` in RSS + pub copyright: Option, + // `icon` in Atom, not present in RSS + pub icon: Option, + // `logo` in Atom, and `image` in RSS + pub image: Option, + + // `generator` in both Atom and RSS + // TODO: Add a Generator type so this can be implemented + // pub generator: Option, + // + // `links` in Atom, and `link` in RSS (produces a 1 item Vec) + pub links: Vec, + // `categories` in both Atom and RSS + pub categories: Vec, + // TODO: Define our own Person type for API stability reasons + // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? + // `authors` in Atom, `managing_editor` in RSS (produces 1 item Vec) + pub authors: Vec, + // `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec) + pub contributors: Vec, + // `entries` in Atom, and `items` in RSS + // TODO: Add more fields that are necessary for RSS + // TODO: Fancy translation, e.g. Atom = RSS `source` + pub entries: Vec, +} + +impl From for Feed { + fn from(feed: atom_syndication::Feed) -> Self { + Feed { + source_data: Some(FeedData::Atom(feed.clone())), + id: Some(feed.id), + title: feed.title, + description: feed.subtitle, + updated: DateTime::parse_from_rfc3339(feed.updated.as_str()) + .ok() + .map(|date| date.with_timezone(&UTC)), + copyright: feed.rights, + icon: feed.icon, + image: feed.logo, + // NOTE: We throw away the generator field + // TODO: Add more fields to the link type + links: feed.links + .into_iter() + .map(|link| Link { href: link.href }) + .collect::>(), + // TODO: Handle this once the Category type is defined + categories: vec![], + authors: feed.authors, + contributors: feed.contributors, + entries: feed.entries + .into_iter() + .map(|entry| entry.into()) + .collect::>(), + } + } +} + +impl From for atom_syndication::Feed { + fn from(feed: Feed) -> Self { + // Performing no translation at all is both faster, and won't lose any data! + if let Some(FeedData::Atom(feed)) = feed.source_data { + feed + } else { + atom_syndication::Feed { + // TODO: Producing an empty string is probably very very bad + // is there anything better that can be done...? + id: feed.id.unwrap_or(String::from("")), + title: feed.title, + subtitle: feed.description, + // TODO: Is there a better way to handle a missing date here? + updated: feed.updated.unwrap_or(UTC::now()).to_rfc3339(), + rights: feed.copyright, + icon: feed.icon, + logo: feed.image, + generator: None, + links: feed.links + .into_iter() + .map(|link| atom_syndication::Link { href: link.href, ..Default::default() }) + .collect::>(), + // TODO: Convert from our Category type instead of throwing them away + categories: vec![], + authors: feed.authors, + contributors: feed.contributors, + entries: feed.entries + .into_iter() + .map(|entry| entry.into()) + .collect::>(), + } + } + } +} + +impl FromStr for Feed { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(FeedData::Atom(feed)) => Ok(feed.into()), + // TODO: Implement the RSS conversions + Ok(FeedData::RSS(_)) => Err("RSS Unimplemented"), + Err(e) => Err(e), + } + } +} + +impl FromStr for FeedData { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.parse::() { + Ok(feed) => Ok(FeedData::Atom(feed)), + _ => { + match s.parse::() { + Ok(rss::Rss(channel)) => Ok(FeedData::RSS(channel)), + _ => Err("Could not parse XML as Atom or RSS from input"), + } + } + } + } +} + +impl ToString for FeedData { + fn to_string(&self) -> String { + match self { + &FeedData::Atom(ref atom_feed) => atom_feed.to_string(), + &FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), + } + } +} + +#[cfg(test)] +mod test { + extern crate atom_syndication; + extern crate rss; + + use std::fs::File; + use std::io::Read; + use std::str::FromStr; + + use feed::FeedData; + + // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs + #[test] + fn test_from_atom_file() { + let mut file = File::open("test-data/atom.xml").unwrap(); + let mut atom_string = String::new(); + file.read_to_string(&mut atom_string).unwrap(); + let feed = FeedData::from_str(&atom_string).unwrap(); + assert!(feed.to_string().len() > 0); + } + + // Source: https://github.com/frewsxcv/rust-rss/blob/master/src/lib.rs + #[test] + fn test_from_rss_file() { + let mut file = File::open("test-data/rss.xml").unwrap(); + let mut rss_string = String::new(); + file.read_to_string(&mut rss_string).unwrap(); + let rss = FeedData::from_str(&rss_string).unwrap(); + assert!(rss.to_string().len() > 0); + } + + // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs + #[test] + fn test_atom_to_string() { + let author = + atom_syndication::Person { name: "N. Blogger".to_string(), ..Default::default() }; + + let entry = atom_syndication::Entry { + title: "My first post!".to_string(), + content: Some("This is my first post".to_string()), + ..Default::default() + }; + + let feed = FeedData::Atom(atom_syndication::Feed { + title: "My Blog".to_string(), + authors: vec![author], + entries: vec![entry], + ..Default::default() + }); + + assert_eq!(feed.to_string(), + "My \ + BlogN. \ + BloggerMy first \ + post!This is my first \ + post"); + } + + // Source: https://github.com/frewsxcv/rust-rss/blob/master/src/lib.rs + #[test] + fn test_rss_to_string() { + let item = rss::Item { + title: Some("My first post!".to_string()), + link: Some("http://myblog.com/post1".to_string()), + description: Some("This is my first post".to_string()), + ..Default::default() + }; + + let channel = rss::Channel { + title: "My Blog".to_string(), + link: "http://myblog.com".to_string(), + description: "Where I write stuff".to_string(), + items: vec![item], + ..Default::default() + }; + + let rss = FeedData::RSS(channel); + assert_eq!(rss.to_string(), + "My \ + Bloghttp://myblog.comWhere I write \ + stuffMy first \ + post!http://myblog.com/post1This is my \ + first post"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0cbd19c..0367cd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,321 +2,14 @@ extern crate atom_syndication; extern crate rss; extern crate chrono; -use std::str::FromStr; -use chrono::{DateTime, UTC}; - -enum EntryData { - Atom(atom_syndication::Entry), - RSS(rss::Item), -} - -pub struct Category { } -pub struct Link { href: String } -pub struct Person { } - -pub struct Entry { - // If created from an Atom or RSS entry, this is the original contents - source_data: Option, - - // `id` in Atom (required), and `guid` in RSS - pub id: Option, - // `title` in Atom and RSS, optional only in RSS - pub title: Option, - // `updated` in Atom (required), not present in RSS - pub updated: DateTime, - // `published` in Atom, and `pub_date` in RSS - pub published: Option>, - // `summary` in Atom - pub summary: Option, - // `content` in Atom, `description` in RSS - pub content: Option, - - // TODO: Figure out the `source` field in the Atom Entry type (It refers to - // the atom Feed type, which owns the Entry, is it a copy of the Feed with - // no entries?) How do we include this? - - // `links` in Atom, and `link` in RSS (produces a Vec with 0 or 1 items) - pub links: Vec, - // `categories` in both Atom and RSS - pub categories: Vec, - // `authors` in Atom, `author` in RSS (produces a Vec with 0 or 1 items) - // TODO: Define our own Person type for API stability reasons - pub authors: Vec, - // `contributors` in Atom, not present in RSS (produces an empty Vec) - pub contributors: Vec, -} - -impl From for Entry { - fn from(entry: atom_syndication::Entry) -> Self { - Entry { - source_data: Some(EntryData::Atom(entry.clone())), - id: Some(entry.id), - title: Some(entry.title), - updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) - .map(|date| date.with_timezone(&UTC)).unwrap_or(UTC::now()), - published: entry.published - .and_then(|d| DateTime::parse_from_rfc3339(d.as_str()).ok()) - .map(|date| date.with_timezone(&UTC)), - summary: entry.summary, - content: entry.content, - links: entry.links.into_iter() - .map(|link| Link { href: link.href }) - .collect::>(), - // TODO: Implement the Category type for converting this - categories: vec![], - authors: entry.authors, - contributors: entry.contributors, - } - } -} - -impl From for atom_syndication::Entry { - fn from(entry: Entry) -> Self { - if let Some(EntryData::Atom(entry)) = entry.source_data { - entry - } else { - atom_syndication::Entry { - // TODO: How should we handle a missing id? - id: entry.id.unwrap_or(String::from("")), - title: entry.title.unwrap_or(String::from("")), - updated: entry.updated.to_rfc3339(), - published: entry.published.map(|date| date.to_rfc3339()), - source: None, - summary: entry.summary, - content: entry.content, - links: entry.links.into_iter() - .map(|link| atom_syndication::Link { - href: link.href, ..Default::default() - }).collect::>(), - // TODO: Convert from the category type - categories: vec![], - authors: entry.authors, - contributors: entry.contributors, - } - } - } -} - -enum FeedData { - Atom(atom_syndication::Feed), - RSS(rss::Channel), -} - -// A helpful table of approximately equivalent elements can be found here: -// http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#head-018c297098e131956bf394c0f7c8b6dd60f5cf78 -pub struct Feed { - // If created from an RSS or Atom feed, this is the original contents - source_data: Option, - - // `id` in Atom, not present in RSS - pub id: Option, - // `title` in both Atom and RSS - pub title: String, - // `subtitle` in Atom, and `description` in RSS (required) - pub description: Option, - // `updated` in Atom (required), and `pub_date` or `last_build_date` in RSS - // TODO: Document which RSS field is preferred - // This field is required in Atom, but optional in RSS - pub updated: Option>, - // `rights` in Atom, and `copyright` in RSS - pub copyright: Option, - // `icon` in Atom, not present in RSS - pub icon: Option, - // `logo` in Atom, and `image` in RSS - pub image: Option, - - // `generator` in both Atom and RSS - // TODO: Add a Generator type so this can be implemented - // pub generator: Option, - - // `links` in Atom, and `link` in RSS (produces a 1 item Vec) - pub links: Vec, - // `categories` in both Atom and RSS - pub categories: Vec, - // TODO: Define our own Person type for API stability reasons - // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? - // `authors` in Atom, `managing_editor` in RSS (produces 1 item Vec) - pub authors: Vec, - // `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec) - pub contributors: Vec, - // `entries` in Atom, and `items` in RSS - pub entries: Vec, - - // TODO: Add more fields that are necessary for RSS - // TODO: Fancy translation, e.g. Atom = RSS `source`, etc -} - -impl From for Feed { - fn from(feed: atom_syndication::Feed) -> Self { - Feed { - source_data: Some(FeedData::Atom(feed.clone())), - id: Some(feed.id), - title: feed.title, - description: feed.subtitle, - updated: DateTime::parse_from_rfc3339(feed.updated.as_str()).ok() - .map(|date| date.with_timezone(&UTC)), - copyright: feed.rights, - icon: feed.icon, - image: feed.logo, - // NOTE: We throw away the generator field - // TODO: Add more fields to the link type - links: feed.links.into_iter() - .map(|link| Link { href: link.href }) - .collect::>(), - // TODO: Handle this once the Category type is defined - categories: vec![], - authors: feed.authors, - contributors: feed.contributors, - entries: feed.entries.into_iter().map(|entry| entry.into()) - .collect::>(), - } - } -} - -impl From for atom_syndication::Feed { - fn from(feed: Feed) -> Self { - // Performing no translation at all is both faster, and won't lose any data! - if let Some(FeedData::Atom(feed)) = feed.source_data { - feed - } else { - atom_syndication::Feed { - // TODO: Producing an empty string is probably very very bad - // is there anything better that can be done...? - id: feed.id.unwrap_or(String::from("")), - title: feed.title, - subtitle: feed.description, - // TODO: Is there a better way to handle a missing date here? - updated: feed.updated.unwrap_or(UTC::now()).to_rfc3339(), - rights: feed.copyright, - icon: feed.icon, - logo: feed.image, - generator: None, - links: feed.links.into_iter() - .map(|link| atom_syndication::Link { - href: link.href, ..Default::default() - }).collect::>(), - // TODO: Convert from our Category type instead of throwing them away - categories: vec![], - authors: feed.authors, - contributors: feed.contributors, - entries: feed.entries.into_iter().map(|entry| entry.into()) - .collect::>(), - } - } - } -} - -impl FromStr for Feed { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.parse::() { - Ok(FeedData::Atom(feed)) => Ok(feed.into()), - // TODO: Implement the RSS conversions - Ok(FeedData::RSS(_)) => Err("RSS Unimplemented"), - Err(e) => Err(e), - } - } -} - - -impl FromStr for FeedData { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.parse::() { - Ok (feed) => Ok (FeedData::Atom(feed)), - _ => match s.parse::() { - Ok (rss::Rss(channel)) => Ok (FeedData::RSS(channel)), - _ => Err ("Could not parse XML as Atom or RSS from input") - } - } - } -} - -impl ToString for FeedData { - fn to_string(&self) -> String { - match self { - &FeedData::Atom(ref atom_feed) => atom_feed.to_string(), - &FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), - } - } -} - -#[cfg(test)] -mod test { - extern crate atom_syndication; - extern crate rss; - - use std::fs::File; - use std::io::Read; - use std::str::FromStr; - - use super::FeedData; - - // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs - #[test] - fn test_from_atom_file() { - let mut file = File::open("test-data/atom.xml").unwrap(); - let mut atom_string = String::new(); - file.read_to_string(&mut atom_string).unwrap(); - let feed = FeedData::from_str(&atom_string).unwrap(); - assert!(feed.to_string().len() > 0); - } - - // Source: https://github.com/frewsxcv/rust-rss/blob/master/src/lib.rs - #[test] - fn test_from_rss_file() { - let mut file = File::open("test-data/rss.xml").unwrap(); - let mut rss_string = String::new(); - file.read_to_string(&mut rss_string).unwrap(); - let rss = FeedData::from_str(&rss_string).unwrap(); - assert!(rss.to_string().len() > 0); - } - - // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs - #[test] - fn test_atom_to_string() { - let author = atom_syndication::Person { - name: "N. Blogger".to_string(), - ..Default::default() - }; - - let entry = atom_syndication::Entry { - title: "My first post!".to_string(), - content: Some("This is my first post".to_string()), - ..Default::default() - }; - - let feed = FeedData::Atom(atom_syndication::Feed { - title: "My Blog".to_string(), - authors: vec![author], - entries: vec![entry], - ..Default::default() - }); - - assert_eq!(feed.to_string(), "My BlogN. BloggerMy first post!This is my first post"); - } - - // Source: https://github.com/frewsxcv/rust-rss/blob/master/src/lib.rs - #[test] - fn test_rss_to_string() { - let item = rss::Item { - title: Some("My first post!".to_string()), - link: Some("http://myblog.com/post1".to_string()), - description: Some("This is my first post".to_string()), - ..Default::default() - }; - - let channel = rss::Channel { - title: "My Blog".to_string(), - link: "http://myblog.com".to_string(), - description: "Where I write stuff".to_string(), - items: vec![item], - ..Default::default() - }; - - let rss = FeedData::RSS(channel); - assert_eq!(rss.to_string(), "My Bloghttp://myblog.comWhere I write stuffMy first post!http://myblog.com/post1This is my first post"); - } -} +mod category; +mod link; +mod person; +mod entry; +mod feed; + +pub use ::category::Category; +pub use ::link::Link; +pub use ::person::Person; +pub use ::entry::Entry; +pub use ::feed::Feed; diff --git a/src/link.rs b/src/link.rs new file mode 100644 index 0000000..17fb5ab --- /dev/null +++ b/src/link.rs @@ -0,0 +1,3 @@ +pub struct Link { + pub href: String, +} diff --git a/src/person.rs b/src/person.rs new file mode 100644 index 0000000..1efdb4d --- /dev/null +++ b/src/person.rs @@ -0,0 +1 @@ +pub struct Person { } From e30b0d47b7c050f5e8503ea059f4772ea32be4c1 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Tue, 17 May 2016 18:21:08 -0400 Subject: [PATCH 10/16] Implement all types needed for the public API The public API shouldn't expose any types from rss or atom_syndication, so this adds all the necessary types. --- src/category.rs | 49 ++++++++++++++++++++++++++++- src/entry.rs | 51 +++++++++++++----------------- src/feed.rs | 80 ++++++++++++++++++++---------------------------- src/generator.rs | 37 ++++++++++++++++++++++ src/lib.rs | 6 ++-- src/link.rs | 46 ++++++++++++++++++++++++++++ src/person.rs | 38 ++++++++++++++++++++++- 7 files changed, 227 insertions(+), 80 deletions(-) create mode 100644 src/generator.rs diff --git a/src/category.rs b/src/category.rs index 2f08401..85f141f 100644 --- a/src/category.rs +++ b/src/category.rs @@ -1 +1,48 @@ -pub struct Category { } +use atom_syndication as atom; +use rss; + +pub struct Category { + pub term: String, + pub scheme: Option, + pub label: Option, +} + +impl From for Category { + fn from(category: atom::Category) -> Category { + Category { + term: category.term, + scheme: category.scheme, + label: category.label, + } + } +} + +impl From for Category { + fn from(category: rss::Category) -> Category { + Category { + term: category.value, + scheme: category.domain, + // TODO: Should we duplicate the term in the label? + label: None, + } + } +} + +impl Into for Category { + fn into(self) -> atom::Category { + atom::Category { + term: self.term, + scheme: self.scheme, + label: self.label, + } + } +} + +impl Into for Category { + fn into(self) -> rss::Category { + rss::Category { + value: self.term, + domain: self.scheme, + } + } +} diff --git a/src/entry.rs b/src/entry.rs index 4ec8742..922d236 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -1,13 +1,13 @@ -extern crate atom_syndication; -extern crate rss; -extern crate chrono; +use atom_syndication as atom; +use rss; use chrono::{DateTime, UTC}; use category::Category; use link::Link; +use person::Person; enum EntryData { - Atom(atom_syndication::Entry), + Atom(atom::Entry), RSS(rss::Item), } @@ -37,60 +37,51 @@ pub struct Entry { // `categories` in both Atom and RSS pub categories: Vec, // `authors` in Atom, `author` in RSS (produces a Vec with 0 or 1 items) - // TODO: Define our own Person type for API stability reasons - pub authors: Vec, + pub authors: Vec, // `contributors` in Atom, not present in RSS (produces an empty Vec) - pub contributors: Vec, + pub contributors: Vec, } -impl From for Entry { - fn from(entry: atom_syndication::Entry) -> Self { +impl From for Entry { + fn from(entry: atom::Entry) -> Self { Entry { source_data: Some(EntryData::Atom(entry.clone())), id: Some(entry.id), title: Some(entry.title), updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) .map(|date| date.with_timezone(&UTC)) - .unwrap_or(UTC::now()), + .unwrap_or_else(|_| UTC::now()), published: entry.published .and_then(|d| DateTime::parse_from_rfc3339(d.as_str()).ok()) .map(|date| date.with_timezone(&UTC)), summary: entry.summary, content: entry.content, - links: entry.links - .into_iter() - .map(|link| Link { href: link.href }) - .collect::>(), - // TODO: Implement the Category type for converting this - categories: vec![], - authors: entry.authors, - contributors: entry.contributors, + links: entry.links.into_iter().map(|link| link.into()).collect(), + categories: entry.categories.into_iter().map(|category| category.into()).collect(), + authors: entry.authors.into_iter().map(|person| person.into()).collect(), + contributors: entry.contributors.into_iter().map(|person| person.into()).collect(), } } } -impl From for atom_syndication::Entry { +impl From for atom::Entry { fn from(entry: Entry) -> Self { if let Some(EntryData::Atom(entry)) = entry.source_data { entry } else { - atom_syndication::Entry { + atom::Entry { // TODO: How should we handle a missing id? - id: entry.id.unwrap_or(String::from("")), - title: entry.title.unwrap_or(String::from("")), + id: entry.id.unwrap_or_else(|| String::from("")), + title: entry.title.unwrap_or_else(|| String::from("")), updated: entry.updated.to_rfc3339(), published: entry.published.map(|date| date.to_rfc3339()), source: None, summary: entry.summary, content: entry.content, - links: entry.links - .into_iter() - .map(|link| atom_syndication::Link { href: link.href, ..Default::default() }) - .collect::>(), - // TODO: Convert from the category type - categories: vec![], - authors: entry.authors, - contributors: entry.contributors, + links: entry.links.into_iter().map(|link| link.into()).collect(), + categories: entry.categories.into_iter().map(|category| category.into()).collect(), + authors: entry.authors.into_iter().map(|person| person.into()).collect(), + contributors: entry.contributors.into_iter().map(|person| person.into()).collect(), } } } diff --git a/src/feed.rs b/src/feed.rs index 290943c..4c80d4a 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -1,21 +1,22 @@ -extern crate atom_syndication; -extern crate rss; -extern crate chrono; +use atom_syndication as atom; +use rss; use std::str::FromStr; use chrono::{DateTime, UTC}; +use link::Link; +use person::Person; +use generator::Generator; use category::Category; -use link::Link; use entry::Entry; enum FeedData { - Atom(atom_syndication::Feed), + Atom(atom::Feed), RSS(rss::Channel), } // A helpful table of approximately equivalent elements can be found here: -// http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#head-018c297098e131956bf394c0f7c8b6dd60f5cf78 +// http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#table pub struct Feed { // If created from an RSS or Atom feed, this is the original contents source_data: Option, @@ -38,27 +39,24 @@ pub struct Feed { pub image: Option, // `generator` in both Atom and RSS - // TODO: Add a Generator type so this can be implemented - // pub generator: Option, - // + pub generator: Option, // `links` in Atom, and `link` in RSS (produces a 1 item Vec) pub links: Vec, // `categories` in both Atom and RSS pub categories: Vec, - // TODO: Define our own Person type for API stability reasons // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? // `authors` in Atom, `managing_editor` in RSS (produces 1 item Vec) - pub authors: Vec, + pub authors: Vec, // `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec) - pub contributors: Vec, + pub contributors: Vec, // `entries` in Atom, and `items` in RSS // TODO: Add more fields that are necessary for RSS // TODO: Fancy translation, e.g. Atom = RSS `source` pub entries: Vec, } -impl From for Feed { - fn from(feed: atom_syndication::Feed) -> Self { +impl From for Feed { + fn from(feed: atom::Feed) -> Self { Feed { source_data: Some(FeedData::Atom(feed.clone())), id: Some(feed.id), @@ -70,16 +68,11 @@ impl From for Feed { copyright: feed.rights, icon: feed.icon, image: feed.logo, - // NOTE: We throw away the generator field - // TODO: Add more fields to the link type - links: feed.links - .into_iter() - .map(|link| Link { href: link.href }) - .collect::>(), - // TODO: Handle this once the Category type is defined - categories: vec![], - authors: feed.authors, - contributors: feed.contributors, + generator: feed.generator.map(|generator| generator.into()), + links: feed.links.into_iter().map(|link| link.into()).collect(), + categories: feed.categories.into_iter().map(|person| person.into()).collect(), + authors: feed.authors.into_iter().map(|person| person.into()).collect(), + contributors: feed.contributors.into_iter().map(|person| person.into()).collect(), entries: feed.entries .into_iter() .map(|entry| entry.into()) @@ -88,32 +81,28 @@ impl From for Feed { } } -impl From for atom_syndication::Feed { +impl From for atom::Feed { fn from(feed: Feed) -> Self { // Performing no translation at all is both faster, and won't lose any data! if let Some(FeedData::Atom(feed)) = feed.source_data { feed } else { - atom_syndication::Feed { + atom::Feed { // TODO: Producing an empty string is probably very very bad // is there anything better that can be done...? - id: feed.id.unwrap_or(String::from("")), + id: feed.id.unwrap_or_else(|| String::from("")), title: feed.title, subtitle: feed.description, // TODO: Is there a better way to handle a missing date here? - updated: feed.updated.unwrap_or(UTC::now()).to_rfc3339(), + updated: feed.updated.unwrap_or_else(UTC::now).to_rfc3339(), rights: feed.copyright, icon: feed.icon, logo: feed.image, generator: None, - links: feed.links - .into_iter() - .map(|link| atom_syndication::Link { href: link.href, ..Default::default() }) - .collect::>(), - // TODO: Convert from our Category type instead of throwing them away - categories: vec![], - authors: feed.authors, - contributors: feed.contributors, + links: feed.links.into_iter().map(|link| link.into()).collect(), + categories: feed.categories.into_iter().map(|category| category.into()).collect(), + authors: feed.authors.into_iter().map(|person| person.into()).collect(), + contributors: feed.contributors.into_iter().map(|person| person.into()).collect(), entries: feed.entries .into_iter() .map(|entry| entry.into()) @@ -140,7 +129,7 @@ impl FromStr for FeedData { type Err = &'static str; fn from_str(s: &str) -> Result { - match s.parse::() { + match s.parse::() { Ok(feed) => Ok(FeedData::Atom(feed)), _ => { match s.parse::() { @@ -154,17 +143,17 @@ impl FromStr for FeedData { impl ToString for FeedData { fn to_string(&self) -> String { - match self { - &FeedData::Atom(ref atom_feed) => atom_feed.to_string(), - &FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), + match *self { + FeedData::Atom(ref atom_feed) => atom_feed.to_string(), + FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), } } } #[cfg(test)] mod test { - extern crate atom_syndication; - extern crate rss; + use atom_syndication as atom; + use rss; use std::fs::File; use std::io::Read; @@ -195,16 +184,15 @@ mod test { // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs #[test] fn test_atom_to_string() { - let author = - atom_syndication::Person { name: "N. Blogger".to_string(), ..Default::default() }; + let author = atom::Person { name: "N. Blogger".to_string(), ..Default::default() }; - let entry = atom_syndication::Entry { + let entry = atom::Entry { title: "My first post!".to_string(), content: Some("This is my first post".to_string()), ..Default::default() }; - let feed = FeedData::Atom(atom_syndication::Feed { + let feed = FeedData::Atom(atom::Feed { title: "My Blog".to_string(), authors: vec![author], entries: vec![entry], diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..14f7cf4 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,37 @@ +use atom_syndication as atom; + +pub struct Generator { + pub name: String, + pub uri: Option, + pub version: Option, +} + +impl Generator { + pub fn from_name(name: String) -> Generator { + Generator { + name: name, + uri: None, + version: None, + } + } +} + +impl From for Generator { + fn from(generator: atom::Generator) -> Generator { + Generator { + name: generator.name, + uri: generator.uri, + version: generator.version, + } + } +} + +impl Into for Generator { + fn into(self) -> atom::Generator { + atom::Generator { + name: self.name, + uri: self.uri, + version: self.version, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0367cd6..bcbb45c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,15 @@ extern crate rss; extern crate chrono; mod category; -mod link; mod person; mod entry; mod feed; +mod link; +mod generator; pub use ::category::Category; -pub use ::link::Link; pub use ::person::Person; pub use ::entry::Entry; pub use ::feed::Feed; +pub use ::link::Link; +pub use ::generator::Generator; diff --git a/src/link.rs b/src/link.rs index 17fb5ab..621859c 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,3 +1,49 @@ +use atom_syndication as atom; + pub struct Link { pub href: String, + pub rel: Option, + pub mediatype: Option, + pub hreflang: Option, + pub title: Option, + pub length: Option, +} + +impl Link { + pub fn from_href(href: String) -> Link { + Link { + href: href, + rel: None, + mediatype: None, + hreflang: None, + title: None, + length: None, + } + } +} + +impl From for Link { + fn from(link: atom::Link) -> Link { + Link { + href: link.href, + rel: link.rel, + mediatype: link.mediatype, + hreflang: link.hreflang, + title: link.title, + length: link.length, + } + } +} + +impl Into for Link { + fn into(self) -> atom::Link { + atom::Link { + href: self.href, + rel: self.rel, + mediatype: self.mediatype, + hreflang: self.hreflang, + title: self.title, + length: self.length, + } + } } diff --git a/src/person.rs b/src/person.rs index 1efdb4d..904e728 100644 --- a/src/person.rs +++ b/src/person.rs @@ -1 +1,37 @@ -pub struct Person { } +use atom_syndication as atom; + +pub struct Person { + pub name: String, + pub uri: Option, + pub email: Option, +} + +impl Person { + pub fn from_name(name: String) -> Person { + Person { + name: name, + uri: None, + email: None, + } + } +} + +impl From for Person { + fn from(person: atom::Person) -> Person { + Person { + name: person.name, + uri: person.uri, + email: person.email, + } + } +} + +impl Into for Person { + fn into(self) -> atom::Person { + atom::Person { + name: self.name, + uri: self.uri, + email: self.email, + } + } +} From b3b6b4971dc1ec95f62301be73b8d93af297d125 Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Tue, 14 Jun 2016 21:17:39 -0400 Subject: [PATCH 11/16] Change Into impls to From impls --- src/category.rs | 28 ++++++++++++++-------------- src/generator.rs | 10 +++++----- src/link.rs | 16 ++++++++-------- src/person.rs | 10 +++++----- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/category.rs b/src/category.rs index 85f141f..2be7afa 100644 --- a/src/category.rs +++ b/src/category.rs @@ -17,6 +17,16 @@ impl From for Category { } } +impl From for atom::Category { + fn from(category: Category) -> atom::Category { + atom::Category { + term: category.term, + scheme: category.scheme, + label: category.label, + } + } +} + impl From for Category { fn from(category: rss::Category) -> Category { Category { @@ -28,21 +38,11 @@ impl From for Category { } } -impl Into for Category { - fn into(self) -> atom::Category { - atom::Category { - term: self.term, - scheme: self.scheme, - label: self.label, - } - } -} - -impl Into for Category { - fn into(self) -> rss::Category { +impl From for rss::Category { + fn from(category: Category) -> rss::Category { rss::Category { - value: self.term, - domain: self.scheme, + value: category.term, + domain: category.scheme, } } } diff --git a/src/generator.rs b/src/generator.rs index 14f7cf4..1fa4a06 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -26,12 +26,12 @@ impl From for Generator { } } -impl Into for Generator { - fn into(self) -> atom::Generator { +impl From for atom::Generator { + fn from(generator: Generator) -> atom::Generator { atom::Generator { - name: self.name, - uri: self.uri, - version: self.version, + name: generator.name, + uri: generator.uri, + version: generator.version, } } } diff --git a/src/link.rs b/src/link.rs index 621859c..d1e7765 100644 --- a/src/link.rs +++ b/src/link.rs @@ -35,15 +35,15 @@ impl From for Link { } } -impl Into for Link { - fn into(self) -> atom::Link { +impl From for atom::Link { + fn from(link: Link) -> atom::Link { atom::Link { - href: self.href, - rel: self.rel, - mediatype: self.mediatype, - hreflang: self.hreflang, - title: self.title, - length: self.length, + href: link.href, + rel: link.rel, + mediatype: link.mediatype, + hreflang: link.hreflang, + title: link.title, + length: link.length, } } } diff --git a/src/person.rs b/src/person.rs index 904e728..409f278 100644 --- a/src/person.rs +++ b/src/person.rs @@ -26,12 +26,12 @@ impl From for Person { } } -impl Into for Person { - fn into(self) -> atom::Person { +impl From for atom::Person { + fn from(person: Person) -> atom::Person { atom::Person { - name: self.name, - uri: self.uri, - email: self.email, + name: person.name, + uri: person.uri, + email: person.email, } } } From 3ffcfea1430c4998d45e19c42276bf49f54b5c6a Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Tue, 14 Jun 2016 22:01:00 -0400 Subject: [PATCH 12/16] Add RSS Entry conversions Add Guid type since the RSS Guid is more detailed than the Atom one. --- src/entry.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/guid.rs | 33 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/guid.rs diff --git a/src/entry.rs b/src/entry.rs index 922d236..4102e40 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -1,14 +1,16 @@ use atom_syndication as atom; use rss; +use std::str::FromStr; use chrono::{DateTime, UTC}; use category::Category; use link::Link; use person::Person; +use guid::Guid; enum EntryData { Atom(atom::Entry), - RSS(rss::Item), + Rss(rss::Item), } pub struct Entry { @@ -16,7 +18,7 @@ pub struct Entry { source_data: Option, // `id` in Atom (required), and `guid` in RSS - pub id: Option, + pub id: Option, // `title` in Atom and RSS, optional only in RSS pub title: Option, // `updated` in Atom (required), not present in RSS @@ -40,13 +42,15 @@ pub struct Entry { pub authors: Vec, // `contributors` in Atom, not present in RSS (produces an empty Vec) pub contributors: Vec, + // not present in Atom (produces None), `comments` in RSS + pub comments: Option, } impl From for Entry { fn from(entry: atom::Entry) -> Self { Entry { source_data: Some(EntryData::Atom(entry.clone())), - id: Some(entry.id), + id: Some(Guid::from_id(entry.id)), title: Some(entry.title), updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) .map(|date| date.with_timezone(&UTC)) @@ -60,6 +64,7 @@ impl From for Entry { categories: entry.categories.into_iter().map(|category| category.into()).collect(), authors: entry.authors.into_iter().map(|person| person.into()).collect(), contributors: entry.contributors.into_iter().map(|person| person.into()).collect(), + comments: None, } } } @@ -71,10 +76,11 @@ impl From for atom::Entry { } else { atom::Entry { // TODO: How should we handle a missing id? - id: entry.id.unwrap_or_else(|| String::from("")), + id: entry.id.unwrap_or_else(|| Guid::from_id("".into())).id, title: entry.title.unwrap_or_else(|| String::from("")), updated: entry.updated.to_rfc3339(), published: entry.published.map(|date| date.to_rfc3339()), + // TODO: Figure out this thing... source: None, summary: entry.summary, content: entry.content, @@ -86,3 +92,43 @@ impl From for atom::Entry { } } } + +impl From for Entry { + fn from(entry: rss::Item) -> Self { + let entry_clone = entry.clone(); + let date = entry.pub_date.and_then(|d| DateTime::from_str(&d[..]).ok()); + Entry { + source_data: Some(EntryData::Rss(entry_clone)), + id: entry.guid.map(|id| id.into()), + title: entry.title, + updated: date.clone().unwrap_or_else(UTC::now), + published: date, + summary: None, + content: entry.description, + links: entry.link.into_iter().map(Link::from_href).collect(), + categories: entry.categories.into_iter().map(|category| category.into()).collect(), + authors: entry.author.into_iter().map(Person::from_name).collect(), + contributors: vec![], + comments: entry.comments, + } + } +} + +impl From for rss::Item { + fn from(entry: Entry) -> rss::Item { + if let Some(EntryData::Rss(entry)) = entry.source_data { + entry + } else { + rss::Item { + guid: entry.id.map(|id| id.into()), + title: entry.title, + author: entry.authors.into_iter().next().map(|person| person.name), + pub_date: entry.published.map(|date| date.to_rfc2822()), + description: entry.content, + link: entry.links.into_iter().next().map(|link| link.href), + categories: entry.categories.into_iter().map(|category| category.into()).collect(), + comments: entry.comments, + } + } + } +} diff --git a/src/guid.rs b/src/guid.rs new file mode 100644 index 0000000..43ac5a1 --- /dev/null +++ b/src/guid.rs @@ -0,0 +1,33 @@ +use rss; + +pub struct Guid { + pub is_permalink: bool, + pub id: String, +} + +impl Guid { + pub fn from_id(id: String) -> Guid { + Guid { + is_permalink: true, + id: id, + } + } +} + +impl From for Guid { + fn from(id: rss::Guid) -> Guid { + Guid { + is_permalink: id.is_perma_link, + id: id.value, + } + } +} + +impl From for rss::Guid { + fn from(id: Guid) -> rss::Guid { + rss::Guid { + is_perma_link: id.is_permalink, + value: id.id, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index bcbb45c..632da2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod entry; mod feed; mod link; mod generator; +mod guid; pub use ::category::Category; pub use ::person::Person; @@ -15,3 +16,4 @@ pub use ::entry::Entry; pub use ::feed::Feed; pub use ::link::Link; pub use ::generator::Generator; +pub use ::guid::Guid; From bbf50f90f5c77626d98e695a40ddf9506d92622f Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Wed, 15 Jun 2016 02:50:49 -0400 Subject: [PATCH 13/16] Add conversions between our Entry and RSS's Entry Add the Image and TextInput types to support the change. --- .gitignore | 2 + src/entry.rs | 3 +- src/feed.rs | 120 +++++++++++++++++++++++++++++++++++++++++----- src/image.rs | 33 +++++++++++++ src/lib.rs | 4 ++ src/text_input.rs | 30 ++++++++++++ 6 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 src/image.rs create mode 100644 src/text_input.rs diff --git a/.gitignore b/.gitignore index a9d37c5..14637c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target Cargo.lock +.DS_Store +*.bk diff --git a/src/entry.rs b/src/entry.rs index 4102e40..130232b 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -3,6 +3,7 @@ use rss; use std::str::FromStr; use chrono::{DateTime, UTC}; + use category::Category; use link::Link; use person::Person; @@ -101,7 +102,7 @@ impl From for Entry { source_data: Some(EntryData::Rss(entry_clone)), id: entry.guid.map(|id| id.into()), title: entry.title, - updated: date.clone().unwrap_or_else(UTC::now), + updated: date.unwrap_or_else(UTC::now), published: date, summary: None, content: entry.description, diff --git a/src/feed.rs b/src/feed.rs index 4c80d4a..d8132ad 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -3,16 +3,18 @@ use rss; use std::str::FromStr; use chrono::{DateTime, UTC}; + use link::Link; use person::Person; use generator::Generator; - use category::Category; use entry::Entry; +use image::Image; +use text_input::TextInput; enum FeedData { Atom(atom::Feed), - RSS(rss::Channel), + Rss(rss::Channel), } // A helpful table of approximately equivalent elements can be found here: @@ -36,7 +38,7 @@ pub struct Feed { // `icon` in Atom, not present in RSS pub icon: Option, // `logo` in Atom, and `image` in RSS - pub image: Option, + pub image: Option, // `generator` in both Atom and RSS pub generator: Option, @@ -44,21 +46,39 @@ pub struct Feed { pub links: Vec, // `categories` in both Atom and RSS pub categories: Vec, - // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? // `authors` in Atom, `managing_editor` in RSS (produces 1 item Vec) + // TODO: Should the `web_master` be in `contributors`, `authors`, or at all? pub authors: Vec, // `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec) pub contributors: Vec, // `entries` in Atom, and `items` in RSS - // TODO: Add more fields that are necessary for RSS // TODO: Fancy translation, e.g. Atom = RSS `source` pub entries: Vec, + + // TODO: Add more fields that are necessary for RSS + // `ttl` in RSS, not present in Atom + pub ttl: Option, + // `skip_hours` in RSS, not present in Atom + pub skip_hours: Option, + // `skip_days` in RSS, not present in Atom + pub skip_days: Option, + // `text_input` in RSS, not present in Atom + pub text_input: Option, + // `language` in RSS, not present in Atom + pub language: Option, + // `docs` in RSS, not present in Atom + pub docs: Option, + // `rating` in RSS, not present in Atom + pub rating: Option, } impl From for Feed { fn from(feed: atom::Feed) -> Self { + let feed_clone = feed.clone(); + let title = feed.title.clone(); + let link = feed.links.first().map_or_else(|| "".into(), |link| link.href.clone()); Feed { - source_data: Some(FeedData::Atom(feed.clone())), + source_data: Some(FeedData::Atom(feed_clone)), id: Some(feed.id), title: feed.title, description: feed.subtitle, @@ -67,7 +87,17 @@ impl From for Feed { .map(|date| date.with_timezone(&UTC)), copyright: feed.rights, icon: feed.icon, - image: feed.logo, + // (Note, in practice the image and <link> should have the same value as the + // channel's <title> and <link>.) + image: feed.logo.map(|url| { + Image { + url: url, + title: title, + link: link, + width: None, + height: None, + } + }), generator: feed.generator.map(|generator| generator.into()), links: feed.links.into_iter().map(|link| link.into()).collect(), categories: feed.categories.into_iter().map(|person| person.into()).collect(), @@ -77,6 +107,13 @@ impl From<atom::Feed> for Feed { .into_iter() .map(|entry| entry.into()) .collect::<Vec<_>>(), + ttl: None, + skip_hours: None, + skip_days: None, + text_input: None, + language: None, + docs: None, + rating: None, } } } @@ -97,7 +134,7 @@ impl From<Feed> for atom::Feed { updated: feed.updated.unwrap_or_else(UTC::now).to_rfc3339(), rights: feed.copyright, icon: feed.icon, - logo: feed.image, + logo: feed.image.map(|image| image.url), generator: None, links: feed.links.into_iter().map(|link| link.into()).collect(), categories: feed.categories.into_iter().map(|category| category.into()).collect(), @@ -112,14 +149,71 @@ impl From<Feed> for atom::Feed { } } +impl From<rss::Channel> for Feed { + fn from(feed: rss::Channel) -> Self { + Feed { + source_data: Some(FeedData::Rss(feed.clone())), + id: None, + title: feed.title, + description: Some(feed.description), + updated: None, + copyright: feed.copyright, + icon: None, + image: feed.image.map(|image| image.into()), + generator: feed.generator.map(Generator::from_name), + links: vec![Link::from_href(feed.link)], + categories: feed.categories.into_iter().map(|person| person.into()).collect(), + authors: feed.managing_editor.into_iter().map(Person::from_name).collect(), + contributors: feed.web_master.into_iter().map(Person::from_name).collect(), + entries: feed.items.into_iter().map(|entry| entry.into()).collect(), + ttl: feed.ttl, + skip_hours: feed.skip_hours, + skip_days: feed.skip_days, + text_input: feed.text_input.map(|input| input.into()), + rating: feed.rating, + language: feed.language, + docs: feed.docs, + } + } +} + +impl From<Feed> for rss::Channel { + fn from(feed: Feed) -> rss::Channel { + if let Some(FeedData::Rss(feed)) = feed.source_data { + feed + } else { + rss::Channel { + title: feed.title, + description: feed.description.unwrap_or("".into()), + pub_date: None, + last_build_date: feed.updated.map(|date| date.to_rfc2822()), + link: feed.links.into_iter().next().map_or_else(|| "".into(), |link| link.href), + items: feed.entries.into_iter().map(|entry| entry.into()).collect(), + categories: feed.categories.into_iter().map(|category| category.into()).collect(), + image: feed.image.map(|image| image.into()), + generator: feed.generator.map(|generator| generator.name), + managing_editor: feed.authors.into_iter().next().map(|person| person.name), + web_master: feed.contributors.into_iter().next().map(|person| person.name), + copyright: feed.copyright, + ttl: feed.ttl, + skip_hours: feed.skip_hours, + skip_days: feed.skip_days, + text_input: feed.text_input.map(|input| input.into()), + rating: feed.rating, + language: feed.language, + docs: feed.docs, + } + } + } +} + impl FromStr for Feed { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { match s.parse::<FeedData>() { Ok(FeedData::Atom(feed)) => Ok(feed.into()), - // TODO: Implement the RSS conversions - Ok(FeedData::RSS(_)) => Err("RSS Unimplemented"), + Ok(FeedData::Rss(feed)) => Ok(feed.into()), Err(e) => Err(e), } } @@ -133,7 +227,7 @@ impl FromStr for FeedData { Ok(feed) => Ok(FeedData::Atom(feed)), _ => { match s.parse::<rss::Rss>() { - Ok(rss::Rss(channel)) => Ok(FeedData::RSS(channel)), + Ok(rss::Rss(channel)) => Ok(FeedData::Rss(channel)), _ => Err("Could not parse XML as Atom or RSS from input"), } } @@ -145,7 +239,7 @@ impl ToString for FeedData { fn to_string(&self) -> String { match *self { FeedData::Atom(ref atom_feed) => atom_feed.to_string(), - FeedData::RSS(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), + FeedData::Rss(ref rss_channel) => rss::Rss(rss_channel.clone()).to_string(), } } } @@ -226,7 +320,7 @@ mod test { ..Default::default() }; - let rss = FeedData::RSS(channel); + let rss = FeedData::Rss(channel); assert_eq!(rss.to_string(), "<?xml version=\'1.0\' encoding=\'UTF-8\'?><rss \ version=\'2.0\'><channel><title>My \ diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..fcbfb4a --- /dev/null +++ b/src/image.rs @@ -0,0 +1,33 @@ +use rss; + +pub struct Image { + pub url: String, + pub title: String, + pub link: String, + pub width: Option<u32>, + pub height: Option<u32>, +} + +impl From<rss::Image> for Image { + fn from(image: rss::Image) -> Image { + Image { + url: image.url, + title: image.title, + link: image.link, + width: image.width, + height: image.height, + } + } +} + +impl From<Image> for rss::Image { + fn from(image: Image) -> rss::Image { + rss::Image { + url: image.url, + title: image.title, + link: image.link, + width: image.width, + height: image.height, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 632da2b..d99dfd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ mod feed; mod link; mod generator; mod guid; +mod image; +mod text_input; pub use ::category::Category; pub use ::person::Person; @@ -17,3 +19,5 @@ pub use ::feed::Feed; pub use ::link::Link; pub use ::generator::Generator; pub use ::guid::Guid; +pub use ::image::Image; +pub use ::text_input::TextInput; diff --git a/src/text_input.rs b/src/text_input.rs new file mode 100644 index 0000000..5f4a4e7 --- /dev/null +++ b/src/text_input.rs @@ -0,0 +1,30 @@ +use rss; + +pub struct TextInput { + pub title: String, + pub description: String, + pub name: String, + pub link: String, +} + +impl From<rss::TextInput> for TextInput { + fn from(input: rss::TextInput) -> TextInput { + TextInput { + title: input.title, + description: input.description, + name: input.name, + link: input.link, + } + } +} + +impl From<TextInput> for rss::TextInput { + fn from(input: TextInput) -> rss::TextInput { + rss::TextInput { + title: input.title, + description: input.description, + name: input.name, + link: input.link, + } + } +} From c11d7efef6d9c3cdc5d0d59b75a42ccc1d7b1a7c Mon Sep 17 00:00:00 2001 From: Caleb Jones <self@calebjones.net> Date: Wed, 15 Jun 2016 02:54:39 -0400 Subject: [PATCH 14/16] Derive Debug, PartialEq, Eq, & Clone when possible EntryData and FeedData need an explicitly implementation of Debug because the atom types don't support it, and we probably don't want to show the contained feeds anyway. --- src/category.rs | 1 + src/entry.rs | 12 ++++++++++++ src/feed.rs | 12 ++++++++++++ src/generator.rs | 1 + src/guid.rs | 1 + src/image.rs | 1 + src/link.rs | 1 + src/person.rs | 1 + src/text_input.rs | 1 + 9 files changed, 31 insertions(+) diff --git a/src/category.rs b/src/category.rs index 2be7afa..2dd3c03 100644 --- a/src/category.rs +++ b/src/category.rs @@ -1,6 +1,7 @@ use atom_syndication as atom; use rss; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Category { pub term: String, pub scheme: Option<String>, diff --git a/src/entry.rs b/src/entry.rs index 130232b..c588898 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -2,6 +2,7 @@ use atom_syndication as atom; use rss; use std::str::FromStr; +use std::fmt::{Formatter, Debug, Error}; use chrono::{DateTime, UTC}; use category::Category; @@ -9,11 +10,22 @@ use link::Link; use person::Person; use guid::Guid; +#[derive(Clone)] enum EntryData { Atom(atom::Entry), Rss(rss::Item), } +impl Debug for EntryData { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match *self { + EntryData::Atom(_) => write!(f, "Atom(_)"), + EntryData::Rss(_) => write!(f, "Rss(_)"), + } + } +} + +#[derive(Debug, Clone)] pub struct Entry { // If created from an Atom or RSS entry, this is the original contents source_data: Option<EntryData>, diff --git a/src/feed.rs b/src/feed.rs index d8132ad..af68144 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -2,6 +2,7 @@ use atom_syndication as atom; use rss; use std::str::FromStr; +use std::fmt::{Debug, Formatter, Error}; use chrono::{DateTime, UTC}; use link::Link; @@ -12,13 +13,24 @@ use entry::Entry; use image::Image; use text_input::TextInput; +#[derive(Clone)] enum FeedData { Atom(atom::Feed), Rss(rss::Channel), } +impl Debug for FeedData { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match *self { + FeedData::Atom(_) => write!(f, "Atom(_)"), + FeedData::Rss(_) => write!(f, "Rss(_)"), + } + } +} + // A helpful table of approximately equivalent elements can be found here: // http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared#table +#[derive(Debug, Clone)] pub struct Feed { // If created from an RSS or Atom feed, this is the original contents source_data: Option<FeedData>, diff --git a/src/generator.rs b/src/generator.rs index 1fa4a06..a8da791 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,5 +1,6 @@ use atom_syndication as atom; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Generator { pub name: String, pub uri: Option<String>, diff --git a/src/guid.rs b/src/guid.rs index 43ac5a1..5bf23f8 100644 --- a/src/guid.rs +++ b/src/guid.rs @@ -1,5 +1,6 @@ use rss; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Guid { pub is_permalink: bool, pub id: String, diff --git a/src/image.rs b/src/image.rs index fcbfb4a..7954b8c 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,5 +1,6 @@ use rss; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Image { pub url: String, pub title: String, diff --git a/src/link.rs b/src/link.rs index d1e7765..289de9a 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,5 +1,6 @@ use atom_syndication as atom; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Link { pub href: String, pub rel: Option<String>, diff --git a/src/person.rs b/src/person.rs index 409f278..53816e8 100644 --- a/src/person.rs +++ b/src/person.rs @@ -1,5 +1,6 @@ use atom_syndication as atom; +#[derive(Debug, PartialEq, Clone)] pub struct Person { pub name: String, pub uri: Option<String>, diff --git a/src/text_input.rs b/src/text_input.rs index 5f4a4e7..5fccfc4 100644 --- a/src/text_input.rs +++ b/src/text_input.rs @@ -1,5 +1,6 @@ use rss; +#[derive(Debug, PartialEq, Eq, Clone)] pub struct TextInput { pub title: String, pub description: String, From aa0b250c1feaff66323ebf2ec8ca0b3f32634774 Mon Sep 17 00:00:00 2001 From: Caleb Jones <self@calebjones.net> Date: Wed, 15 Jun 2016 15:01:40 -0400 Subject: [PATCH 15/16] Add some basic tests for Feed --- src/feed.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/feed.rs b/src/feed.rs index af68144..ed2a82a 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -84,6 +84,26 @@ pub struct Feed { pub rating: Option<String>, } +impl Feed { + pub fn to_rss_string(&self) -> String { + if let Some(FeedData::Rss(ref feed)) = self.source_data { + rss::Rss(feed.clone()).to_string() + } else { + let rss: rss::Channel = self.clone().into(); + rss::Rss(rss).to_string() + } + } + + pub fn to_atom_string(&self) -> String { + if let Some(FeedData::Atom(ref feed)) = self.source_data { + feed.to_string() + } else { + let atom: atom::Feed = self.clone().into(); + atom.to_string() + } + } +} + impl From<atom::Feed> for Feed { fn from(feed: atom::Feed) -> Self { let feed_clone = feed.clone(); @@ -265,7 +285,7 @@ mod test { use std::io::Read; use std::str::FromStr; - use feed::FeedData; + use super::{FeedData, Feed}; // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs #[test] @@ -274,9 +294,20 @@ mod test { let mut atom_string = String::new(); file.read_to_string(&mut atom_string).unwrap(); let feed = FeedData::from_str(&atom_string).unwrap(); + // TODO: Assert a stronger property on this assert!(feed.to_string().len() > 0); } + #[test] + fn test_feed_from_atom_file() { + let mut file = File::open("test-data/atom.xml").unwrap(); + let mut atom_string = String::new(); + file.read_to_string(&mut atom_string).unwrap(); + let feed = Feed::from_str(&atom_string).unwrap(); + // TODO: Assert a stronger property than this + assert!(feed.to_atom_string().len() > 0); + } + // Source: https://github.com/frewsxcv/rust-rss/blob/master/src/lib.rs #[test] fn test_from_rss_file() { @@ -284,9 +315,20 @@ mod test { let mut rss_string = String::new(); file.read_to_string(&mut rss_string).unwrap(); let rss = FeedData::from_str(&rss_string).unwrap(); + // TODO: Assert a stronger property than this assert!(rss.to_string().len() > 0); } + #[test] + fn test_feed_from_rss_file() { + let mut file = File::open("test-data/rss.xml").unwrap(); + let mut rss_string = String::new(); + file.read_to_string(&mut rss_string).unwrap(); + let rss = Feed::from_str(&rss_string).unwrap(); + // TODO: Assert a stronger property than this + assert!(rss.to_rss_string().len() > 0); + } + // Source: https://github.com/vtduncan/rust-atom/blob/master/src/lib.rs #[test] fn test_atom_to_string() { From 24e3df4f4fbd46bef98fbd8f6d3a60a130ceda77 Mon Sep 17 00:00:00 2001 From: Caleb Jones <code@calebjones.net> Date: Fri, 21 Apr 2017 16:22:56 -0400 Subject: [PATCH 16/16] Update version number and add myself as an author --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9672caf..295a57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "syndication" -version = "0.4.0" -authors = ["Tom Shen <tom@shen.io>"] +version = "0.5.0" +authors = ["Tom Shen <tom@shen.io>", "Caleb Jones <code@calebjones.net>"] license = "MIT OR Apache-2.0" repository = "https://github.com/tomshen/rust-syndication" description = "Library for serializing Atom and RSS web feeds"