chug_cli/
formulae.rs

1use std::collections::{BTreeMap, btree_map::Entry};
2
3use serde::Deserialize;
4
5use crate::bottles::Bottles;
6
7const FORMULA_API: &str = "https://formulae.brew.sh/api/formula.json";
8
9#[derive(Debug, Deserialize)]
10pub struct Formula {
11    pub name: String,
12    pub aliases: Vec<String>,
13    pub dependencies: Vec<String>,
14    pub versions: Versions,
15    pub bottle: Bottles,
16}
17
18#[derive(Debug, Deserialize)]
19pub struct Versions {
20    pub stable: String,
21    pub bottle: bool,
22}
23
24impl Formula {
25    pub fn all() -> anyhow::Result<&'static [Formula]> {
26        let formulae = cache!(Vec<Formula>)
27            .with_file("formula.json")
28            .get_or_init_json(|| {
29                println!("Downloading fresh formula list...");
30                let json = reqwest::blocking::get(FORMULA_API)?.text()?;
31                Ok(json)
32            })?;
33        Ok(formulae)
34    }
35
36    pub fn get(name: &str) -> anyhow::Result<&'static Formula> {
37        Formula::get_exact(name).or_else(|_| Formula::get_by_alias(name))
38    }
39
40    pub fn get_exact(name: &str) -> anyhow::Result<&'static Formula> {
41        let formulae = Formula::all()?;
42        let Ok(index) = formulae.binary_search_by_key(&name, |f| &f.name) else {
43            anyhow::bail!("Unable to find formula with exact name: {name:?}");
44        };
45        Ok(&formulae[index])
46    }
47
48    fn get_by_alias(alias: &str) -> anyhow::Result<&'static Formula> {
49        let aliases = cache!(Vec<(&str, &Formula)>).get_or_init(|| {
50            let formulae = Formula::all()?;
51            let mut aliases = formulae
52                .iter()
53                .flat_map(|f| f.aliases.iter().map(move |a| (a.as_str(), f)))
54                .collect::<Vec<_>>();
55            aliases.sort_by_key(|&(a, _)| a);
56            Ok(aliases)
57        })?;
58
59        let Ok(index) = aliases.binary_search_by_key(&alias, |(a, _)| a) else {
60            anyhow::bail!("Unable to find formula: {alias:?}");
61        };
62        Ok(aliases[index].1)
63    }
64
65    pub fn resolve_dependencies(
66        roots: Vec<&str>,
67    ) -> anyhow::Result<BTreeMap<&'static str, &'static Formula>> {
68        let mut result = BTreeMap::<&str, &Formula>::new();
69        let mut stack = roots;
70        while let Some(name) = stack.pop() {
71            let formula = Formula::get(name)?;
72
73            if let Entry::Vacant(entry) = result.entry(formula.name.as_str()) {
74                entry.insert(formula);
75
76                for dependency in &formula.dependencies {
77                    if !result.contains_key(&dependency.as_str()) {
78                        stack.push(dependency);
79                    }
80                }
81            }
82        }
83
84        Ok(result)
85    }
86}