1use anyhow::Result;
2use camino::Utf8Path;
3use weaveffi_ir::ir::Api;
4
5use crate::cache;
6use crate::config::GeneratorConfig;
7
8pub trait Generator {
9 fn name(&self) -> &'static str;
10 fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()>;
11
12 fn generate_with_config(
13 &self,
14 api: &Api,
15 out_dir: &Utf8Path,
16 _config: &GeneratorConfig,
17 ) -> Result<()> {
18 self.generate(api, out_dir)
19 }
20
21 fn output_files(&self, _api: &Api, _out_dir: &Utf8Path) -> Vec<String> {
22 vec![]
23 }
24}
25
26#[derive(Default)]
27pub struct Orchestrator<'a> {
28 generators: Vec<&'a dyn Generator>,
29}
30
31impl<'a> Orchestrator<'a> {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn with_generator(mut self, gen: &'a dyn Generator) -> Self {
37 self.generators.push(gen);
38 self
39 }
40
41 pub fn run(
42 &self,
43 api: &Api,
44 out_dir: &Utf8Path,
45 config: &GeneratorConfig,
46 force: bool,
47 ) -> Result<()> {
48 let hash = cache::hash_api(api);
49
50 if !force {
51 if let Some(cached) = cache::read_cache(out_dir) {
52 if cached == hash {
53 println!("No changes detected, skipping code generation.");
54 return Ok(());
55 }
56 }
57 }
58
59 for g in &self.generators {
60 g.generate_with_config(api, out_dir, config)?;
61 }
62
63 cache::write_cache(out_dir, &hash)?;
64 Ok(())
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::sync::atomic::{AtomicUsize, Ordering};
72 use std::sync::Arc;
73 use weaveffi_ir::ir::{Function, Module, Param, TypeRef};
74
75 struct CountingGenerator {
76 calls: Arc<AtomicUsize>,
77 }
78
79 impl Generator for CountingGenerator {
80 fn name(&self) -> &'static str {
81 "counting"
82 }
83
84 fn generate(&self, _api: &Api, out_dir: &Utf8Path) -> Result<()> {
85 self.calls.fetch_add(1, Ordering::SeqCst);
86 std::fs::write(out_dir.join("output.txt").as_std_path(), "generated")?;
87 Ok(())
88 }
89 }
90
91 fn test_api() -> Api {
92 Api {
93 version: "0.1.0".to_string(),
94 modules: vec![Module {
95 name: "math".to_string(),
96 functions: vec![Function {
97 name: "add".to_string(),
98 params: vec![
99 Param {
100 name: "a".to_string(),
101 ty: TypeRef::I32,
102 },
103 Param {
104 name: "b".to_string(),
105 ty: TypeRef::I32,
106 },
107 ],
108 returns: Some(TypeRef::I32),
109 doc: None,
110 r#async: false,
111 }],
112 structs: vec![],
113 enums: vec![],
114 errors: None,
115 }],
116 }
117 }
118
119 #[test]
120 fn incremental_skips_when_unchanged() {
121 let dir = tempfile::tempdir().unwrap();
122 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
123 let api = test_api();
124 let config = GeneratorConfig::default();
125 let calls = Arc::new(AtomicUsize::new(0));
126 let gen = CountingGenerator {
127 calls: Arc::clone(&calls),
128 };
129
130 let orch = Orchestrator::new().with_generator(&gen);
131
132 orch.run(&api, out_dir, &config, false).unwrap();
133 assert_eq!(calls.load(Ordering::SeqCst), 1);
134 let content_after_first = std::fs::read_to_string(out_dir.join("output.txt")).unwrap();
135
136 orch.run(&api, out_dir, &config, false).unwrap();
137 assert_eq!(
138 calls.load(Ordering::SeqCst),
139 1,
140 "generator should not run again"
141 );
142 let content_after_second = std::fs::read_to_string(out_dir.join("output.txt")).unwrap();
143
144 assert_eq!(content_after_first, content_after_second);
145 }
146
147 #[test]
148 fn force_bypasses_cache() {
149 let dir = tempfile::tempdir().unwrap();
150 let out_dir = Utf8Path::from_path(dir.path()).unwrap();
151 let api = test_api();
152 let config = GeneratorConfig::default();
153 let calls = Arc::new(AtomicUsize::new(0));
154 let gen = CountingGenerator {
155 calls: Arc::clone(&calls),
156 };
157
158 let orch = Orchestrator::new().with_generator(&gen);
159
160 orch.run(&api, out_dir, &config, false).unwrap();
161 assert_eq!(calls.load(Ordering::SeqCst), 1);
162
163 orch.run(&api, out_dir, &config, true).unwrap();
164 assert_eq!(calls.load(Ordering::SeqCst), 2, "force should bypass cache");
165 }
166}