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::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
32            Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
33            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
34            Language::KotlinAndroid => self
35                .kotlin_android
36                .as_ref()
37                .and_then(|c| c.rename_fields.get(&explicit_key)),
38            Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
39            Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
40            Language::Zig => self.zig.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
41            Language::Dart => self.dart.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
42            Language::Swift => self.swift.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
43            Language::Gleam => self.gleam.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
44            Language::Rust | Language::C | Language::Jni => None,
45        };
46        if let Some(renamed) = explicit {
47            if renamed != field_name {
48                return Some(renamed.clone());
49            }
50            return None;
51        }
52
53        // 2. Automatic keyword escaping.
54        match lang {
55            Language::Python => crate::keywords::python_safe_name(field_name),
56            // Java and C# use PascalCase for field names — no conflict.
57            // Go uses PascalCase for exported fields — no conflict.
58            // JS/TS handles keyword escaping at the napi layer via js_name attributes.
59            _ => None,
60        }
61    }
62
63    /// Get the effective serde rename_all strategy for a given language.
64    ///
65    /// Resolution order:
66    /// 1. Per-language config override (`[python] serde_rename_all = "..."`)
67    /// 2. Language default (idiomatic per-language JSON wire convention):
68    ///    - camelCase: node, wasm, java, csharp, php, kotlin, swift, dart
69    ///    - snake_case: python, ruby, go, ffi, elixir, r, rust, zig, c
70    pub fn serde_rename_all_for_language(&self, lang: Language) -> String {
71        let override_val = match lang {
72            Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
73            Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
74            Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
75            Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
76            Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
77            Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
78            Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
79            Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
80            Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
81            Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
82            Language::KotlinAndroid => self.kotlin_android.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
83            Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
84            Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
85            Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
86            Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
87            Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
88            Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
89            Language::Rust | Language::C | Language::Jni => None,
90        };
91
92        if let Some(val) = override_val {
93            return val.to_string();
94        }
95
96        match lang {
97            Language::Node
98            | Language::Wasm
99            | Language::Java
100            | Language::Csharp
101            | Language::Php
102            | Language::Kotlin
103            | Language::KotlinAndroid
104            | Language::Swift
105            | Language::Dart => "camelCase".to_string(),
106            Language::Python
107            | Language::Ruby
108            | Language::Go
109            | Language::Ffi
110            | Language::Elixir
111            | Language::R
112            | Language::Rust
113            | Language::Gleam
114            | Language::Zig
115            | Language::C
116            | Language::Jni => "snake_case".to_string(),
117        }
118    }
119
120    /// Rewrite a Rust path using `path_mappings`.
121    ///
122    /// Matches the longest prefix first so more-specific mappings take
123    /// priority over broader ones.
124    pub fn rewrite_path(&self, rust_path: &str) -> String {
125        let mut mappings: Vec<_> = self.path_mappings.iter().collect();
126        mappings.sort_by_key(|b| Reverse(b.0.len()));
127
128        for (from, to) in &mappings {
129            if rust_path.starts_with(from.as_str()) {
130                return format!("{}{}", to, &rust_path[from.len()..]);
131            }
132        }
133        rust_path.to_string()
134    }
135
136    /// Collect all associated type names declared across every configured trait bridge.
137    ///
138    /// Returns the union of [`crate::config::TraitBridgeConfig::associated_type_names`]
139    /// for all bridges. Backends use this set to skip generic record/enum codegen for
140    /// these types, deferring to visitor-specific generators instead.
141    pub fn bridge_associated_types(&self) -> std::collections::HashSet<String> {
142        let mut set = std::collections::HashSet::new();
143        for bridge in &self.trait_bridges {
144            for name in bridge.associated_type_names() {
145                set.insert(name.to_string());
146            }
147        }
148        set
149    }
150
151    /// Attempt to read the resolved version string from the configured `version_from` file.
152    ///
153    /// Returns `None` if the file cannot be read or the version cannot be found.
154    /// Checks `[workspace.package] version` first, then `[package] version`.
155    pub fn resolved_version(&self) -> Option<String> {
156        let content = std::fs::read_to_string(&self.version_from).ok()?;
157        let value: toml::Value = toml::from_str(&content).ok()?;
158        if let Some(v) = value
159            .get("workspace")
160            .and_then(|w| w.get("package"))
161            .and_then(|p| p.get("version"))
162            .and_then(|v| v.as_str())
163        {
164            return Some(v.to_string());
165        }
166        value
167            .get("package")
168            .and_then(|p| p.get("version"))
169            .and_then(|v| v.as_str())
170            .map(|v| v.to_string())
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use crate::config::extras::Language;
177    use crate::config::new_config::NewAlefConfig;
178
179    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
180        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
181        cfg.resolve().unwrap().remove(0)
182    }
183
184    fn minimal() -> super::super::ResolvedCrateConfig {
185        resolved_one(
186            r#"
187[workspace]
188languages = ["python"]
189
190[[crates]]
191name = "test-lib"
192sources = ["src/lib.rs"]
193"#,
194        )
195    }
196
197    #[test]
198    fn serde_rename_all_python_defaults_to_snake_case() {
199        let r = minimal();
200        assert_eq!(r.serde_rename_all_for_language(Language::Python), "snake_case");
201    }
202
203    #[test]
204    fn serde_rename_all_node_defaults_to_camel_case() {
205        let r = resolved_one(
206            r#"
207[workspace]
208languages = ["node"]
209
210[[crates]]
211name = "test-lib"
212sources = ["src/lib.rs"]
213"#,
214        );
215        assert_eq!(r.serde_rename_all_for_language(Language::Node), "camelCase");
216    }
217
218    #[test]
219    fn serde_rename_all_java_defaults_to_camel_case() {
220        let r = resolved_one(
221            r#"
222[workspace]
223languages = ["java"]
224
225[[crates]]
226name = "test-lib"
227sources = ["src/lib.rs"]
228"#,
229        );
230        assert_eq!(r.serde_rename_all_for_language(Language::Java), "camelCase");
231    }
232
233    #[test]
234    fn serde_rename_all_per_language_override_wins() {
235        let r = resolved_one(
236            r#"
237[workspace]
238languages = ["python"]
239
240[[crates]]
241name = "test-lib"
242sources = ["src/lib.rs"]
243
244[crates.python]
245serde_rename_all = "camelCase"
246"#,
247        );
248        assert_eq!(r.serde_rename_all_for_language(Language::Python), "camelCase");
249    }
250
251    #[test]
252    fn resolved_resolve_field_name_keyword_escapes_python() {
253        use crate::keywords::python_safe_name;
254        let r = minimal();
255        // "class" is a Python keyword, should be escaped
256        let result = r.resolve_field_name(Language::Python, "MyType", "class");
257        assert_eq!(result, python_safe_name("class"));
258    }
259
260    #[test]
261    fn resolved_resolve_field_name_explicit_rename_wins_over_keyword_escape() {
262        let r = resolved_one(
263            r#"
264[workspace]
265languages = ["python"]
266
267[[crates]]
268name = "test-lib"
269sources = ["src/lib.rs"]
270
271[crates.python]
272rename_fields = { "MyType.class" = "klass" }
273"#,
274        );
275        let result = r.resolve_field_name(Language::Python, "MyType", "class");
276        assert_eq!(result, Some("klass".to_string()));
277    }
278
279    #[test]
280    fn resolved_resolve_field_name_non_keyword_returns_none() {
281        let r = minimal();
282        let result = r.resolve_field_name(Language::Python, "MyType", "my_field");
283        assert_eq!(result, None);
284    }
285
286    #[test]
287    fn rewrite_path_applies_longest_prefix_first() {
288        let r = resolved_one(
289            r#"
290[workspace]
291languages = ["python"]
292
293[[crates]]
294name = "test-lib"
295sources = ["src/lib.rs"]
296path_mappings = { "foo::bar" = "baz::qux", "foo" = "zzz" }
297"#,
298        );
299        // Longer prefix "foo::bar" wins over "foo"
300        assert_eq!(r.rewrite_path("foo::bar::Struct"), "baz::qux::Struct");
301        assert_eq!(r.rewrite_path("foo::Other"), "zzz::Other");
302        assert_eq!(r.rewrite_path("unrelated"), "unrelated");
303    }
304}