1use 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 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 => 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 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 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 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 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 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 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 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 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 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 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 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 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}