From b0fab89de2441508b96b302ed4905ab7ffc2162f Mon Sep 17 00:00:00 2001 From: Caleb Jones Date: Tue, 17 May 2016 01:06:43 -0400 Subject: [PATCH] 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 { }