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 => 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 => &[],
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 => 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            _ => None,
262        };
263        if let Some(lang_deps) = lang_deps {
264            deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
265        }
266        let exclude: &[String] = match lang {
267            Language::Wasm => self
268                .wasm
269                .as_ref()
270                .map(|c| c.exclude_extra_dependencies.as_slice())
271                .unwrap_or(&[]),
272            Language::Dart => self
273                .dart
274                .as_ref()
275                .map(|c| c.exclude_extra_dependencies.as_slice())
276                .unwrap_or(&[]),
277            Language::Swift => self
278                .swift
279                .as_ref()
280                .map(|c| c.exclude_extra_dependencies.as_slice())
281                .unwrap_or(&[]),
282            _ => &[],
283        };
284        for key in exclude {
285            deps.remove(key);
286        }
287        deps
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use crate::config::extras::Language;
294    use crate::config::new_config::NewAlefConfig;
295
296    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
297        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
298        cfg.resolve().unwrap().remove(0)
299    }
300
301    fn minimal() -> super::super::ResolvedCrateConfig {
302        resolved_one(
303            r#"
304[workspace]
305languages = ["python", "node"]
306
307[[crates]]
308name = "test-lib"
309sources = ["src/lib.rs"]
310"#,
311        )
312    }
313
314    #[test]
315    fn resolved_lint_config_inherits_workspace_when_crate_unset() {
316        let r = resolved_one(
317            r#"
318[workspace]
319languages = ["python"]
320
321[workspace.lint.python]
322check = "ruff check ."
323
324[[crates]]
325name = "test-lib"
326sources = ["src/lib.rs"]
327"#,
328        );
329        let lint = r.lint_config_for_language(Language::Python);
330        assert_eq!(lint.check.unwrap().commands(), vec!["ruff check ."]);
331    }
332
333    #[test]
334    fn resolved_lint_config_crate_overrides_workspace_field_wholesale() {
335        let r = resolved_one(
336            r#"
337[workspace]
338languages = ["python"]
339
340[workspace.lint.python]
341check = "ruff check ."
342
343[[crates]]
344name = "test-lib"
345sources = ["src/lib.rs"]
346
347[crates.lint.python]
348check = "ruff check crates/test-lib-py/"
349"#,
350        );
351        let lint = r.lint_config_for_language(Language::Python);
352        assert_eq!(lint.check.unwrap().commands(), vec!["ruff check crates/test-lib-py/"]);
353    }
354
355    #[test]
356    fn resolved_features_per_language_overrides_crate_default() {
357        let r = resolved_one(
358            r#"
359[workspace]
360languages = ["python"]
361
362[[crates]]
363name = "test-lib"
364sources = ["src/lib.rs"]
365features = ["base"]
366
367[crates.python]
368features = ["python-extra"]
369"#,
370        );
371        assert_eq!(r.features_for_language(Language::Python), &["python-extra"]);
372        assert_eq!(r.features_for_language(Language::Node), &["base"]);
373    }
374
375    #[test]
376    fn resolved_extra_deps_crate_value_wins_on_key_collision() {
377        let r = resolved_one(
378            r#"
379[workspace]
380languages = ["python"]
381
382[[crates]]
383name = "test-lib"
384sources = ["src/lib.rs"]
385
386[crates.extra_dependencies]
387tokio = "1"
388
389[crates.python]
390extra_dependencies = { tokio = "2" }
391"#,
392        );
393        let deps = r.extra_deps_for_language(Language::Python);
394        let tokio = deps.get("tokio").unwrap().as_str().unwrap();
395        assert_eq!(tokio, "2", "per-language extra_dep should win on collision");
396    }
397
398    #[test]
399    fn resolved_extra_deps_excludes_apply_after_merge() {
400        let r = resolved_one(
401            r#"
402[workspace]
403languages = ["wasm"]
404
405[[crates]]
406name = "test-lib"
407sources = ["src/lib.rs"]
408
409[crates.extra_dependencies]
410tokio = "1"
411serde = "1"
412
413[crates.wasm]
414exclude_extra_dependencies = ["tokio"]
415"#,
416        );
417        let deps = r.extra_deps_for_language(Language::Wasm);
418        assert!(!deps.contains_key("tokio"), "excluded dep should be absent");
419        assert!(deps.contains_key("serde"), "non-excluded dep should be present");
420    }
421
422    #[test]
423    fn package_dir_defaults_are_correct() {
424        let r = minimal();
425        assert_eq!(r.package_dir(Language::Python), "packages/python");
426        assert_eq!(r.package_dir(Language::Node), "packages/node");
427        assert_eq!(r.package_dir(Language::Ruby), "packages/ruby");
428        assert_eq!(r.package_dir(Language::Go), "packages/go");
429        assert_eq!(r.package_dir(Language::Java), "packages/java");
430    }
431}