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