majordomus_cli/command_graph/
load.rs1use std::path::{Path, PathBuf};
23
24use crate::capability::builtin;
25use crate::capability::registry::CapabilityRegistry;
26use crate::cli::RepoArgs;
27use crate::environment::workflows;
28use crate::error::{Error, Result};
29use crate::repository::Repository;
30use crate::share::Share;
31
32use super::{bridge, build, CommandGraph, Inputs};
33
34pub const CACHE: &str = "graph.json";
36
37pub struct Loaded {
39 pub graph: CommandGraph,
41 pub root: PathBuf,
43 pub cached: bool,
45}
46
47pub fn registry() -> Option<CapabilityRegistry> {
55 CapabilityRegistry::builder()
56 .with_modules(builtin::modules())
57 .build()
58 .ok()
59}
60
61pub fn full(repo: &RepoArgs) -> Result<Loaded> {
63 let root = root_of(repo)?;
64 let share = Share::locate(repo.share.as_deref(), &root).ok();
65 let registry = registry();
66 let catalogue = workflows::resolve(&root);
67 let graph = build(&Inputs {
68 registry: registry.as_ref(),
69 share: share.as_ref().map(|s| s.dir()),
70 workflows: Some(&catalogue),
71 bridged: bridge::bridged(&root),
72 });
73 let _ = write_cache(&root, &graph);
74 Ok(Loaded {
75 graph,
76 root,
77 cached: false,
78 })
79}
80
81pub fn fast(repo: &RepoArgs) -> Result<Loaded> {
83 let root = root_of(repo)?;
84 if let Some(graph) = read_cache(&root) {
85 return Ok(Loaded {
86 graph,
87 root,
88 cached: true,
89 });
90 }
91 let share = Share::locate(repo.share.as_deref(), &root).ok();
92 let registry = registry();
93 let graph = build(&Inputs {
94 registry: registry.as_ref(),
95 share: share.as_ref().map(|s| s.dir()),
96 workflows: None,
97 bridged: Default::default(),
98 });
99 Ok(Loaded {
100 graph,
101 root,
102 cached: false,
103 })
104}
105
106pub fn fast_at(root: &Path, share: Option<&Path>) -> CommandGraph {
112 fast_with(root, share, None)
113}
114
115pub fn fast_with(
135 root: &Path,
136 share: Option<&Path>,
137 held: Option<&CapabilityRegistry>,
138) -> CommandGraph {
139 if let Some(graph) = read_cache(root) {
140 return graph;
141 }
142 match held {
143 Some(registry) => build(&Inputs {
144 registry: Some(registry),
145 share,
146 workflows: None,
147 bridged: Default::default(),
148 }),
149 None => {
150 let registry = registry();
151 build(&Inputs {
152 registry: registry.as_ref(),
153 share,
154 workflows: None,
155 bridged: Default::default(),
156 })
157 }
158 }
159}
160
161pub fn full_at(root: &Path, share: Option<&Path>) -> CommandGraph {
163 let registry = registry();
164 let catalogue = workflows::resolve(root);
165 build(&Inputs {
166 registry: registry.as_ref(),
167 share,
168 workflows: Some(&catalogue),
169 bridged: bridge::bridged(root),
170 })
171}
172
173pub fn root(repo: &RepoArgs) -> Result<PathBuf> {
175 root_of(repo)
176}
177
178fn root_of(repo: &RepoArgs) -> Result<PathBuf> {
180 let start = repo
181 .repo
182 .clone()
183 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
184 Ok(Repository::discover(&start)?.root().to_path_buf())
185}
186
187pub fn read_cache(root: &Path) -> Option<CommandGraph> {
193 let text = std::fs::read_to_string(root.join(bridge::DIRECTORY).join(CACHE)).ok()?;
194 let graph: CommandGraph = serde_json::from_str(&text).ok()?;
195 (graph.schema == super::SCHEMA).then_some(graph)
196}
197
198pub fn write_cache(root: &Path, graph: &CommandGraph) -> std::io::Result<()> {
200 let dir = root.join(bridge::DIRECTORY);
201 std::fs::create_dir_all(&dir)?;
202 let path = dir.join(CACHE);
203 let tmp = path.with_extension(format!("tmp.{}", std::process::id()));
204 std::fs::write(
205 &tmp,
206 serde_json::to_vec(graph).map_err(std::io::Error::other)?,
207 )?;
208 std::fs::rename(&tmp, &path)
209}
210
211pub fn cache_path(root: &Path) -> PathBuf {
213 root.join(bridge::DIRECTORY).join(CACHE)
214}
215
216pub fn missing_repository(e: Error) -> Error {
218 e
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn a_cache_round_trips_and_a_foreign_one_is_ignored() {
227 let dir = tempfile::tempdir().expect("a temporary directory");
228 let graph = build(&Inputs::default());
229 write_cache(dir.path(), &graph).expect("the cache is written");
230 let read = read_cache(dir.path()).expect("the cache is read back");
231 assert_eq!(read.fingerprint, graph.fingerprint);
232 assert_eq!(read.commands.len(), graph.commands.len());
233
234 std::fs::write(cache_path(dir.path()), br#"{"schema":"other/v9"}"#).unwrap();
235 assert!(read_cache(dir.path()).is_none());
236 }
237
238 #[test]
242 fn a_cold_graph_is_the_same_whoever_holds_the_registry() {
243 let dir = tempfile::tempdir().expect("a temporary directory");
244 let held = registry().expect("this executable's registry builds");
245 let built = fast_at(dir.path(), None);
246 assert!(
247 !built.commands.is_empty(),
248 "a cold graph is built, not empty"
249 );
250 assert_eq!(built, fast_with(dir.path(), None, Some(&held)));
251 assert!(
252 read_cache(dir.path()).is_none(),
253 "a fast read never writes the cache"
254 );
255
256 let cached = build(&Inputs::default());
257 write_cache(dir.path(), &cached).expect("the cache is written");
258 assert_eq!(fast_at(dir.path(), None), cached);
259 assert_eq!(fast_with(dir.path(), None, Some(&held)), cached);
260 }
261}