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