Skip to content

Commit

Permalink
Split the library into modules
Browse files Browse the repository at this point in the history
  • Loading branch information
porglezomp committed May 17, 2016
1 parent a5513f9 commit b0fab89
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 318 deletions.
1 change: 1 addition & 0 deletions src/category.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub struct Category { }
97 changes: 97 additions & 0 deletions src/entry.rs
Original file line number Diff line number Diff line change
@@ -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<EntryData>,

// `id` in Atom (required), and `guid` in RSS
pub id: Option<String>,
// `title` in Atom and RSS, optional only in RSS
pub title: Option<String>,
// `updated` in Atom (required), not present in RSS
pub updated: DateTime<UTC>,
// `published` in Atom, and `pub_date` in RSS
pub published: Option<DateTime<UTC>>,
// `summary` in Atom
pub summary: Option<String>,
// `content` in Atom, `description` in RSS
pub content: Option<String>,

// 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<Link>,
// `categories` in both Atom and RSS
pub categories: Vec<Category>,
// `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<atom_syndication::Person>,
// `contributors` in Atom, not present in RSS (produces an empty Vec)
pub contributors: Vec<atom_syndication::Person>,
}

impl From<atom_syndication::Entry> 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::<Vec<_>>(),
// TODO: Implement the Category type for converting this
categories: vec![],
authors: entry.authors,
contributors: entry.contributors,
}
}
}

impl From<Entry> 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::<Vec<_>>(),
// TODO: Convert from the category type
categories: vec![],
authors: entry.authors,
contributors: entry.contributors,
}
}
}
}
250 changes: 250 additions & 0 deletions src/feed.rs
Original file line number Diff line number Diff line change
@@ -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<FeedData>,

// `id` in Atom, not present in RSS
pub id: Option<String>,
// `title` in both Atom and RSS
pub title: String,
// `subtitle` in Atom, and `description` in RSS (required)
pub description: Option<String>,
// `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<DateTime<UTC>>,
// `rights` in Atom, and `copyright` in RSS
pub copyright: Option<String>,
// `icon` in Atom, not present in RSS
pub icon: Option<String>,
// `logo` in Atom, and `image` in RSS
pub image: Option<String>,

// `generator` in both Atom and RSS
// TODO: Add a Generator type so this can be implemented
// pub generator: Option<Generator>,
//
// `links` in Atom, and `link` in RSS (produces a 1 item Vec)
pub links: Vec<Link>,
// `categories` in both Atom and RSS
pub categories: Vec<Category>,
// 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<atom_syndication::Person>,
// `contributors` in Atom, `web_master` in RSS (produces a 1 item Vec)
pub contributors: Vec<atom_syndication::Person>,
// `entries` in Atom, and `items` in RSS
// TODO: Add more fields that are necessary for RSS
// TODO: Fancy translation, e.g. Atom <link rel="via"> = RSS `source`
pub entries: Vec<Entry>,
}

impl From<atom_syndication::Feed> 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::<Vec<_>>(),
// 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::<Vec<_>>(),
}
}
}

impl From<Feed> 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::<Vec<_>>(),
// 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::<Vec<_>>(),
}
}
}
}

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"),
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_syndication::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 {
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(),
"<?xml version=\"1.0\" encoding=\"utf-8\"?><feed \
xmlns=\'http://www.w3.org/2005/Atom\'><id></id><title>My \
Blog</title><updated></updated><author><name>N. \
Blogger</name></author><entry><id></id><title>My first \
post!</title><updated></updated><content>This is my first \
post</content></entry></feed>");
}

// 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(),
"<?xml version=\'1.0\' encoding=\'UTF-8\'?><rss \
version=\'2.0\'><channel><title>My \
Blog</title><link>http://myblog.com</link><description>Where I write \
stuff</description><item><title>My first \
post!</title><link>http://myblog.com/post1</link><description>This is my \
first post</description></item></channel></rss>");
}
}
Loading

0 comments on commit b0fab89

Please sign in to comment.