news-flash 3.0.1

Base library for a modern feed reader
Documentation
use crate::feed_api::{FeedApiError, FeedApiResult};
use crate::models::FeedID;
use chrono::{DateTime, TimeDelta, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};

static FILE_NAME: &str = "local_rss_cache.json";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LocalRSSCache {
    #[serde(default)]
    etags: HashMap<FeedID, String>,
    #[serde(default)]
    cache_control: HashMap<FeedID, DateTime<Utc>>,
    #[serde(skip_serializing)]
    #[serde(skip_deserializing)]
    path: PathBuf,
}

impl LocalRSSCache {
    pub fn load(config_dir: &Path) -> FeedApiResult<Self> {
        let path = config_dir.join(FILE_NAME);
        if path.as_path().exists() {
            let mut contents = String::new();
            tracing::info!(?path, "Attempting to open local cache file");
            let mut file = File::open(&path).map_err(move |error| {
                tracing::error!(%error, "Failed to load local cache file");
                FeedApiError::IO(error)
            })?;
            file.read_to_string(&mut contents).map_err(move |error| {
                tracing::error!(%error, "Reading content of file failed");
                FeedApiError::IO(error)
            })?;
            let mut cache: LocalRSSCache = serde_json::from_str(&contents)?;
            cache.path = path;

            return Ok(cache);
        }

        tracing::info!(?path, "Local cache file does not exist. Returning empty cache");
        Ok(LocalRSSCache {
            etags: HashMap::new(),
            cache_control: HashMap::new(),
            path,
        })
    }

    pub fn get_etag(&self, feed_id: &FeedID) -> Option<String> {
        self.etags.get(feed_id).cloned()
    }

    pub fn set_etag(&mut self, feed_id: &FeedID, etag: Option<String>) {
        if let Some(etag) = etag {
            self.etags.insert(feed_id.clone(), etag);
        } else {
            self.etags.remove(feed_id);
        }
    }

    pub fn get_cache_control(&self, feed_id: &FeedID) -> Option<DateTime<Utc>> {
        self.cache_control.get(feed_id).copied()
    }

    pub fn set_cache_control(&mut self, feed_id: &FeedID, max_age: Option<u64>) {
        if let Some(max_age) = max_age {
            self.cache_control
                .insert(feed_id.clone(), Utc::now() + TimeDelta::seconds(max_age as i64));
        } else {
            self.cache_control.remove(feed_id);
        }
    }

    pub fn write(&self) -> FeedApiResult<()> {
        let data = serde_json::to_string_pretty(self)?;
        fs::write(&self.path, data).map_err(|error| {
            tracing::error!(?self.path, %error, "Failed to write local cache file to");
            FeedApiError::IO(error)
        })?;
        Ok(())
    }

    pub fn delete(&self) -> FeedApiResult<()> {
        fs::remove_file(&self.path)?;
        Ok(())
    }
}