Commit Diff


commit - 6397d1ece16453ad239561e06deebb0a352cd85a
commit + fac6f83825df33e7e88b106983ce5889f9d0c4e3
blob - 136c1bfa9246f2b06b5a9244755309d61db0db17
blob + 4b3d013017785bc22b3946c921230182cd48c4b2
--- Cargo.lock
+++ Cargo.lock
@@ -23,12 +23,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "anyhow"
-version = "1.0.61"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "508b352bb5c066aac251f6daf6b36eccd03e8a88e8081cd44959ea277a3af9a8"
-
-[[package]]
 name = "async-trait"
 version = "0.1.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -359,6 +353,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
 name = "dotenvy"
 version = "0.15.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1139,7 +1139,6 @@ dependencies = [
 name = "rss-email"
 version = "0.4.2"
 dependencies = [
- "anyhow",
  "atom_syndication",
  "chrono",
  "clap",
@@ -1150,6 +1149,7 @@ dependencies = [
  "rss",
  "serde",
  "simple_logger",
+ "snafu",
  "sqlx",
  "tokio",
  "toml",
@@ -1290,6 +1290,28 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
 
 [[package]]
+name = "snafu"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0656e7e3ffb70f6c39b3c2a86332bb74aa3c679da781642590f3c1118c5045"
+dependencies = [
+ "doc-comment",
+ "snafu-derive",
+]
+
+[[package]]
+name = "snafu-derive"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "475b3bbe5245c26f2d8a6f62d67c1f30eb9fffeccee721c45d162c3ebbdf81b2"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "socket2"
 version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1459,18 +1481,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e052
 
 [[package]]
 name = "thiserror"
-version = "1.0.32"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.32"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
 dependencies = [
  "proc-macro2",
  "quote",
blob - 6217251e560d2a295835dcd110a091c2be2b505a
blob + a6d7de3594d6fdeac55280d1386b7aa77a4698ab
--- Cargo.toml
+++ Cargo.toml
@@ -16,7 +16,6 @@ strip = "symbols"
 
 [dependencies]
 rss = "2.0"
-anyhow = "1.0"
 reqwest = {version = "0.11", default-features = false, features = ["rustls-tls"]}
 clap = { version = "3", features = ["derive"] }
 chrono = "0.4"
@@ -31,3 +30,4 @@ tokio = { version = "1.21.2", default-features = false
 sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "migrate", "sqlite", "offline"] }
 atom_syndication = "0.11.0"
 simple_logger = "4.0.0"
+snafu = "0.7.4"
blob - 2066caf7a569852f8bf6e3708166090170fbb650
blob + 104d5f46d08cb482905330ea1282eebb8cfdcee6
--- src/cli.rs
+++ src/cli.rs
@@ -2,6 +2,19 @@ use clap::Parser;
 use log::debug;
 use std::path::PathBuf;
 
+use snafu::{prelude::*, Snafu};
+
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("Io Error for \"{path}\": {source}"))]
+    Io {
+        path: String,
+        source: std::io::Error,
+    },
+    #[snafu(display("Logger error: {source}"))]
+    Logger { source: log::SetLoggerError },
+}
+
 #[derive(Parser)]
 #[clap(author, version, about)]
 pub struct Cli {
@@ -45,7 +58,7 @@ pub struct Cli {
 
 impl Cli {
     /// Parse the clap `Cli` struct with the command line arguments and create the database if it does not exist yet.
-    pub fn build_app() -> anyhow::Result<Self> {
+    pub fn build_app() -> Result<Self, Error> {
         use std::fs::File;
 
         let args = Self::parse();
@@ -57,7 +70,7 @@ impl Cli {
             2 => log::Level::Debug,
             _ => log::Level::Trace,
         };
-        simple_logger::init_with_level(verbosity)?;
+        simple_logger::init_with_level(verbosity).context(LoggerSnafu)?;
 
         let data_dir = data_dir();
         if !PathBuf::from(&data_dir).exists() {
@@ -65,7 +78,9 @@ impl Cli {
                 "No data directory exists, creating a new one: {:?}",
                 &data_dir
             );
-            std::fs::create_dir(&data_dir)?;
+            std::fs::create_dir(&data_dir).with_context(|_| IoSnafu {
+                path: data_dir.display().to_string(),
+            })?;
         }
 
         if File::open(&args.database_path).is_err() {
@@ -73,7 +88,9 @@ impl Cli {
                 "No database file exists, creating a new one: {:?}",
                 &args.database_path
             );
-            File::create(&args.database_path)?;
+            File::create(&args.database_path).with_context(|_| IoSnafu {
+                path: args.database_path.clone(),
+            })?;
         }
 
         Ok(args)
@@ -82,7 +99,8 @@ impl Cli {
 
 fn project_dirs() -> directories::ProjectDirs {
     #[allow(clippy::unwrap_used)]
-    directories::ProjectDirs::from("", "", "rss-email").unwrap()
+    directories::ProjectDirs::from("", "", "rss-email")
+        .expect("A valid home directory path from the OS")
 }
 
 /// Returns the base directory where all the configuration files are stored.
blob - 284b9035bb825496a7056c5689c715ad1b88ac96
blob + 40cbfa995122f513006038b8b4c7d727aa91fbd6
--- src/config.rs
+++ src/config.rs
@@ -1,8 +1,24 @@
-use anyhow::Context;
 use serde::Deserialize;
+use snafu::{prelude::*, Snafu};
 use std::fs::File;
 use std::{io::Read, path::Path};
 
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("Io Error for \"{path}\": {source}"))]
+    Io {
+        path: String,
+        source: std::io::Error,
+    },
+    Toml {
+        source: toml::de::Error,
+    },
+    #[snafu(display("Cli error: {source}"))]
+    Cli {
+        source: crate::cli::Error,
+    },
+}
+
 #[derive(Debug, Deserialize)]
 pub struct Config {
     pub(crate) mail: Mail,
@@ -24,12 +40,18 @@ pub struct SmtpConfig {
 }
 
 impl Config {
-    pub fn new(config_path: impl AsRef<Path>) -> anyhow::Result<Self> {
+    pub fn new(config_path: impl AsRef<Path>) -> Result<Self, Error> {
+        let config_path = config_path.as_ref();
         let mut string = String::new();
-        File::open(config_path.as_ref())
-            .with_context(|| format!("File {:?} does not exist", &config_path.as_ref()))?
-            .read_to_string(&mut string)?;
-        let config: Self = toml::de::from_str(&string)?;
+        File::open(config_path)
+            .with_context(|_| IoSnafu {
+                path: config_path.display().to_string(),
+            })?
+            .read_to_string(&mut string)
+            .with_context(|_| IoSnafu {
+                path: config_path.display().to_string(),
+            })?;
+        let config: Self = toml::de::from_str(&string).context(TomlSnafu)?;
 
         Ok(config)
     }
@@ -59,8 +81,8 @@ impl AppConfig {
     ///
     /// Internally, this will parse the command line arguments with [`clap`] and read the
     /// configuration file *if* all of the required options have *not* been given.
-    pub fn new() -> anyhow::Result<Self> {
-        let cli = crate::cli::Cli::build_app()?;
+    pub fn new() -> Result<Self, Error> {
+        let cli = crate::cli::Cli::build_app().context(CliSnafu)?;
         let mut s = Self {
             database_path: cli.database_path,
             urls_path: cli.urls_path,
blob - c1797f8dd887d33f4370e8cdfef1f3a55b19d7c5
blob + e51b0580ebdb438787ae9c3c43c394c317fae02a
--- src/db.rs
+++ src/db.rs
@@ -1,14 +1,16 @@
+use snafu::prelude::*;
 use sqlx::pool::PoolConnection;
 use sqlx::Sqlite;
 
 use crate::models::Post;
+use crate::{Error, GeneralDatabaseSnafu};
 
 /// Insert a new post or update an old one with the same GUID.
 ///
 /// # Errors
 ///
 /// An error occurs if the statement cannot be executed.
-pub async fn insert_item(mut conn: PoolConnection<Sqlite>, post: &Post) -> anyhow::Result<()> {
+pub async fn insert_item(mut conn: PoolConnection<Sqlite>, post: &Post) -> Result<(), Error> {
     sqlx::query!(
         "insert or ignore into posts (guid, title, url, pub_date, content) values (?, ?, ?, ?, ?)",
         post.guid,
@@ -18,7 +20,8 @@ pub async fn insert_item(mut conn: PoolConnection<Sqli
         post.content
     )
     .execute(&mut conn)
-    .await?;
+    .await
+    .context(GeneralDatabaseSnafu)?;
 
     Ok(())
 }
blob - cdc2c1750a457cdc08f93f652ead8270c1ac3500
blob + 29d0bf7e8ec045d8ee2c556bc47934a9a7741e4f
--- src/feed.rs
+++ src/feed.rs
@@ -1,26 +1,59 @@
 use atom_syndication;
 use rss;
+use snafu::{prelude::*, Snafu};
 
-use crate::anyhow::Context;
 use crate::models::Post;
 
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("reqwest got error fetching feed \"{url}\": {source}"))]
+    ReceivingFeed { url: String, source: reqwest::Error },
+    #[snafu(display("RSS feed is formatted incorrectly: {url}"))]
+    InvalidRSS { url: String },
+    #[snafu(display("Atom feed is formatted incorrectly: {url}"))]
+    InvalidAtom { url: String },
+    #[snafu(display("Feed \"{url}\" is neither an RSS nor an Atom feed"))]
+    InvalidFeed { url: String },
+}
+
 /// Fetch a new feed, RSS or Atom.
 ///
 /// # Errors
 ///
 /// An error occurs if the feed cannot be fetched or is invalid.
-pub async fn fetch_new<S>(url: S) -> anyhow::Result<Vec<Post>>
+pub async fn fetch_new<S>(url: S) -> Result<Vec<Post>, Error>
 where
     S: AsRef<str> + Send,
 {
     debug!("Fetching feed for {}", url.as_ref());
     let url = url.as_ref();
-    let content = reqwest::get(url).await?.bytes().await?;
+    let content = reqwest::get(url)
+        .await
+        .with_context(|_| ReceivingFeedSnafu {
+            url: url.to_string(),
+        })?
+        .bytes()
+        .await
+        .with_context(|_| ReceivingFeedSnafu {
+            url: url.to_string(),
+        })?;
     match fetch_new_rss(&content[..]) {
-        Err(_) => {
-            fetch_new_atom(&content[..]).with_context(|| format!("Failed fetching feed for {url}"))
+        Err(rss::Error::InvalidStartTag) => match fetch_new_atom(&content[..]) {
+            Err(atom_syndication::Error::InvalidStartTag) => InvalidFeedSnafu {
+                url: url.to_string(),
+            }
+            .fail(),
+            Err(_) => InvalidAtomSnafu {
+                url: url.to_string(),
+            }
+            .fail(),
+            Ok(v) => Ok(v),
+        },
+        Err(_) => InvalidRSSSnafu {
+            url: url.to_string(),
         }
-        p => p,
+        .fail(),
+        Ok(v) => Ok(v),
     }
 }
 
@@ -29,8 +62,8 @@ where
 /// # Errors
 ///
 /// An error occurs if the given bytes are an invalid RSS feed.
-pub fn fetch_new_rss(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
-    let channel = rss::Channel::read_from(bytes).context("Unable to read from RSS feed")?;
+pub fn fetch_new_rss(bytes: &[u8]) -> Result<Vec<Post>, rss::Error> {
+    let channel = rss::Channel::read_from(bytes)?;
 
     Ok(channel
         .items
@@ -50,8 +83,8 @@ pub fn fetch_new_rss(bytes: &[u8]) -> anyhow::Result<V
 /// # Errors
 ///
 /// An error occurs if the given bytes are an invalid Atom feed.
-pub fn fetch_new_atom(bytes: &[u8]) -> anyhow::Result<Vec<Post>> {
-    let feed = atom_syndication::Feed::read_from(bytes).context("Unable to read from atom feed")?;
+pub fn fetch_new_atom(bytes: &[u8]) -> Result<Vec<Post>, atom_syndication::Error> {
+    let feed = atom_syndication::Feed::read_from(bytes)?;
 
     Ok(feed
         .entries
blob - 1f217a66fd933723bd85b74832ff8e3ce3a3d71c
blob + 51fbeb368498d43e6cd239897feffa5f323775e3
--- src/mail.rs
+++ src/mail.rs
@@ -3,7 +3,20 @@ use lettre::{
     transport::smtp::{authentication::Credentials, AsyncSmtpTransport},
     AsyncTransport, Tokio1Executor,
 };
+use snafu::{prelude::*, Snafu};
 
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("General Mail error: {source}"))]
+    GeneralMail { source: lettre::error::Error },
+    #[snafu(display("SMTP Mail error: {source}"))]
+    SMTP {
+        source: lettre::transport::smtp::Error,
+    },
+    #[snafu(display("Unable to parse {value}"))]
+    Parse { value: String },
+}
+
 pub struct Mail {
     subject: String,
     body: String,
@@ -36,15 +49,23 @@ impl Mail {
         from: &'a str,
         to: &'a str,
         mailer: &AsyncSmtpTransport<Tokio1Executor>,
-    ) -> anyhow::Result<()> {
+    ) -> Result<(), Error> {
         trace!("Sending to {}: {}", to, &self.subject);
+        let from = from.parse().map_err(|_| Error::Parse {
+            value: from.to_string(),
+        })?;
+        let to = to.parse().map_err(|_| Error::Parse {
+            value: to.to_string(),
+        })?;
+
         let email = Message::builder()
-            .from(from.parse()?)
-            .to(to.parse()?)
+            .from(from)
+            .to(to)
             .subject(self.subject)
-            .body(self.body)?;
+            .body(self.body)
+            .context(GeneralMailSnafu)?;
 
-        mailer.send(email).await?;
+        mailer.send(email).await.context(SMTPSnafu)?;
 
         Ok(())
     }
@@ -62,10 +83,11 @@ pub fn get_mailer(
     password: String,
     server: &str,
     port: u16,
-) -> anyhow::Result<AsyncSmtpTransport<Tokio1Executor>> {
+) -> Result<AsyncSmtpTransport<Tokio1Executor>, Error> {
     let creds = Credentials::new(user, password);
 
-    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(server)?
+    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(server)
+        .context(SMTPSnafu)?
         .credentials(creds)
         .port(port)
         .build();
blob - 05e73279c485fb1800f2dd273817995e1ea9e2a3
blob + 6f90934474817e779ff2f68d5db85b57206aec68
--- src/main.rs
+++ src/main.rs
@@ -2,7 +2,6 @@
     clippy::pedantic,
     clippy::nursery,
     clippy::unwrap_used,
-    clippy::expect_used,
     clippy::cargo,
     clippy::style,
     clippy::complexity,
@@ -16,8 +15,6 @@
 
 #[macro_use]
 extern crate log;
-#[macro_use]
-extern crate anyhow;
 
 pub mod cli;
 pub mod config;
@@ -30,8 +27,8 @@ use crate::{
     config::AppConfig,
     mail::{get_mailer, Mail},
 };
-use anyhow::Context;
 use lettre::{AsyncSmtpTransport, Tokio1Executor};
+use snafu::{prelude::*, Snafu};
 use std::{
     fs::File,
     io::{BufRead, BufReader},
@@ -41,26 +38,56 @@ use std::{
 use sqlx::{sqlite::SqlitePoolOptions, Sqlite};
 use tokio::task::JoinSet;
 
+#[derive(Debug, Snafu)]
+pub enum Error {
+    #[snafu(display("Io Error at \"{path}\": {source}"))]
+    Io {
+        path: String,
+        source: std::io::Error,
+    },
+    #[snafu(display("General database error: {source}"))]
+    GeneralDatabaseError { source: sqlx::Error },
+    #[snafu(display("Migration error: {source}"))]
+    MigrationError { source: sqlx::migrate::MigrateError },
+    #[snafu(display("Task in JoinSet failed to execute to completion: {source}"))]
+    JoinSet { source: tokio::task::JoinError },
+    #[snafu(display("Mail error: {source}"))]
+    Mail { source: crate::mail::Error },
+    #[snafu(display("Config error: {source}"))]
+    Config { source: crate::config::Error },
+}
+
 #[tokio::main]
-async fn main() -> anyhow::Result<()> {
-    let config = Arc::new(AppConfig::new()?);
-    let urls: Vec<String> = BufReader::new(
-        File::open(config.urls_path.as_str())
-            .with_context(|| format!("File {:?} does not exist", &config.urls_path))?,
-    )
-    .lines()
-    .filter_map(Result::ok)
-    .filter(|l| !l.starts_with('#'))
-    .collect();
+async fn main() {
+    if let Err(e) = app_main().await {
+        eprintln!("{e}");
+        std::process::exit(1);
+    }
+}
 
+async fn app_main() -> Result<(), Error> {
+    let config = Arc::new(AppConfig::new().context(ConfigSnafu)?);
+    let urls: Vec<String> =
+        BufReader::new(File::open(config.urls_path.as_str()).context(IoSnafu {
+            path: &config.urls_path,
+        })?)
+        .lines()
+        .filter_map(Result::ok)
+        .filter(|l| !l.starts_with('#'))
+        .collect();
+
     let db_path = &config.database_path;
     debug!("Establishing connection to database at {:?}", db_path);
     let pool = SqlitePoolOptions::new()
         .max_connections(5)
         .connect(db_path.as_str())
-        .await?;
+        .await
+        .context(GeneralDatabaseSnafu)?;
 
-    sqlx::migrate!("./migrations").run(&pool).await?;
+    sqlx::migrate!("./migrations")
+        .run(&pool)
+        .await
+        .context(MigrationSnafu)?;
 
     if config.no_fetch {
         info!("Not fetching any feeds as \"--no-fetch\" has been passed");
@@ -73,18 +100,20 @@ async fn main() -> anyhow::Result<()> {
         "select * from posts where sent != true order by pub_date desc"
     )
     .fetch_all(&pool)
-    .await?;
+    .await
+    .context(GeneralDatabaseSnafu)?;
 
     let mailer = get_mailer(
         config.smtp_user.clone(),
         config.smtp_password.clone(),
         &config.smtp_server,
         config.smtp_port,
-    )?;
+    )
+    .context(MailSnafu)?;
 
     let mut handles = JoinSet::new();
     for result in results {
-        let mut conn = pool.acquire().await?;
+        let mut conn = pool.acquire().await.context(GeneralDatabaseSnafu)?;
         let mailer = mailer.clone();
         let config = config.clone();
 
@@ -103,37 +132,32 @@ async fn main() -> anyhow::Result<()> {
 
     while let Some(handle) = handles.join_next().await {
         // TODO: retry sending mail instead? user should specify number of retries in config
-        if let Err(e) = handle? {
-            log::error!("An error occured while sending an email: {}", e);
+        if let Err(e) = handle.context(JoinSetSnafu)? {
+            log::error!("An error occured while sending an email: {e}");
         }
     }
 
     Ok(())
 }
 
-async fn fetch_feeds(urls: Vec<String>, pool: &sqlx::Pool<Sqlite>) -> anyhow::Result<()> {
+async fn fetch_feeds(urls: Vec<String>, pool: &sqlx::Pool<Sqlite>) -> Result<(), Error> {
     let mut set = JoinSet::new();
     for u in urls {
         set.spawn(async move { feed::fetch_new(u).await });
     }
 
     while let Some(new) = set.join_next().await {
-        let posts = match new? {
+        let posts = match new.context(JoinSetSnafu)? {
             Ok(p) => p,
             Err(e) => {
-                log::error!("Error while fetching feed: {}", e);
+                log::error!("Error while fetching feed: {e}");
                 continue;
             }
         };
 
         for i in posts {
-            let conn = pool.acquire().await?;
-            db::insert_item(conn, &i).await.with_context(|| {
-                format!(
-                    "Unable to insert item from {:?} with GUID {:?}",
-                    i.url, i.guid
-                )
-            })?;
+            let conn = pool.acquire().await.context(GeneralDatabaseSnafu)?;
+            db::insert_item(conn, &i).await?;
         }
     }
 
@@ -147,7 +171,7 @@ async fn send_post<'a, E>(
     to: &'a str,
     post: models::Post,
     dry_run: bool,
-) -> anyhow::Result<()>
+) -> Result<(), Error>
 where
     E: sqlx::Executor<'a, Database = Sqlite>,
 {
@@ -156,12 +180,14 @@ where
     } else {
         Mail::new(post.title, post.content, post.url)
             .send_email(from, to, &mailer)
-            .await?;
+            .await
+            .context(MailSnafu)?;
     }
 
     sqlx::query!("update posts set sent = true where guid = ?", post.guid)
         .execute(conn)
-        .await?;
+        .await
+        .context(GeneralDatabaseSnafu)?;
 
     Ok(())
 }
blob - 6b9fc15c737a4c11fd765fb3267a921c74ad404e
blob + 9e2af03f15e99e78f6d321acff448143c9c69437
--- src/models.rs
+++ src/models.rs
@@ -1,5 +1,12 @@
 use chrono::DateTime;
+use snafu::Snafu;
 
+#[derive(Debug, Snafu)]
+#[snafu(display("Missing GUID on item: {item:?}"))]
+pub struct MissingGUID {
+    item: rss::Item,
+}
+
 #[derive(Debug)]
 pub struct Post {
     pub guid: String,
@@ -11,9 +18,9 @@ pub struct Post {
 }
 
 impl TryFrom<rss::Item> for Post {
-    type Error = anyhow::Error;
+    type Error = MissingGUID;
 
-    fn try_from(item: rss::Item) -> anyhow::Result<Self> {
+    fn try_from(item: rss::Item) -> Result<Self, Self::Error> {
         let time = item.pub_date().map(|date| {
             DateTime::parse_from_rfc2822(date)
                 .unwrap_or_else(|_| DateTime::default())
@@ -22,7 +29,7 @@ impl TryFrom<rss::Item> for Post {
 
         let guid = item
             .guid()
-            .ok_or_else(|| anyhow!("No guid found"))?
+            .ok_or_else(|| MissingGUID { item: item.clone() })?
             .value()
             .to_string();
         let title = item.title().map(String::from);
@@ -44,8 +51,10 @@ impl TryFrom<rss::Item> for Post {
     }
 }
 
+// `TryFrom` is implemented for consistency, not because it can actually fail at the moment
 impl TryFrom<atom_syndication::Entry> for Post {
-    type Error = anyhow::Error;
+    // NOTE: NOT USED
+    type Error = MissingGUID;
 
     fn try_from(value: atom_syndication::Entry) -> Result<Self, Self::Error> {
         let guid = value.id.clone();