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}