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/Cargo.toml b/Cargo.toml index 67501e1..295a57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "syndication" -version = "0.4.0" -authors = ["Tom Shen "] +version = "0.5.0" +authors = ["Tom Shen ", "Caleb Jones "] license = "MIT OR Apache-2.0" repository = "https://github.com/tomshen/rust-syndication" description = "Library for serializing Atom and RSS web feeds" @@ -11,3 +11,4 @@ exclude = ["test-data/*"] [dependencies] atom_syndication = "0.3" rss = "0.3" +chrono = "0.2" 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/category.rs b/src/category.rs new file mode 100644 index 0000000..2dd3c03 --- /dev/null +++ b/src/category.rs @@ -0,0 +1,49 @@ +use atom_syndication as atom; +use rss; + +#[derive(Debug, PartialEq, Eq, Clone)] +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 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 { + term: category.value, + scheme: category.domain, + // TODO: Should we duplicate the term in the label? + label: None, + } + } +} + +impl From for rss::Category { + fn from(category: Category) -> rss::Category { + rss::Category { + value: category.term, + domain: category.scheme, + } + } +} diff --git a/src/entry.rs b/src/entry.rs new file mode 100644 index 0000000..457c620 --- /dev/null +++ b/src/entry.rs @@ -0,0 +1,151 @@ +use atom_syndication as atom; +use rss; + +use std::str::FromStr; +use std::fmt::{Formatter, Debug, Error}; +use chrono::{DateTime, UTC}; + +use category::Category; +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, + + // `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 + // TODO: Change this to include the type information from atom_syndication + 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) + 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(Guid::from_id(entry.id)), + title: Some(entry.title), + updated: DateTime::parse_from_rfc3339(entry.updated.as_str()) + .map(|date| date.with_timezone(&UTC)) + .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.map(|x| match x { + atom::Content::Text(s) | atom::Content::Html(s) => s, + atom::Content::Xhtml(x) => x.to_string(), + }), + 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(), + comments: None, + } + } +} + +impl From for atom::Entry { + fn from(entry: Entry) -> Self { + if let Some(EntryData::Atom(entry)) = entry.source_data { + entry + } else { + atom::Entry { + // TODO: How should we handle a missing id? + 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.map(atom::Content::Text), + 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 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.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/feed.rs b/src/feed.rs new file mode 100644 index 0000000..81d04fc --- /dev/null +++ b/src/feed.rs @@ -0,0 +1,397 @@ +use atom_syndication as atom; +use rss; + +use std::str::FromStr; +use std::fmt::{Debug, Formatter, Error}; +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; + +#[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(_)"), + } + } +} + +impl ToString for Feed { + fn to_string(&self) -> String { + match self.source_data { + Some(FeedData::Atom(ref atom_feed)) => atom_feed.to_string(), + Some(FeedData::Rss(ref rss_channel)) => rss::Rss(rss_channel.clone()).to_string(), + None => self.to_atom_string() + } + } +} + + +// 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, + + // `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 + 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, + // `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: 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 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 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)), + 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, + // (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(), + 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()) + .collect::<Vec<_>>(), + ttl: None, + skip_hours: None, + skip_days: None, + text_input: None, + language: None, + docs: None, + rating: None, + } + } +} + +impl From<Feed> 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::Feed { + // TODO: Producing an empty string is probably very very bad + // is there anything better that can be done...? + 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_else(UTC::now).to_rfc3339(), + rights: feed.copyright, + icon: feed.icon, + 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(), + 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()) + .collect::<Vec<_>>(), + } + } + } +} + +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()), + Ok(FeedData::Rss(feed)) => Ok(feed.into()), + Err(e) => Err(e), + } + } +} + +impl FromStr for FeedData { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.parse::<atom::Feed>() { + Ok(feed) => Ok(FeedData::Atom(feed)), + _ => { + match s.parse::<rss::Rss>() { + 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 { + use atom_syndication as atom; + use rss; + + use std::fs::File; + use std::io::Read; + use std::str::FromStr; + + use super::{FeedData, Feed}; + + // 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(); + // 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() { + 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(); + // 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() { + let author = atom::Person { name: "N. Blogger".to_string(), ..Default::default() }; + + let entry = atom::Entry { + title: "My first post!".to_string(), + content: Some(atom::Content::Text("This is my first post".to_string())), + ..Default::default() + }; + + let feed = FeedData::Atom(atom::Feed { + title: "My Blog".to_string(), + authors: vec![author], + entries: vec![entry], + ..Default::default() + }); + + assert_eq!(feed.to_string(), + "<?xml version=\"1.0\" encoding=\"utf-8\"?><feed \ + xmlns=\'http://www.w3.org/2005/Atom\'><id></id><title>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/generator.rs b/src/generator.rs new file mode 100644 index 0000000..a8da791 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,38 @@ +use atom_syndication as atom; + +#[derive(Debug, PartialEq, Eq, Clone)] +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 From for atom::Generator { + fn from(generator: Generator) -> atom::Generator { + atom::Generator { + name: generator.name, + uri: generator.uri, + version: generator.version, + } + } +} diff --git a/src/guid.rs b/src/guid.rs new file mode 100644 index 0000000..5bf23f8 --- /dev/null +++ b/src/guid.rs @@ -0,0 +1,34 @@ +use rss; + +#[derive(Debug, PartialEq, Eq, Clone)] +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/image.rs b/src/image.rs new file mode 100644 index 0000000..7954b8c --- /dev/null +++ b/src/image.rs @@ -0,0 +1,34 @@ +use rss; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Image { + pub url: String, + pub title: String, + pub link: String, + pub width: Option, + pub height: Option, +} + +impl From 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 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 bb39ebd..d99dfd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,110 +1,23 @@ extern crate atom_syndication; extern crate rss; - -use std::str::FromStr; - -pub enum Feed { - Atom(atom_syndication::Feed), - RSS(rss::Channel), -} - -impl FromStr for Feed { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s.parse::() { - Ok (feed) => Ok (Feed::Atom(feed)), - _ => match s.parse::() { - Ok (rss::Rss(channel)) => Ok (Feed::RSS(channel)), - _ => Err ("Could not parse XML as Atom or RSS from input") - } - } - } -} - -impl ToString for Feed { - 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(), - } - } -} - -#[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::Feed; - - // 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 = Feed::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 = Feed::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(atom_syndication::Content::Text("This is my first post".to_string())), - ..Default::default() - }; - - let feed = Feed::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 = Feed::RSS(channel); - assert_eq!(rss.to_string(), "My Bloghttp://myblog.comWhere I write stuffMy first post!http://myblog.com/post1This is my first post"); - } -} +extern crate chrono; + +mod category; +mod person; +mod entry; +mod feed; +mod link; +mod generator; +mod guid; +mod image; +mod text_input; + +pub use ::category::Category; +pub use ::person::Person; +pub use ::entry::Entry; +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/link.rs b/src/link.rs new file mode 100644 index 0000000..289de9a --- /dev/null +++ b/src/link.rs @@ -0,0 +1,50 @@ +use atom_syndication as atom; + +#[derive(Debug, PartialEq, Eq, Clone)] +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 From for atom::Link { + fn from(link: Link) -> atom::Link { + atom::Link { + 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 new file mode 100644 index 0000000..53816e8 --- /dev/null +++ b/src/person.rs @@ -0,0 +1,38 @@ +use atom_syndication as atom; + +#[derive(Debug, PartialEq, Clone)] +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 From for atom::Person { + fn from(person: Person) -> atom::Person { + atom::Person { + name: person.name, + uri: person.uri, + email: person.email, + } + } +} diff --git a/src/text_input.rs b/src/text_input.rs new file mode 100644 index 0000000..5fccfc4 --- /dev/null +++ b/src/text_input.rs @@ -0,0 +1,31 @@ +use rss; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TextInput { + pub title: String, + pub description: String, + pub name: String, + pub link: String, +} + +impl From for TextInput { + fn from(input: rss::TextInput) -> TextInput { + TextInput { + title: input.title, + description: input.description, + name: input.name, + link: input.link, + } + } +} + +impl From for rss::TextInput { + fn from(input: TextInput) -> rss::TextInput { + rss::TextInput { + title: input.title, + description: input.description, + name: input.name, + link: input.link, + } + } +}