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