1use crate::config::Author;
4use crate::post::Post;
5use atom_syndication::{Entry, Error as AtomError, Feed, Link, Person, Text};
6use chrono::{
7 FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, ParseError, ParseResult,
8 TimeZone, Utc,
9};
10use std::fmt;
11use std::io::Write;
12use url::Url;
13
14pub struct FeedConfig {
16 pub title: String,
17 pub id: String,
18 pub author: Option<Author>,
19 pub home_page: Url,
20}
21
22pub fn write_feed<W: Write>(
26 config: FeedConfig,
27 posts: &[Post],
28 w: W,
29) -> Result<()> {
30 feed(config, posts)?.write_to(w)?;
31 Ok(())
32}
33
34fn feed(config: FeedConfig, posts: &[Post]) -> ParseResult<Feed> {
35 use std::collections::BTreeMap;
36 Ok(Feed {
37 entries: feed_entries(&config, posts)?,
38 title: Text::plain(config.title),
39 id: config.id,
40 updated: FixedOffset::east(0)
41 .from_utc_datetime(&Utc::now().naive_utc()),
42 authors: author_to_people(config.author),
43 categories: Vec::new(),
44 contributors: Vec::new(),
45 generator: None,
46 icon: None,
47 logo: None,
48 rights: None,
49 subtitle: None,
50 extensions: BTreeMap::new(),
51 namespaces: BTreeMap::new(),
52 links: vec![Link {
53 href: config.home_page.into(),
54 rel: "alternate".to_string(),
55 title: None,
56 hreflang: None,
57 mime_type: None,
58 length: None,
59 }],
60 base: None,
61 lang: None,
62 })
63}
64
65fn feed_entries(
66 config: &FeedConfig,
67 posts: &[Post],
68) -> ParseResult<Vec<Entry>> {
69 use std::collections::BTreeMap;
70 let mut entries: Vec<Entry> = Vec::with_capacity(posts.len());
71
72 for post in posts {
73 let (summary, _) = post.summary();
74
75 let naive_date = NaiveDate::parse_from_str(&post.date, "%Y-%m-%d")?;
83 let naive_time = NaiveTime::from_hms(0, 0, 0);
84 let naive_date_time = NaiveDateTime::new(naive_date, naive_time);
85 let offset = FixedOffset::east(0);
86 let date = offset.from_utc_datetime(&naive_date_time);
87
88 entries.push(Entry {
89 id: post.url.to_string(),
90 title: Text::plain(post.title.clone()),
91 updated: date,
92 authors: author_to_people(config.author.clone()),
93 links: vec![Link {
94 href: post.url.to_string(),
95 rel: "alternate".to_owned(),
96 title: None,
97 mime_type: None,
98 hreflang: None,
99 length: None,
100 }],
101 rights: None,
102 summary: Some(Text::html(summary.to_owned())),
103 categories: Vec::new(),
104 contributors: Vec::new(),
105 published: Some(date),
106 source: None,
107 content: None,
108 extensions: BTreeMap::new(),
109 })
110 }
111 Ok(entries)
112}
113
114fn author_to_people(author: Option<Author>) -> Vec<Person> {
115 match author {
116 Some(author) => vec![Person {
117 name: author.name,
118 email: author.email,
119 uri: None,
120 }],
121 None => Vec::new(),
122 }
123}
124
125type Result<T> = std::result::Result<T, Error>;
126
127#[derive(Debug)]
130pub enum Error {
131 Io(std::io::Error),
133
134 Atom(AtomError),
136
137 DateTimeParse(ParseError),
139}
140
141impl fmt::Display for Error {
142 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
144 match self {
145 Error::Io(err) => err.fmt(f),
146 Error::Atom(err) => err.fmt(f),
147 Error::DateTimeParse(err) => err.fmt(f),
148 }
149 }
150}
151
152impl std::error::Error for Error {
153 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
155 match self {
156 Error::Io(err) => Some(err),
157 Error::Atom(err) => Some(err),
158 Error::DateTimeParse(err) => Some(err),
159 }
160 }
161}
162
163impl From<std::io::Error> for Error {
164 fn from(err: std::io::Error) -> Error {
167 Error::Io(err)
168 }
169}
170
171impl From<AtomError> for Error {
172 fn from(err: AtomError) -> Error {
175 Error::Atom(err)
176 }
177}
178
179impl From<ParseError> for Error {
180 fn from(err: ParseError) -> Error {
183 Error::DateTimeParse(err)
184 }
185}