Skip to main content

aptos_sdk/codegen/
types.rs

1//! Type mapping between Move and Rust types.
2
3use std::collections::HashMap;
4
5/// A Rust type representation.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct RustType {
8    /// The full type path (e.g., "`Vec<u8>`", "`AccountAddress`").
9    pub path: String,
10    /// Whether this type requires BCS serialization as an argument.
11    pub needs_bcs: bool,
12    /// Whether this type is a reference.
13    pub is_ref: bool,
14    /// Documentation for this type.
15    pub doc: Option<String>,
16}
17
18impl RustType {
19    /// Creates a new Rust type.
20    #[must_use]
21    pub fn new(path: impl Into<String>) -> Self {
22        Self {
23            path: path.into(),
24            needs_bcs: true,
25            is_ref: false,
26            doc: None,
27        }
28    }
29
30    /// Creates a type that doesn't need BCS serialization.
31    #[must_use]
32    pub fn primitive(path: impl Into<String>) -> Self {
33        Self {
34            path: path.into(),
35            needs_bcs: false,
36            is_ref: false,
37            doc: None,
38        }
39    }
40
41    /// Creates a reference type.
42    #[must_use]
43    pub fn reference(mut self) -> Self {
44        self.is_ref = true;
45        self
46    }
47
48    /// Adds documentation.
49    #[must_use]
50    pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
51        self.doc = Some(doc.into());
52        self
53    }
54
55    /// Returns the type as a function argument type.
56    pub fn as_arg_type(&self) -> String {
57        if self.is_ref {
58            format!("&{}", self.path)
59        } else {
60            self.path.clone()
61        }
62    }
63
64    /// Returns the type as a return type.
65    pub fn as_return_type(&self) -> String {
66        self.path.clone()
67    }
68}
69
70/// Maps Move types to Rust types.
71#[derive(Debug, Clone)]
72pub struct MoveTypeMapper {
73    /// Custom type mappings.
74    custom_mappings: HashMap<String, RustType>,
75}
76
77impl Default for MoveTypeMapper {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl MoveTypeMapper {
84    /// Creates a new type mapper with default mappings.
85    pub fn new() -> Self {
86        Self {
87            custom_mappings: HashMap::new(),
88        }
89    }
90
91    /// Adds a custom type mapping.
92    pub fn add_mapping(&mut self, move_type: impl Into<String>, rust_type: RustType) {
93        self.custom_mappings.insert(move_type.into(), rust_type);
94    }
95
96    /// Maps a Move type string to a Rust type.
97    pub fn map_type(&self, move_type: &str) -> RustType {
98        // Check custom mappings first
99        if let Some(rust_type) = self.custom_mappings.get(move_type) {
100            return rust_type.clone();
101        }
102
103        // Handle primitive types
104        match move_type {
105            "bool" => RustType::primitive("bool"),
106            "u8" => RustType::primitive("u8"),
107            "u16" => RustType::primitive("u16"),
108            "u32" => RustType::primitive("u32"),
109            "u64" => RustType::primitive("u64"),
110            "u128" => RustType::primitive("u128"),
111            // Move `u256` has no primitive Rust counterpart, so it maps to the
112            // SDK's `MoveU256` value type (imported from `aptos_sdk::transaction`),
113            // which round-trips in both BCS (32-byte little-endian) and JSON
114            // (decimal string, matching the Aptos API encoding).
115            "u256" => RustType::new("MoveU256"),
116            "address" => RustType::new("AccountAddress"),
117            "signer" | "&signer" => RustType::new("AccountAddress")
118                .with_doc("Signer address (automatically set to sender)"),
119            _ => self.map_complex_type(move_type),
120        }
121    }
122
123    /// Maps complex Move types (vectors, structs, etc.)
124    fn map_complex_type(&self, move_type: &str) -> RustType {
125        // Handle vector types
126        if move_type.starts_with("vector<") && move_type.ends_with('>') {
127            let inner = &move_type[7..move_type.len() - 1];
128            let inner_type = self.map_type(inner);
129
130            // Special case: `vector<u8>` -> `Vec<u8>` (bytes)
131            if inner == "u8" {
132                return RustType::new("Vec<u8>").with_doc("Bytes");
133            }
134
135            return RustType::new(format!("Vec<{}>", inner_type.path));
136        }
137
138        // Handle Option types (0x1::option::Option<T>)
139        if move_type.contains("::option::Option<")
140            && let Some(start) = move_type.find("Option<")
141        {
142            let rest = &move_type[start + 7..];
143            if let Some(end) = rest.rfind('>') {
144                let inner = &rest[..end];
145                let inner_type = self.map_type(inner);
146                return RustType::new(format!("Option<{}>", inner_type.path));
147            }
148        }
149
150        // Handle String type
151        if move_type == "0x1::string::String" || move_type.ends_with("::string::String") {
152            return RustType::new("String");
153        }
154
155        // Handle Object types
156        if move_type.contains("::object::Object<") {
157            return RustType::new("AccountAddress").with_doc("Object address");
158        }
159
160        // Handle generic struct types (e.g., 0x1::coin::Coin<0x1::aptos_coin::AptosCoin>)
161        if move_type.contains("::") {
162            // Use rsplit to avoid collecting into Vec - we only need the last part
163            let part_count = move_type.matches("::").count() + 1;
164            if part_count >= 3 {
165                // Get the base struct name (without generics) using rsplit
166                if let Some(struct_name) = move_type.rsplit("::").next() {
167                    let base_name = struct_name.split('<').next().unwrap_or(struct_name);
168
169                    // Create a pascal case name
170                    let rust_name = to_pascal_case(base_name);
171                    return RustType::new(rust_name).with_doc(format!("Move type: {move_type}"));
172                }
173            }
174        }
175
176        // Default: use serde_json::Value for unknown types
177        RustType::new("serde_json::Value").with_doc(format!("Unknown Move type: {move_type}"))
178    }
179
180    /// Maps a Move type to a BCS argument encoding expression.
181    pub fn to_bcs_arg(&self, move_type: &str, var_name: &str) -> String {
182        let rust_type = self.map_type(move_type);
183
184        if !rust_type.needs_bcs {
185            // Primitives that don't need special handling
186            return format!(
187                "::aptos_sdk::aptos_bcs::to_bytes(&{var_name}).map_err(|e| AptosError::Bcs(e.to_string()))?"
188            );
189        }
190
191        // SECURITY: Use error propagation instead of .unwrap() to prevent
192        // panics in generated code if BCS serialization fails.
193        format!(
194            "::aptos_sdk::aptos_bcs::to_bytes(&{var_name}).map_err(|e| AptosError::Bcs(e.to_string()))?"
195        )
196    }
197
198    /// Determines if a parameter should be excluded from the function signature.
199    /// (e.g., &signer is always the sender)
200    pub fn is_signer_param(&self, move_type: &str) -> bool {
201        move_type == "&signer" || move_type == "signer"
202    }
203}
204
205/// Converts a `snake_case` or other string to `PascalCase`.
206pub fn to_pascal_case(s: &str) -> String {
207    let mut result = String::new();
208    let mut capitalize_next = true;
209
210    for c in s.chars() {
211        if c == '_' || c == '-' || c == ' ' {
212            capitalize_next = true;
213        } else if capitalize_next {
214            result.push(c.to_ascii_uppercase());
215            capitalize_next = false;
216        } else {
217            result.push(c);
218        }
219    }
220
221    result
222}
223
224/// Converts a `PascalCase` or other string to `snake_case`.
225pub fn to_snake_case(s: &str) -> String {
226    let mut result = String::new();
227
228    for (i, c) in s.chars().enumerate() {
229        if c.is_ascii_uppercase() {
230            if i > 0 {
231                result.push('_');
232            }
233            result.push(c.to_ascii_lowercase());
234        } else {
235            result.push(c);
236        }
237    }
238
239    result
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_primitive_mapping() {
248        let mapper = MoveTypeMapper::new();
249
250        assert_eq!(mapper.map_type("bool").path, "bool");
251        assert_eq!(mapper.map_type("u8").path, "u8");
252        assert_eq!(mapper.map_type("u64").path, "u64");
253        assert_eq!(mapper.map_type("u128").path, "u128");
254        assert_eq!(mapper.map_type("address").path, "AccountAddress");
255    }
256
257    #[test]
258    fn test_vector_mapping() {
259        let mapper = MoveTypeMapper::new();
260
261        assert_eq!(mapper.map_type("vector<u8>").path, "Vec<u8>");
262        assert_eq!(
263            mapper.map_type("vector<address>").path,
264            "Vec<AccountAddress>"
265        );
266        assert_eq!(mapper.map_type("vector<u64>").path, "Vec<u64>");
267    }
268
269    #[test]
270    fn test_string_mapping() {
271        let mapper = MoveTypeMapper::new();
272
273        assert_eq!(mapper.map_type("0x1::string::String").path, "String");
274    }
275
276    #[test]
277    fn test_to_pascal_case() {
278        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
279        assert_eq!(to_pascal_case("coin"), "Coin");
280        assert_eq!(to_pascal_case("aptos_coin"), "AptosCoin");
281    }
282
283    #[test]
284    fn test_to_snake_case() {
285        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
286        assert_eq!(to_snake_case("Coin"), "coin");
287        assert_eq!(to_snake_case("AptosCoin"), "aptos_coin");
288    }
289
290    #[test]
291    fn test_rust_type_new() {
292        let rt = RustType::new("MyType");
293        assert_eq!(rt.path, "MyType");
294        assert!(rt.needs_bcs);
295        assert!(!rt.is_ref);
296        assert!(rt.doc.is_none());
297    }
298
299    #[test]
300    fn test_rust_type_primitive() {
301        let rt = RustType::primitive("u64");
302        assert_eq!(rt.path, "u64");
303        assert!(!rt.needs_bcs);
304    }
305
306    #[test]
307    fn test_rust_type_reference() {
308        let rt = RustType::new("MyType").reference();
309        assert!(rt.is_ref);
310        assert_eq!(rt.as_arg_type(), "&MyType");
311    }
312
313    #[test]
314    fn test_rust_type_with_doc() {
315        let rt = RustType::new("MyType").with_doc("My documentation");
316        assert_eq!(rt.doc, Some("My documentation".to_string()));
317    }
318
319    #[test]
320    fn test_rust_type_as_return_type() {
321        let rt = RustType::new("MyType").reference();
322        assert_eq!(rt.as_return_type(), "MyType"); // References don't affect return type
323    }
324
325    #[test]
326    fn test_mapper_default() {
327        let mapper = MoveTypeMapper::default();
328        assert_eq!(mapper.map_type("bool").path, "bool");
329    }
330
331    #[test]
332    fn test_mapper_custom_mapping() {
333        let mut mapper = MoveTypeMapper::new();
334        mapper.add_mapping("MyCustomType", RustType::new("CustomRustType"));
335        assert_eq!(mapper.map_type("MyCustomType").path, "CustomRustType");
336    }
337
338    #[test]
339    fn test_mapper_u256() {
340        // Move u256 has no primitive Rust counterpart, so it maps to the SDK's
341        // `MoveU256` value type, which round-trips in both BCS and JSON.
342        let mapper = MoveTypeMapper::new();
343        assert_eq!(mapper.map_type("u256").path, "MoveU256");
344    }
345
346    #[test]
347    fn test_mapper_signer() {
348        let mapper = MoveTypeMapper::new();
349        assert_eq!(mapper.map_type("&signer").path, "AccountAddress");
350        assert_eq!(mapper.map_type("signer").path, "AccountAddress");
351    }
352
353    #[test]
354    fn test_mapper_nested_vector() {
355        let mapper = MoveTypeMapper::new();
356        let result = mapper.map_type("vector<vector<u8>>");
357        assert_eq!(result.path, "Vec<Vec<u8>>");
358    }
359
360    #[test]
361    fn test_mapper_option_type() {
362        let mapper = MoveTypeMapper::new();
363        let result = mapper.map_type("0x1::option::Option<u64>");
364        assert_eq!(result.path, "Option<u64>");
365    }
366
367    #[test]
368    fn test_mapper_object_type() {
369        let mapper = MoveTypeMapper::new();
370        let result = mapper.map_type("0x1::object::Object<Token>");
371        assert_eq!(result.path, "AccountAddress");
372    }
373
374    #[test]
375    fn test_mapper_unknown_struct() {
376        let mapper = MoveTypeMapper::new();
377        let result = mapper.map_type("0x1::module::SomeStruct");
378        assert!(result.doc.is_some());
379    }
380
381    #[test]
382    fn test_mapper_unknown_type() {
383        let mapper = MoveTypeMapper::new();
384        let result = mapper.map_type("some_completely_unknown_thing");
385        assert_eq!(result.path, "serde_json::Value");
386    }
387
388    #[test]
389    fn test_to_bcs_arg_address() {
390        let mapper = MoveTypeMapper::new();
391        let result = mapper.to_bcs_arg("address", "my_addr");
392        assert!(result.contains("aptos_bcs::to_bytes"));
393        assert!(result.contains("my_addr"));
394    }
395
396    #[test]
397    fn test_to_bcs_arg_vector_u8() {
398        let mapper = MoveTypeMapper::new();
399        let result = mapper.to_bcs_arg("vector<u8>", "my_bytes");
400        assert!(result.contains("aptos_bcs::to_bytes"));
401    }
402
403    #[test]
404    fn test_to_bcs_arg_vector_other() {
405        let mapper = MoveTypeMapper::new();
406        let result = mapper.to_bcs_arg("vector<u64>", "my_vec");
407        assert!(result.contains("aptos_bcs::to_bytes"));
408    }
409
410    #[test]
411    fn test_to_bcs_arg_string() {
412        let mapper = MoveTypeMapper::new();
413        let result = mapper.to_bcs_arg("0x1::string::String", "my_string");
414        assert!(result.contains("aptos_bcs::to_bytes"));
415    }
416
417    #[test]
418    fn test_to_bcs_arg_other_string() {
419        let mapper = MoveTypeMapper::new();
420        let result = mapper.to_bcs_arg("0xabc::my_module::string::String", "s");
421        assert!(result.contains("aptos_bcs::to_bytes"));
422    }
423
424    #[test]
425    fn test_is_signer_param() {
426        let mapper = MoveTypeMapper::new();
427        assert!(mapper.is_signer_param("&signer"));
428        assert!(mapper.is_signer_param("signer"));
429        assert!(!mapper.is_signer_param("address"));
430        assert!(!mapper.is_signer_param("u64"));
431    }
432
433    #[test]
434    fn test_to_pascal_case_with_spaces() {
435        assert_eq!(to_pascal_case("hello world"), "HelloWorld");
436    }
437
438    #[test]
439    fn test_to_pascal_case_with_dashes() {
440        assert_eq!(to_pascal_case("hello-world"), "HelloWorld");
441    }
442
443    #[test]
444    fn test_to_snake_case_single_word() {
445        assert_eq!(to_snake_case("hello"), "hello");
446    }
447
448    #[test]
449    fn test_to_snake_case_already_lowercase() {
450        assert_eq!(to_snake_case("helloworld"), "helloworld");
451    }
452}