Skip to main content

alef_core/config/resolved/
lookups.rs

1//! Pipeline config lookups: lint, test, setup, update, clean, build, extras.
2
3use std::collections::HashMap;
4
5use super::ResolvedCrateConfig;
6use crate::config::extras::Language;
7use crate::config::output::{BuildCommandConfig, CleanConfig, LintConfig, SetupConfig, TestConfig, UpdateConfig};
8use crate::config::tools::LangContext;
9use crate::config::{build_defaults, clean_defaults, lint_defaults, setup_defaults, test_defaults, update_defaults};
10
11impl ResolvedCrateConfig {
12    /// Get the package output directory for a language.
13    /// Uses `scaffold_output` from per-language config if set, otherwise defaults.
14    pub fn package_dir(&self, lang: Language) -> String {
15        let override_path = match lang {
16            Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
17            Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
18            Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
19            Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
20            Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
21            _ => None,
22        };
23        if let Some(p) = override_path {
24            p.to_string_lossy().to_string()
25        } else {
26            match lang {
27                Language::Python => "packages/python".to_string(),
28                Language::Node => "packages/node".to_string(),
29                Language::Ruby => "packages/ruby".to_string(),
30                Language::Php => "packages/php".to_string(),
31                Language::Elixir => "packages/elixir".to_string(),
32                _ => format!("packages/{lang}"),
33            }
34        }
35    }
36
37    /// Get the run_wrapper for a language, if set.
38    pub fn run_wrapper_for_language(&self, lang: Language) -> Option<&str> {
39        match lang {
40            Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
41            Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
42            Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
43            Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
44            Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
45            Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
46            Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
47            Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
48            Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
49            Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
50            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.run_wrapper.as_deref()),
51            Language::Dart => self.dart.as_ref().and_then(|c| c.run_wrapper.as_deref()),
52            Language::Swift => self.swift.as_ref().and_then(|c| c.run_wrapper.as_deref()),
53            Language::Zig => self.zig.as_ref().and_then(|c| c.run_wrapper.as_deref()),
54            Language::Ffi | Language::Rust | Language::C => None,
55        }
56    }
57
58    /// Get the extra_lint_paths for a language.
59    pub fn extra_lint_paths_for_language(&self, lang: Language) -> &[String] {
60        match lang {
61            Language::Python => self
62                .python
63                .as_ref()
64                .map(|c| c.extra_lint_paths.as_slice())
65                .unwrap_or(&[]),
66            Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
67            Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
68            Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
69            Language::Elixir => self
70                .elixir
71                .as_ref()
72                .map(|c| c.extra_lint_paths.as_slice())
73                .unwrap_or(&[]),
74            Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
75            Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
76            Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
77            Language::Csharp => self
78                .csharp
79                .as_ref()
80                .map(|c| c.extra_lint_paths.as_slice())
81                .unwrap_or(&[]),
82            Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
83            Language::Kotlin => self
84                .kotlin
85                .as_ref()
86                .map(|c| c.extra_lint_paths.as_slice())
87                .unwrap_or(&[]),
88            Language::Dart => self.dart.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
89            Language::Swift => self
90                .swift
91                .as_ref()
92                .map(|c| c.extra_lint_paths.as_slice())
93                .unwrap_or(&[]),
94            Language::Zig => self.zig.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
95            Language::Ffi | Language::Rust | Language::C => &[],
96        }
97    }
98
99    /// Get the project_file for a language (Java or C# only).
100    pub fn project_file_for_language(&self, lang: Language) -> Option<&str> {
101        match lang {
102            Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
103            Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
104            _ => None,
105        }
106    }
107
108    /// Get the effective lint configuration for a language.
109    pub fn lint_config_for_language(&self, lang: Language) -> LintConfig {
110        let lang_str = lang.to_string();
111        if let Some(explicit) = self.lint.get(&lang_str) {
112            return explicit.clone();
113        }
114        let output_dir = self.package_dir(lang);
115        let run_wrapper = self.run_wrapper_for_language(lang);
116        let extra_lint_paths = self.extra_lint_paths_for_language(lang);
117        let project_file = self.project_file_for_language(lang);
118        let ctx = LangContext {
119            tools: &self.tools,
120            run_wrapper,
121            extra_lint_paths,
122            project_file,
123        };
124        lint_defaults::default_lint_config(lang, &output_dir, &ctx)
125    }
126
127    /// Get the effective update configuration for a language.
128    pub fn update_config_for_language(&self, lang: Language) -> UpdateConfig {
129        let lang_str = lang.to_string();
130        if let Some(explicit) = self.update.get(&lang_str) {
131            return explicit.clone();
132        }
133        let output_dir = self.package_dir(lang);
134        let ctx = LangContext {
135            tools: &self.tools,
136            run_wrapper: None,
137            extra_lint_paths: &[],
138            project_file: None,
139        };
140        update_defaults::default_update_config(lang, &output_dir, &ctx)
141    }
142
143    /// Get the effective test configuration for a language.
144    pub fn test_config_for_language(&self, lang: Language) -> TestConfig {
145        let lang_str = lang.to_string();
146        if let Some(explicit) = self.test.get(&lang_str) {
147            return explicit.clone();
148        }
149        let output_dir = self.package_dir(lang);
150        let run_wrapper = self.run_wrapper_for_language(lang);
151        let project_file = self.project_file_for_language(lang);
152        let ctx = LangContext {
153            tools: &self.tools,
154            run_wrapper,
155            extra_lint_paths: &[],
156            project_file,
157        };
158        test_defaults::default_test_config(lang, &output_dir, &ctx)
159    }
160
161    /// Get the effective setup configuration for a language.
162    pub fn setup_config_for_language(&self, lang: Language) -> SetupConfig {
163        let lang_str = lang.to_string();
164        if let Some(explicit) = self.setup.get(&lang_str) {
165            return explicit.clone();
166        }
167        let output_dir = self.package_dir(lang);
168        let ctx = LangContext {
169            tools: &self.tools,
170            run_wrapper: None,
171            extra_lint_paths: &[],
172            project_file: None,
173        };
174        setup_defaults::default_setup_config(lang, &output_dir, &ctx)
175    }
176
177    /// Get the effective clean configuration for a language.
178    pub fn clean_config_for_language(&self, lang: Language) -> CleanConfig {
179        let lang_str = lang.to_string();
180        if let Some(explicit) = self.clean.get(&lang_str) {
181            return explicit.clone();
182        }
183        let output_dir = self.package_dir(lang);
184        let ctx = LangContext {
185            tools: &self.tools,
186            run_wrapper: None,
187            extra_lint_paths: &[],
188            project_file: None,
189        };
190        clean_defaults::default_clean_config(lang, &output_dir, &ctx)
191    }
192
193    /// Get the effective build command configuration for a language.
194    pub fn build_command_config_for_language(&self, lang: Language) -> BuildCommandConfig {
195        let lang_str = lang.to_string();
196        if let Some(explicit) = self.build_commands.get(&lang_str) {
197            return explicit.clone();
198        }
199        let output_dir = self.package_dir(lang);
200        let run_wrapper = self.run_wrapper_for_language(lang);
201        let project_file = self.project_file_for_language(lang);
202        let ctx = LangContext {
203            tools: &self.tools,
204            run_wrapper,
205            extra_lint_paths: &[],
206            project_file,
207        };
208        build_defaults::default_build_config(lang, &output_dir, &self.name, &ctx)
209    }
210
211    /// Get the features to use for a specific language's binding crate.
212    pub fn features_for_language(&self, lang: Language) -> &[String] {
213        let override_features = match lang {
214            Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
215            Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
216            Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
217            Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
218            Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
219            Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
220            Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
221            Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
222            Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
223            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.features.as_deref()),
224            Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
225            Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
226            Language::Zig => self.zig.as_ref().and_then(|c| c.features.as_deref()),
227            Language::Dart => self.dart.as_ref().and_then(|c| c.features.as_deref()),
228            Language::Swift => self.swift.as_ref().and_then(|c| c.features.as_deref()),
229            Language::Rust | Language::C => None,
230        };
231        override_features.unwrap_or(&self.features)
232    }
233
234    /// Get the merged extra dependencies for a specific language's binding crate.
235    pub fn extra_deps_for_language(&self, lang: Language) -> HashMap<String, toml::Value> {
236        let mut deps = self.extra_dependencies.clone();
237        let lang_deps = match lang {
238            Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
239            Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
240            Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
241            Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
242            Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
243            Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
244            _ => None,
245        };
246        if let Some(lang_deps) = lang_deps {
247            deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
248        }
249        let exclude: &[String] = match lang {
250            Language::Wasm => self
251                .wasm
252                .as_ref()
253                .map(|c| c.exclude_extra_dependencies.as_slice())
254                .unwrap_or(&[]),
255            Language::Dart => self
256                .dart
257                .as_ref()
258                .map(|c| c.exclude_extra_dependencies.as_slice())
259                .unwrap_or(&[]),
260            Language::Swift => self
261                .swift
262                .as_ref()
263                .map(|c| c.exclude_extra_dependencies.as_slice())
264                .unwrap_or(&[]),
265            _ => &[],
266        };
267        for key in exclude {
268            deps.remove(key);
269        }
270        deps
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use crate::config::extras::Language;
277    use crate::config::new_config::NewAlefConfig;
278
279    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
280        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
281        cfg.resolve().unwrap().remove(0)
282    }
283
284    fn minimal() -> super::super::ResolvedCrateConfig {
285        resolved_one(
286            r#"
287[workspace]
288languages = ["python", "node"]
289
290[[crates]]
291name = "test-lib"
292sources = ["src/lib.rs"]
293"#,
294        )
295    }
296
297    #[test]
298    fn resolved_lint_config_inherits_workspace_when_crate_unset() {
299        let r = resolved_one(
300            r#"
301[workspace]
302languages = ["python"]
303
304[workspace.lint.python]
305check = "ruff check ."
306
307[[crates]]
308name = "test-lib"
309sources = ["src/lib.rs"]
310"#,
311        );
312        let lint = r.lint_config_for_language(Language::Python);
313        assert_eq!(lint.check.unwrap().commands(), vec!["ruff check ."]);
314    }
315
316    #[test]
317    fn resolved_lint_config_crate_overrides_workspace_field_wholesale() {
318        let r = resolved_one(
319            r#"
320[workspace]
321languages = ["python"]
322
323[workspace.lint.python]
324check = "ruff check ."
325
326[[crates]]
327name = "test-lib"
328sources = ["src/lib.rs"]
329
330[crates.lint.python]
331check = "ruff check crates/test-lib-py/"
332"#,
333        );
334        let lint = r.lint_config_for_language(Language::Python);
335        assert_eq!(lint.check.unwrap().commands(), vec!["ruff check crates/test-lib-py/"]);
336    }
337
338    #[test]
339    fn resolved_features_per_language_overrides_crate_default() {
340        let r = resolved_one(
341            r#"
342[workspace]
343languages = ["python"]
344
345[[crates]]
346name = "test-lib"
347sources = ["src/lib.rs"]
348features = ["base"]
349
350[crates.python]
351features = ["python-extra"]
352"#,
353        );
354        assert_eq!(r.features_for_language(Language::Python), &["python-extra"]);
355        assert_eq!(r.features_for_language(Language::Node), &["base"]);
356    }
357
358    #[test]
359    fn resolved_extra_deps_crate_value_wins_on_key_collision() {
360        let r = resolved_one(
361            r#"
362[workspace]
363languages = ["python"]
364
365[[crates]]
366name = "test-lib"
367sources = ["src/lib.rs"]
368
369[crates.extra_dependencies]
370tokio = "1"
371
372[crates.python]
373extra_dependencies = { tokio = "2" }
374"#,
375        );
376        let deps = r.extra_deps_for_language(Language::Python);
377        let tokio = deps.get("tokio").unwrap().as_str().unwrap();
378        assert_eq!(tokio, "2", "per-language extra_dep should win on collision");
379    }
380
381    #[test]
382    fn resolved_extra_deps_excludes_apply_after_merge() {
383        let r = resolved_one(
384            r#"
385[workspace]
386languages = ["wasm"]
387
388[[crates]]
389name = "test-lib"
390sources = ["src/lib.rs"]
391
392[crates.extra_dependencies]
393tokio = "1"
394serde = "1"
395
396[crates.wasm]
397exclude_extra_dependencies = ["tokio"]
398"#,
399        );
400        let deps = r.extra_deps_for_language(Language::Wasm);
401        assert!(!deps.contains_key("tokio"), "excluded dep should be absent");
402        assert!(deps.contains_key("serde"), "non-excluded dep should be present");
403    }
404
405    #[test]
406    fn package_dir_defaults_are_correct() {
407        let r = minimal();
408        assert_eq!(r.package_dir(Language::Python), "packages/python");
409        assert_eq!(r.package_dir(Language::Node), "packages/node");
410        assert_eq!(r.package_dir(Language::Ruby), "packages/ruby");
411        assert_eq!(r.package_dir(Language::Go), "packages/go");
412        assert_eq!(r.package_dir(Language::Java), "packages/java");
413    }
414}