use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RustType {
pub path: String,
pub needs_bcs: bool,
pub is_ref: bool,
pub doc: Option<String>,
}
impl RustType {
#[must_use]
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
needs_bcs: true,
is_ref: false,
doc: None,
}
}
#[must_use]
pub fn primitive(path: impl Into<String>) -> Self {
Self {
path: path.into(),
needs_bcs: false,
is_ref: false,
doc: None,
}
}
#[must_use]
pub fn reference(mut self) -> Self {
self.is_ref = true;
self
}
#[must_use]
pub fn with_doc(mut self, doc: impl Into<String>) -> Self {
self.doc = Some(doc.into());
self
}
pub fn as_arg_type(&self) -> String {
if self.is_ref {
format!("&{}", self.path)
} else {
self.path.clone()
}
}
pub fn as_return_type(&self) -> String {
self.path.clone()
}
}
#[derive(Debug, Clone)]
pub struct MoveTypeMapper {
custom_mappings: HashMap<String, RustType>,
}
impl Default for MoveTypeMapper {
fn default() -> Self {
Self::new()
}
}
impl MoveTypeMapper {
pub fn new() -> Self {
Self {
custom_mappings: HashMap::new(),
}
}
pub fn add_mapping(&mut self, move_type: impl Into<String>, rust_type: RustType) {
self.custom_mappings.insert(move_type.into(), rust_type);
}
pub fn map_type(&self, move_type: &str) -> RustType {
if let Some(rust_type) = self.custom_mappings.get(move_type) {
return rust_type.clone();
}
match move_type {
"bool" => RustType::primitive("bool"),
"u8" => RustType::primitive("u8"),
"u16" => RustType::primitive("u16"),
"u32" => RustType::primitive("u32"),
"u64" => RustType::primitive("u64"),
"u128" => RustType::primitive("u128"),
"u256" => RustType::new("U256"),
"address" => RustType::new("AccountAddress"),
"signer" | "&signer" => RustType::new("AccountAddress")
.with_doc("Signer address (automatically set to sender)"),
_ => self.map_complex_type(move_type),
}
}
fn map_complex_type(&self, move_type: &str) -> RustType {
if move_type.starts_with("vector<") && move_type.ends_with('>') {
let inner = &move_type[7..move_type.len() - 1];
let inner_type = self.map_type(inner);
if inner == "u8" {
return RustType::new("Vec<u8>").with_doc("Bytes");
}
return RustType::new(format!("Vec<{}>", inner_type.path));
}
if move_type.contains("::option::Option<")
&& let Some(start) = move_type.find("Option<")
{
let rest = &move_type[start + 7..];
if let Some(end) = rest.rfind('>') {
let inner = &rest[..end];
let inner_type = self.map_type(inner);
return RustType::new(format!("Option<{}>", inner_type.path));
}
}
if move_type == "0x1::string::String" || move_type.ends_with("::string::String") {
return RustType::new("String");
}
if move_type.contains("::object::Object<") {
return RustType::new("AccountAddress").with_doc("Object address");
}
if move_type.contains("::") {
let part_count = move_type.matches("::").count() + 1;
if part_count >= 3 {
if let Some(struct_name) = move_type.rsplit("::").next() {
let base_name = struct_name.split('<').next().unwrap_or(struct_name);
let rust_name = to_pascal_case(base_name);
return RustType::new(rust_name).with_doc(format!("Move type: {move_type}"));
}
}
}
RustType::new("serde_json::Value").with_doc(format!("Unknown Move type: {move_type}"))
}
pub fn to_bcs_arg(&self, move_type: &str, var_name: &str) -> String {
let rust_type = self.map_type(move_type);
if !rust_type.needs_bcs {
return format!(
"aptos_bcs::to_bytes(&{var_name}).map_err(|e| AptosError::Bcs(e.to_string()))?"
);
}
format!("aptos_bcs::to_bytes(&{var_name}).map_err(|e| AptosError::Bcs(e.to_string()))?")
}
pub fn is_signer_param(&self, move_type: &str) -> bool {
move_type == "&signer" || move_type == "signer"
}
}
pub fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' || c == ' ' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
pub fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_ascii_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_primitive_mapping() {
let mapper = MoveTypeMapper::new();
assert_eq!(mapper.map_type("bool").path, "bool");
assert_eq!(mapper.map_type("u8").path, "u8");
assert_eq!(mapper.map_type("u64").path, "u64");
assert_eq!(mapper.map_type("u128").path, "u128");
assert_eq!(mapper.map_type("address").path, "AccountAddress");
}
#[test]
fn test_vector_mapping() {
let mapper = MoveTypeMapper::new();
assert_eq!(mapper.map_type("vector<u8>").path, "Vec<u8>");
assert_eq!(
mapper.map_type("vector<address>").path,
"Vec<AccountAddress>"
);
assert_eq!(mapper.map_type("vector<u64>").path, "Vec<u64>");
}
#[test]
fn test_string_mapping() {
let mapper = MoveTypeMapper::new();
assert_eq!(mapper.map_type("0x1::string::String").path, "String");
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
assert_eq!(to_pascal_case("coin"), "Coin");
assert_eq!(to_pascal_case("aptos_coin"), "AptosCoin");
}
#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("HelloWorld"), "hello_world");
assert_eq!(to_snake_case("Coin"), "coin");
assert_eq!(to_snake_case("AptosCoin"), "aptos_coin");
}
#[test]
fn test_rust_type_new() {
let rt = RustType::new("MyType");
assert_eq!(rt.path, "MyType");
assert!(rt.needs_bcs);
assert!(!rt.is_ref);
assert!(rt.doc.is_none());
}
#[test]
fn test_rust_type_primitive() {
let rt = RustType::primitive("u64");
assert_eq!(rt.path, "u64");
assert!(!rt.needs_bcs);
}
#[test]
fn test_rust_type_reference() {
let rt = RustType::new("MyType").reference();
assert!(rt.is_ref);
assert_eq!(rt.as_arg_type(), "&MyType");
}
#[test]
fn test_rust_type_with_doc() {
let rt = RustType::new("MyType").with_doc("My documentation");
assert_eq!(rt.doc, Some("My documentation".to_string()));
}
#[test]
fn test_rust_type_as_return_type() {
let rt = RustType::new("MyType").reference();
assert_eq!(rt.as_return_type(), "MyType"); }
#[test]
fn test_mapper_default() {
let mapper = MoveTypeMapper::default();
assert_eq!(mapper.map_type("bool").path, "bool");
}
#[test]
fn test_mapper_custom_mapping() {
let mut mapper = MoveTypeMapper::new();
mapper.add_mapping("MyCustomType", RustType::new("CustomRustType"));
assert_eq!(mapper.map_type("MyCustomType").path, "CustomRustType");
}
#[test]
fn test_mapper_u256() {
let mapper = MoveTypeMapper::new();
assert_eq!(mapper.map_type("u256").path, "U256");
}
#[test]
fn test_mapper_signer() {
let mapper = MoveTypeMapper::new();
assert_eq!(mapper.map_type("&signer").path, "AccountAddress");
assert_eq!(mapper.map_type("signer").path, "AccountAddress");
}
#[test]
fn test_mapper_nested_vector() {
let mapper = MoveTypeMapper::new();
let result = mapper.map_type("vector<vector<u8>>");
assert_eq!(result.path, "Vec<Vec<u8>>");
}
#[test]
fn test_mapper_option_type() {
let mapper = MoveTypeMapper::new();
let result = mapper.map_type("0x1::option::Option<u64>");
assert_eq!(result.path, "Option<u64>");
}
#[test]
fn test_mapper_object_type() {
let mapper = MoveTypeMapper::new();
let result = mapper.map_type("0x1::object::Object<Token>");
assert_eq!(result.path, "AccountAddress");
}
#[test]
fn test_mapper_unknown_struct() {
let mapper = MoveTypeMapper::new();
let result = mapper.map_type("0x1::module::SomeStruct");
assert!(result.doc.is_some());
}
#[test]
fn test_mapper_unknown_type() {
let mapper = MoveTypeMapper::new();
let result = mapper.map_type("some_completely_unknown_thing");
assert_eq!(result.path, "serde_json::Value");
}
#[test]
fn test_to_bcs_arg_address() {
let mapper = MoveTypeMapper::new();
let result = mapper.to_bcs_arg("address", "my_addr");
assert!(result.contains("aptos_bcs::to_bytes"));
assert!(result.contains("my_addr"));
}
#[test]
fn test_to_bcs_arg_vector_u8() {
let mapper = MoveTypeMapper::new();
let result = mapper.to_bcs_arg("vector<u8>", "my_bytes");
assert!(result.contains("aptos_bcs::to_bytes"));
}
#[test]
fn test_to_bcs_arg_vector_other() {
let mapper = MoveTypeMapper::new();
let result = mapper.to_bcs_arg("vector<u64>", "my_vec");
assert!(result.contains("aptos_bcs::to_bytes"));
}
#[test]
fn test_to_bcs_arg_string() {
let mapper = MoveTypeMapper::new();
let result = mapper.to_bcs_arg("0x1::string::String", "my_string");
assert!(result.contains("aptos_bcs::to_bytes"));
}
#[test]
fn test_to_bcs_arg_other_string() {
let mapper = MoveTypeMapper::new();
let result = mapper.to_bcs_arg("0xabc::my_module::string::String", "s");
assert!(result.contains("aptos_bcs::to_bytes"));
}
#[test]
fn test_is_signer_param() {
let mapper = MoveTypeMapper::new();
assert!(mapper.is_signer_param("&signer"));
assert!(mapper.is_signer_param("signer"));
assert!(!mapper.is_signer_param("address"));
assert!(!mapper.is_signer_param("u64"));
}
#[test]
fn test_to_pascal_case_with_spaces() {
assert_eq!(to_pascal_case("hello world"), "HelloWorld");
}
#[test]
fn test_to_pascal_case_with_dashes() {
assert_eq!(to_pascal_case("hello-world"), "HelloWorld");
}
#[test]
fn test_to_snake_case_single_word() {
assert_eq!(to_snake_case("hello"), "hello");
}
#[test]
fn test_to_snake_case_already_lowercase() {
assert_eq!(to_snake_case("helloworld"), "helloworld");
}
}