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