mod client;
mod result;
mod source;
use std::slice::Iter;
use chrono::{DateTime, Local};
use feed_rs::model::{Entry as RssEntry, Feed as RssFeed};
pub use self::client::Client;
pub use self::result::{FeedError, FeedResult};
pub use self::source::FeedSource;
use crate::helpers::strings as str_helpers;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Feed {
pub name: String,
pub(crate) articles: Vec<Article>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Article {
pub id: String,
pub title: Option<String>,
pub authors: Vec<String>,
pub summary: String,
pub url: String,
pub date: Option<DateTime<Local>>,
}
impl Feed {
pub fn articles(&self) -> Iter<'_, Article> {
self.articles.iter()
}
pub fn new(name: impl ToString, feed: RssFeed) -> Self {
let mut articles: Vec<Article> = feed.entries.into_iter().map(Article::from).collect();
articles.sort_by_key(|x| std::cmp::Reverse(x.date));
Self {
name: name.to_string(),
articles,
}
}
}
impl From<RssEntry> for Article {
fn from(entry: RssEntry) -> Self {
let content_or_summary = content_or_summary(&entry);
Self {
id: entry.id.clone(),
title: entry
.title
.map(|x| str_helpers::strip_html(x.content.as_str())),
authors: entry.authors.into_iter().map(|x| x.name).collect(),
summary: content_or_summary,
url: entry
.links
.first()
.map(|x| x.href.clone())
.unwrap_or(entry.id),
date: entry
.published
.or(entry.updated)
.map(DateTime::<Local>::from),
}
}
}
fn content_or_summary(entry: &RssEntry) -> String {
let content = entry
.content
.as_ref()
.and_then(|x| x.body.as_ref().map(|x| str_helpers::strip_html(x)))
.unwrap_or_default();
if content.trim_matches('\n').trim().is_empty() {
entry
.summary
.as_ref()
.map(|x| str_helpers::strip_html(x.content.as_str()))
.unwrap_or_default()
} else {
content
}
}
#[cfg(test)]
mod test {
use feed_rs::model::FeedType;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn should_get_feed_attributes() {
let feed = Feed {
name: String::from("pippo"),
articles: Vec::default(),
};
assert!(feed.articles.is_empty());
}
#[test]
fn should_convert_entry_into_article() {
let entry = RssEntry::default();
let article = Article::from(entry);
assert!(article.authors.is_empty());
assert_eq!(article.date, None);
assert_eq!(article.summary, String::new());
assert_eq!(article.title, None);
assert_eq!(article.url, String::new());
}
#[test]
fn should_convert_rssfeed_into_feed() {
let feed = RssFeed {
feed_type: FeedType::Atom,
id: String::from("pippo"),
contributors: Vec::new(),
title: None,
updated: None,
authors: Vec::new(),
description: None,
links: Vec::new(),
categories: Vec::new(),
generator: None,
icon: None,
language: None,
logo: None,
published: None,
rating: None,
rights: None,
ttl: None,
entries: vec![RssEntry::default(), RssEntry::default()],
};
let feed = Feed::new("pippo", feed);
assert_eq!(feed.articles.len(), 2);
}
}