use crate::config::Author;
use crate::post::Post;
use atom_syndication::{Entry, Error as AtomError, Feed, Link, Person, Text};
use chrono::{
FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, ParseError, ParseResult,
TimeZone, Utc,
};
use std::fmt;
use std::io::Write;
use url::Url;
pub struct FeedConfig {
pub title: String,
pub id: String,
pub author: Option<Author>,
pub home_page: Url,
}
pub fn write_feed<W: Write>(
config: FeedConfig,
posts: &[Post],
w: W,
) -> Result<()> {
feed(config, posts)?.write_to(w)?;
Ok(())
}
fn feed(config: FeedConfig, posts: &[Post]) -> ParseResult<Feed> {
use std::collections::BTreeMap;
Ok(Feed {
entries: feed_entries(&config, posts)?,
title: Text::plain(config.title),
id: config.id,
updated: FixedOffset::east(0)
.from_utc_datetime(&Utc::now().naive_utc()),
authors: author_to_people(config.author),
categories: Vec::new(),
contributors: Vec::new(),
generator: None,
icon: None,
logo: None,
rights: None,
subtitle: None,
extensions: BTreeMap::new(),
namespaces: BTreeMap::new(),
links: vec![Link {
href: config.home_page.into(),
rel: "alternate".to_string(),
title: None,
hreflang: None,
mime_type: None,
length: None,
}],
base: None,
lang: None,
})
}
fn feed_entries(
config: &FeedConfig,
posts: &[Post],
) -> ParseResult<Vec<Entry>> {
use std::collections::BTreeMap;
let mut entries: Vec<Entry> = Vec::with_capacity(posts.len());
for post in posts {
let (summary, _) = post.summary();
let naive_date = NaiveDate::parse_from_str(&post.date, "%Y-%m-%d")?;
let naive_time = NaiveTime::from_hms(0, 0, 0);
let naive_date_time = NaiveDateTime::new(naive_date, naive_time);
let offset = FixedOffset::east(0);
let date = offset.from_utc_datetime(&naive_date_time);
entries.push(Entry {
id: post.url.to_string(),
title: Text::plain(post.title.clone()),
updated: date,
authors: author_to_people(config.author.clone()),
links: vec![Link {
href: post.url.to_string(),
rel: "alternate".to_owned(),
title: None,
mime_type: None,
hreflang: None,
length: None,
}],
rights: None,
summary: Some(Text::html(summary.to_owned())),
categories: Vec::new(),
contributors: Vec::new(),
published: Some(date),
source: None,
content: None,
extensions: BTreeMap::new(),
})
}
Ok(entries)
}
fn author_to_people(author: Option<Author>) -> Vec<Person> {
match author {
Some(author) => vec![Person {
name: author.name,
email: author.email,
uri: None,
}],
None => Vec::new(),
}
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Atom(AtomError),
DateTimeParse(ParseError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Io(err) => err.fmt(f),
Error::Atom(err) => err.fmt(f),
Error::DateTimeParse(err) => err.fmt(f),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(err) => Some(err),
Error::Atom(err) => Some(err),
Error::DateTimeParse(err) => Some(err),
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl From<AtomError> for Error {
fn from(err: AtomError) -> Error {
Error::Atom(err)
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Error {
Error::DateTimeParse(err)
}
}