Skip to main content

alef_core/config/resolved/
fields.rs

1//! Field name resolution, serde strategy, path rewriting, and version methods.
2
3use std::cmp::Reverse;
4
5use super::ResolvedCrateConfig;
6use crate::config::extras::Language;
7
8impl ResolvedCrateConfig {
9    /// Resolve the binding field name for a given language, type, and field.
10    ///
11    /// Resolution order (highest to lowest priority):
12    /// 1. Per-language `rename_fields` map for the key `"TypeName.field_name"`.
13    /// 2. Automatic keyword escaping: if the field name is a reserved keyword in the target
14    ///    language, append `_` (e.g. `class` → `class_`).
15    /// 3. Original field name unchanged.
16    ///
17    /// Returns `Some(escaped_name)` when the field needs renaming, `None` when the original
18    /// name can be used as-is. Call sites that always need a `String` should use
19    /// `resolve_field_name(...).unwrap_or_else(|| field_name.to_string())`.
20    pub fn resolve_field_name(&self, lang: Language, type_name: &str, field_name: &str) -> Option<String> {
21        // 1. Explicit per-language rename_fields entry.
22        let explicit_key = format!("{type_name}.{field_name}");
23        let explicit = match lang {
24            Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
25            Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
26            Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
27            Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
28            Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
29            Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
30            Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
31            Language::Gleam => self.gleam.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
32            Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
33            Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
34            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
35            Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
36            Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
37            Language::Zig => self.zig.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
38            Language::Dart => self.dart.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
39            Language::Swift => self.swift.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
40            Language::Rust | Language::C => None,
41        };
42        if let Some(renamed) = explicit {
43            if renamed != field_name {
44                return Some(renamed.clone());
45            }
46            return None;
47        }
48
49        // 2. Automatic keyword escaping.
50        match lang {
51            Language::Python => crate::keywords::python_safe_name(field_name),
52            // Java and C# use PascalCase for field names — no conflict.
53            // Go uses PascalCase for exported fields — no conflict.
54            // JS/TS handles keyword escaping at the napi layer via js_name attributes.
55            _ => None,
56        }
57    }
58
59    /// Get the effective serde rename_all strategy for a given language.
60    ///
61    /// Resolution order:
62    /// 1. Per-language config override (`[python] serde_rename_all = "..."`)
63    /// 2. Language default:
64    ///    - camelCase: node, wasm, java, csharp
65    ///    - snake_case: all others
66    pub fn serde_rename_all_for_language(&self, lang: Language) -> String {
67        let override_val = match lang {
68            Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
69            Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
70            Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
71            Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
72            Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
73            Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
74            Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
75            Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
76            Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
77            Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
78            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
79            Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
80            Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
81            Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
82            Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
83            Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
84            Language::Rust | Language::C => None,
85        };
86
87        if let Some(val) = override_val {
88            return val.to_string();
89        }
90
91        match lang {
92            Language::Node | Language::Wasm | Language::Java | Language::Csharp => "camelCase".to_string(),
93            Language::Python
94            | Language::Ruby
95            | Language::Php
96            | Language::Go
97            | Language::Ffi
98            | Language::Elixir
99            | Language::R
100            | Language::Rust
101            | Language::Kotlin
102            | Language::Gleam
103            | Language::Zig
104            | Language::Swift
105            | Language::Dart
106            | Language::C => "snake_case".to_string(),
107        }
108    }
109
110    /// Rewrite a Rust path using `path_mappings`.
111    ///
112    /// Matches the longest prefix first so more-specific mappings take
113    /// priority over broader ones.
114    pub fn rewrite_path(&self, rust_path: &str) -> String {
115        let mut mappings: Vec<_> = self.path_mappings.iter().collect();
116        mappings.sort_by_key(|b| Reverse(b.0.len()));
117
118        for (from, to) in &mappings {
119            if rust_path.starts_with(from.as_str()) {
120                return format!("{}{}", to, &rust_path[from.len()..]);
121            }
122        }
123        rust_path.to_string()
124    }
125
126    /// Attempt to read the resolved version string from the configured `version_from` file.
127    ///
128    /// Returns `None` if the file cannot be read or the version cannot be found.
129    /// Checks `[workspace.package] version` first, then `[package] version`.
130    pub fn resolved_version(&self) -> Option<String> {
131        let content = std::fs::read_to_string(&self.version_from).ok()?;
132        let value: toml::Value = toml::from_str(&content).ok()?;
133        if let Some(v) = value
134            .get("workspace")
135            .and_then(|w| w.get("package"))
136            .and_then(|p| p.get("version"))
137            .and_then(|v| v.as_str())
138        {
139            return Some(v.to_string());
140        }
141        value
142            .get("package")
143            .and_then(|p| p.get("version"))
144            .and_then(|v| v.as_str())
145            .map(|v| v.to_string())
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::config::extras::Language;
152    use crate::config::new_config::NewAlefConfig;
153
154    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
155        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
156        cfg.resolve().unwrap().remove(0)
157    }
158
159    fn minimal() -> super::super::ResolvedCrateConfig {
160        resolved_one(
161            r#"
162[workspace]
163languages = ["python"]
164
165[[crates]]
166name = "test-lib"
167sources = ["src/lib.rs"]
168"#,
169        )
170    }
171
172    #[test]
173    fn serde_rename_all_python_defaults_to_snake_case() {
174        let r = minimal();
175        assert_eq!(r.serde_rename_all_for_language(Language::Python), "snake_case");
176    }
177
178    #[test]
179    fn serde_rename_all_node_defaults_to_camel_case() {
180        let r = resolved_one(
181            r#"
182[workspace]
183languages = ["node"]
184
185[[crates]]
186name = "test-lib"
187sources = ["src/lib.rs"]
188"#,
189        );
190        assert_eq!(r.serde_rename_all_for_language(Language::Node), "camelCase");
191    }
192
193    #[test]
194    fn serde_rename_all_java_defaults_to_camel_case() {
195        let r = resolved_one(
196            r#"
197[workspace]
198languages = ["java"]
199
200[[crates]]
201name = "test-lib"
202sources = ["src/lib.rs"]
203"#,
204        );
205        assert_eq!(r.serde_rename_all_for_language(Language::Java), "camelCase");
206    }
207
208    #[test]
209    fn serde_rename_all_per_language_override_wins() {
210        let r = resolved_one(
211            r#"
212[workspace]
213languages = ["python"]
214
215[[crates]]
216name = "test-lib"
217sources = ["src/lib.rs"]
218
219[crates.python]
220serde_rename_all = "camelCase"
221"#,
222        );
223        assert_eq!(r.serde_rename_all_for_language(Language::Python), "camelCase");
224    }
225
226    #[test]
227    fn resolved_resolve_field_name_keyword_escapes_python() {
228        use crate::keywords::python_safe_name;
229        let r = minimal();
230        // "class" is a Python keyword, should be escaped
231        let result = r.resolve_field_name(Language::Python, "MyType", "class");
232        assert_eq!(result, python_safe_name("class"));
233    }
234
235    #[test]
236    fn resolved_resolve_field_name_explicit_rename_wins_over_keyword_escape() {
237        let r = resolved_one(
238            r#"
239[workspace]
240languages = ["python"]
241
242[[crates]]
243name = "test-lib"
244sources = ["src/lib.rs"]
245
246[crates.python]
247rename_fields = { "MyType.class" = "klass" }
248"#,
249        );
250        let result = r.resolve_field_name(Language::Python, "MyType", "class");
251        assert_eq!(result, Some("klass".to_string()));
252    }
253
254    #[test]
255    fn resolved_resolve_field_name_non_keyword_returns_none() {
256        let r = minimal();
257        let result = r.resolve_field_name(Language::Python, "MyType", "my_field");
258        assert_eq!(result, None);
259    }
260
261    #[test]
262    fn rewrite_path_applies_longest_prefix_first() {
263        let r = resolved_one(
264            r#"
265[workspace]
266languages = ["python"]
267
268[[crates]]
269name = "test-lib"
270sources = ["src/lib.rs"]
271path_mappings = { "foo::bar" = "baz::qux", "foo" = "zzz" }
272"#,
273        );
274        // Longer prefix "foo::bar" wins over "foo"
275        assert_eq!(r.rewrite_path("foo::bar::Struct"), "baz::qux::Struct");
276        assert_eq!(r.rewrite_path("foo::Other"), "zzz::Other");
277        assert_eq!(r.rewrite_path("unrelated"), "unrelated");
278    }
279}