chug-cli 0.1.3

The fastest way to consume Homebrew bottles
Documentation
use std::collections::{BTreeMap, btree_map::Entry};

use serde::Deserialize;

use crate::bottles::Bottles;

const FORMULA_API: &str = "https://formulae.brew.sh/api/formula.json";

#[derive(Debug, Deserialize)]
pub struct Formula {
    pub name: String,
    pub aliases: Vec<String>,
    pub dependencies: Vec<String>,
    pub versions: Versions,
    pub bottle: Bottles,
}

#[derive(Debug, Deserialize)]
pub struct Versions {
    pub stable: String,
    pub bottle: bool,
}

impl Formula {
    pub fn all() -> anyhow::Result<&'static [Formula]> {
        let formulae = cache!(Vec<Formula>)
            .with_file("formula.json")
            .get_or_init_json(|| {
                println!("Downloading fresh formula list...");
                let json = reqwest::blocking::get(FORMULA_API)?.text()?;
                Ok(json)
            })?;
        Ok(formulae)
    }

    pub fn get(name: &str) -> anyhow::Result<&'static Formula> {
        Formula::get_exact(name).or_else(|_| Formula::get_by_alias(name))
    }

    pub fn get_exact(name: &str) -> anyhow::Result<&'static Formula> {
        let formulae = Formula::all()?;
        let Ok(index) = formulae.binary_search_by_key(&name, |f| &f.name) else {
            anyhow::bail!("Unable to find formula with exact name: {name:?}");
        };
        Ok(&formulae[index])
    }

    fn get_by_alias(alias: &str) -> anyhow::Result<&'static Formula> {
        let aliases = cache!(Vec<(&str, &Formula)>).get_or_init(|| {
            let formulae = Formula::all()?;
            let mut aliases = formulae
                .iter()
                .flat_map(|f| f.aliases.iter().map(move |a| (a.as_str(), f)))
                .collect::<Vec<_>>();
            aliases.sort_by_key(|&(a, _)| a);
            Ok(aliases)
        })?;

        let Ok(index) = aliases.binary_search_by_key(&alias, |(a, _)| a) else {
            anyhow::bail!("Unable to find formula: {alias:?}");
        };
        Ok(aliases[index].1)
    }

    pub fn resolve_dependencies(
        roots: Vec<&str>,
    ) -> anyhow::Result<BTreeMap<&'static str, &'static Formula>> {
        let mut result = BTreeMap::<&str, &Formula>::new();
        let mut stack = roots;
        while let Some(name) = stack.pop() {
            let formula = Formula::get(name)?;

            if let Entry::Vacant(entry) = result.entry(formula.name.as_str()) {
                entry.insert(formula);

                for dependency in &formula.dependencies {
                    if !result.contains_key(&dependency.as_str()) {
                        stack.push(dependency);
                    }
                }
            }
        }

        Ok(result)
    }
}