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