chug-cli 0.1.3

The fastest way to consume Homebrew bottles
Documentation
use std::{
    fs,
    path::Path,
    sync::Mutex,
    time::{Duration, SystemTime},
};

use serde::de::DeserializeOwned;

use crate::dirs::cache_dir;

const DISK_CACHE_TIMEOUT: Duration = Duration::from_secs(24 * 3_600);

pub struct Cache<T: 'static> {
    contents: Mutex<Option<&'static T>>,
}

pub struct DiskCache<'a, T: 'static> {
    filename: &'a str,
    inner: &'a Cache<T>,
}

impl<T: 'static> Cache<T> {
    pub const fn new() -> Self {
        Cache {
            contents: Mutex::new(None),
        }
    }

    pub fn get_or_init(&self, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<&'static T> {
        let mut lock = self.contents.lock().unwrap();
        if let Some(contents) = lock.as_ref() {
            Ok(contents)
        } else {
            let value = f()?;

            let contents = Box::leak(Box::new(value));
            *lock = Some(contents);
            Ok(contents)
        }
    }

    pub fn with_file<'a>(&'a self, filename: &'a str) -> DiskCache<'a, T> {
        DiskCache {
            filename,
            inner: self,
        }
    }
}

impl<T: 'static> DiskCache<'_, T>
where
    T: DeserializeOwned,
{
    pub fn get_or_init_json(
        &self,
        f: impl FnOnce() -> anyhow::Result<String>,
    ) -> anyhow::Result<&'static T> {
        self.inner.get_or_init(|| {
            let disk_cache_path = cache_dir()?.join(self.filename);
            if let Ok(data) = load_json(&disk_cache_path) {
                return Ok(data);
            }

            let json = f()?;
            let value = serde_json::from_str(&json)?;

            store(&disk_cache_path, &json)?;

            Ok(value)
        })
    }
}

fn load_json<T: DeserializeOwned>(path: &Path) -> anyhow::Result<T> {
    let metadata = path.metadata()?;
    anyhow::ensure!(metadata.is_file());
    anyhow::ensure!(
        SystemTime::now().duration_since(metadata.modified()?)? < DISK_CACHE_TIMEOUT,
        "Disk cache has expired",
    );

    let json = fs::read_to_string(path)?;
    let formulae = serde_json::from_str(&json)?;
    Ok(formulae)
}

fn store(path: &Path, contents: &str) -> anyhow::Result<()> {
    fs::write(path, contents)?;
    Ok(())
}

macro_rules! cache {
    ($ty:ty) => {{
        use crate::cache::Cache;
        static CACHE: Cache<$ty> = Cache::new();
        &CACHE
    }};
}

pub fn http_client() -> &'static reqwest::blocking::Client {
    cache!(reqwest::blocking::Client)
        .get_or_init(|| {
            let client = reqwest::blocking::Client::new();
            Ok(client)
        })
        .unwrap()
}