use crate::api::response::{MoveFunction, MoveModuleABI, MoveStructDef, MoveStructField};
use crate::codegen::move_parser::{EnrichedFunctionInfo, MoveModuleInfo};
use crate::codegen::types::{MoveTypeMapper, to_pascal_case, to_snake_case};
use crate::error::{AptosError, AptosResult};
use std::fmt::Write;
fn sanitize_abi_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace(['\n', '\r'], " ")
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct GeneratorConfig {
pub module_name: Option<String>,
pub generate_entry_functions: bool,
pub generate_view_functions: bool,
pub generate_structs: bool,
pub generate_events: bool,
pub async_functions: bool,
pub type_mapper: MoveTypeMapper,
pub include_address_constant: bool,
pub use_builder_pattern: bool,
}
impl Default for GeneratorConfig {
fn default() -> Self {
Self {
module_name: None,
generate_entry_functions: true,
generate_view_functions: true,
generate_structs: true,
generate_events: true,
async_functions: true,
type_mapper: MoveTypeMapper::new(),
include_address_constant: true,
use_builder_pattern: false,
}
}
}
impl GeneratorConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_module_name(mut self, name: impl Into<String>) -> Self {
self.module_name = Some(name.into());
self
}
#[must_use]
pub fn with_entry_functions(mut self, enabled: bool) -> Self {
self.generate_entry_functions = enabled;
self
}
#[must_use]
pub fn with_view_functions(mut self, enabled: bool) -> Self {
self.generate_view_functions = enabled;
self
}
#[must_use]
pub fn with_structs(mut self, enabled: bool) -> Self {
self.generate_structs = enabled;
self
}
#[must_use]
pub fn with_events(mut self, enabled: bool) -> Self {
self.generate_events = enabled;
self
}
#[must_use]
pub fn with_async(mut self, enabled: bool) -> Self {
self.async_functions = enabled;
self
}
#[must_use]
pub fn with_builder_pattern(mut self, enabled: bool) -> Self {
self.use_builder_pattern = enabled;
self
}
}
#[allow(missing_debug_implementations)] pub struct ModuleGenerator<'a> {
abi: &'a MoveModuleABI,
config: GeneratorConfig,
source_info: Option<MoveModuleInfo>,
}
impl<'a> ModuleGenerator<'a> {
#[must_use]
pub fn new(abi: &'a MoveModuleABI, config: GeneratorConfig) -> Self {
Self {
abi,
config,
source_info: None,
}
}
#[must_use]
pub fn with_source_info(mut self, source_info: MoveModuleInfo) -> Self {
self.source_info = Some(source_info);
self
}
fn get_enriched_function(&self, func: &MoveFunction) -> EnrichedFunctionInfo {
let source_func = self
.source_info
.as_ref()
.and_then(|s| s.functions.get(&func.name));
EnrichedFunctionInfo::from_abi_and_source(
&func.name,
&func.params,
func.generic_type_params.len(),
source_func,
)
}
pub fn generate(&self) -> AptosResult<String> {
let mut output = String::new();
self.write_all(&mut output)
.map_err(|e| AptosError::Internal(format!("Code generation failed: {e}")))?;
Ok(output)
}
fn write_all(&self, output: &mut String) -> std::fmt::Result {
self.write_header(output)?;
self.write_imports(output)?;
if self.config.include_address_constant {
self.write_address_constant(output)?;
}
if self.config.generate_structs {
self.write_structs(output)?;
}
if self.config.generate_events {
self.write_events(output)?;
}
if self.config.generate_entry_functions {
self.write_entry_functions(output)?;
}
if self.config.generate_view_functions {
self.write_view_functions(output)?;
}
Ok(())
}
fn write_events(&self, output: &mut String) -> std::fmt::Result {
let event_structs: Vec<_> = self
.abi
.structs
.iter()
.filter(|s| s.name.ends_with("Event") && !s.is_native)
.collect();
if event_structs.is_empty() {
return Ok(());
}
writeln!(output, "// =============================================")?;
writeln!(output, "// Event Types")?;
writeln!(output, "// =============================================")?;
writeln!(output)?;
writeln!(output, "/// All event types defined in this module.")?;
writeln!(output, "#[derive(Debug, Clone, PartialEq)]")?;
writeln!(output, "pub enum ModuleEvent {{")?;
for struct_def in &event_structs {
let variant_name = to_pascal_case(&struct_def.name);
writeln!(output, " {variant_name}({variant_name}),")?;
}
writeln!(output, " /// Unknown event type.")?;
writeln!(output, " Unknown(serde_json::Value),")?;
writeln!(output, "}}")?;
writeln!(output)?;
writeln!(output, "/// Event type strings for this module.")?;
writeln!(output, "pub mod event_types {{")?;
for struct_def in &event_structs {
let const_name = to_snake_case(&struct_def.name).to_uppercase();
writeln!(
output,
" pub const {}: &str = \"{}::{}::{}\";",
const_name,
sanitize_abi_string(&self.abi.address),
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&struct_def.name)
)?;
}
writeln!(output, "}}")?;
writeln!(output)?;
writeln!(output, "/// Parses a raw event into a typed ModuleEvent.")?;
writeln!(output, "///")?;
writeln!(output, "/// # Arguments")?;
writeln!(output, "///")?;
writeln!(output, "/// * `event_type` - The event type string")?;
writeln!(output, "/// * `data` - The event data as JSON")?;
writeln!(
output,
"pub fn parse_event(event_type: &str, data: serde_json::Value) -> AptosResult<ModuleEvent> {{"
)?;
writeln!(output, " match event_type {{")?;
for struct_def in &event_structs {
let const_name = to_snake_case(&struct_def.name).to_uppercase();
let variant_name = to_pascal_case(&struct_def.name);
writeln!(output, " event_types::{const_name} => {{")?;
writeln!(
output,
" let event: {variant_name} = serde_json::from_value(data)"
)?;
writeln!(
output,
" .map_err(|e| AptosError::Internal(format!(\"Failed to parse {}: {{}}\", e)))?;",
struct_def.name
)?;
writeln!(output, " Ok(ModuleEvent::{variant_name}(event))")?;
writeln!(output, " }}")?;
}
writeln!(output, " _ => Ok(ModuleEvent::Unknown(data)),")?;
writeln!(output, " }}")?;
writeln!(output, "}}")?;
writeln!(output)?;
writeln!(
output,
"/// Checks if an event type belongs to this module."
)?;
writeln!(
output,
"pub fn is_module_event(event_type: &str) -> bool {{"
)?;
writeln!(
output,
" event_type.starts_with(\"{}::{}::\")",
sanitize_abi_string(&self.abi.address),
sanitize_abi_string(&self.abi.name)
)?;
writeln!(output, "}}")?;
writeln!(output)
}
fn write_header(&self, output: &mut String) -> std::fmt::Result {
writeln!(
output,
"//! Generated Rust bindings for `{}::{}`.",
sanitize_abi_string(&self.abi.address),
sanitize_abi_string(&self.abi.name)
)?;
writeln!(output, "//!")?;
writeln!(
output,
"//! This file was auto-generated by aptos-sdk codegen."
)?;
writeln!(output, "//! Do not edit manually.")?;
writeln!(output)?;
writeln!(output, "#![allow(dead_code)]")?;
writeln!(output, "#![allow(unused_imports)]")?;
writeln!(output)
}
#[allow(clippy::unused_self)] fn write_imports(&self, output: &mut String) -> std::fmt::Result {
writeln!(output, "use aptos_sdk::{{")?;
writeln!(output, " account::Account,")?;
writeln!(output, " error::{{AptosError, AptosResult}},")?;
writeln!(
output,
" transaction::{{EntryFunction, TransactionPayload}},"
)?;
writeln!(output, " types::{{AccountAddress, TypeTag}},")?;
writeln!(output, " Aptos,")?;
writeln!(output, "}};")?;
writeln!(output, "use serde::{{Deserialize, Serialize}};")?;
writeln!(output)
}
fn write_address_constant(&self, output: &mut String) -> std::fmt::Result {
writeln!(output, "/// The address where this module is deployed.")?;
writeln!(
output,
"pub const MODULE_ADDRESS: &str = \"{}\";",
sanitize_abi_string(&self.abi.address)
)?;
writeln!(output)?;
writeln!(output, "/// The module name.")?;
writeln!(
output,
"pub const MODULE_NAME: &str = \"{}\";",
sanitize_abi_string(&self.abi.name)
)?;
writeln!(output)
}
fn write_structs(&self, output: &mut String) -> std::fmt::Result {
if self.abi.structs.is_empty() {
return Ok(());
}
writeln!(output, "// =============================================")?;
writeln!(output, "// Struct Definitions")?;
writeln!(output, "// =============================================")?;
writeln!(output)?;
for struct_def in &self.abi.structs {
self.write_struct(output, struct_def)?;
}
Ok(())
}
fn write_struct(&self, output: &mut String, struct_def: &MoveStructDef) -> std::fmt::Result {
if struct_def.is_native {
return Ok(());
}
let rust_name = to_pascal_case(&struct_def.name);
writeln!(
output,
"/// Move struct: `{}::{}`",
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&struct_def.name)
)?;
if !struct_def.abilities.is_empty() {
let abilities = struct_def
.abilities
.iter()
.map(|a| sanitize_abi_string(a))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "///")?;
writeln!(output, "/// Abilities: {abilities}")?;
}
writeln!(
output,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
)?;
if struct_def.generic_type_params.is_empty() {
writeln!(output, "pub struct {rust_name} {{")?;
} else {
let type_params: Vec<String> = struct_def
.generic_type_params
.iter()
.enumerate()
.map(|(i, _)| format!("T{i}"))
.collect();
writeln!(
output,
"pub struct {}<{}> {{",
rust_name,
type_params.join(", ")
)?;
}
for field in &struct_def.fields {
self.write_struct_field(output, field)?;
}
writeln!(output, "}}")?;
writeln!(output)
}
fn write_struct_field(&self, output: &mut String, field: &MoveStructField) -> std::fmt::Result {
let rust_type = self.config.type_mapper.map_type(&field.typ);
let rust_name = to_snake_case(&field.name);
let rust_name = match rust_name.as_str() {
"type" => "r#type".to_string(),
"self" => "r#self".to_string(),
"move" => "r#move".to_string(),
_ => rust_name,
};
if let Some(doc) = &rust_type.doc {
writeln!(output, " /// {}", sanitize_abi_string(doc))?;
}
if rust_name != field.name && !rust_name.starts_with("r#") {
writeln!(
output,
" #[serde(rename = \"{}\")]",
sanitize_abi_string(&field.name)
)?;
}
writeln!(output, " pub {}: {},", rust_name, rust_type.path)
}
fn write_entry_functions(&self, output: &mut String) -> std::fmt::Result {
let entry_functions: Vec<_> = self
.abi
.exposed_functions
.iter()
.filter(|f| f.is_entry)
.collect();
if entry_functions.is_empty() {
return Ok(());
}
writeln!(output, "// =============================================")?;
writeln!(output, "// Entry Functions")?;
writeln!(output, "// =============================================")?;
writeln!(output)?;
for func in entry_functions {
self.write_entry_function(output, func)?;
}
Ok(())
}
fn write_entry_function(&self, output: &mut String, func: &MoveFunction) -> std::fmt::Result {
let rust_name = to_snake_case(&func.name);
let enriched = self.get_enriched_function(func);
let params: Vec<_> = enriched
.non_signer_params()
.into_iter()
.map(|p| {
let rust_type = self.config.type_mapper.map_type(&p.move_type);
(p.name.clone(), p.move_type.clone(), rust_type)
})
.collect();
if let Some(doc) = &enriched.doc {
for line in doc.lines() {
writeln!(output, "/// {}", sanitize_abi_string(line))?;
}
writeln!(output, "///")?;
} else {
writeln!(
output,
"/// Entry function: `{}::{}`",
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&func.name)
)?;
writeln!(output, "///")?;
}
if !params.is_empty() {
writeln!(output, "/// # Arguments")?;
writeln!(output, "///")?;
for (name, move_type, rust_type) in ¶ms {
writeln!(
output,
"/// * `{}` - {} (Move type: `{}`)",
sanitize_abi_string(name),
sanitize_abi_string(&rust_type.path),
sanitize_abi_string(move_type)
)?;
}
}
if !enriched.type_param_names.is_empty() {
let type_params = enriched
.type_param_names
.iter()
.map(|s| sanitize_abi_string(s))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "/// * `type_args` - Type arguments: {type_params}")?;
}
write!(output, "pub fn {rust_name}(")?;
let mut param_strs = Vec::new();
for (name, _, rust_type) in ¶ms {
let safe_name = Self::safe_param_name(name);
param_strs.push(format!("{}: {}", safe_name, rust_type.as_arg_type()));
}
if !enriched.type_param_names.is_empty() {
param_strs.push("type_args: Vec<TypeTag>".to_string());
}
write!(output, "{}", param_strs.join(", "))?;
writeln!(output, ") -> AptosResult<TransactionPayload> {{")?;
writeln!(output, " let function_id = format!(")?;
writeln!(
output,
" \"{}::{}::{}\",",
sanitize_abi_string(&self.abi.address),
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&func.name)
)?;
writeln!(output, " );")?;
writeln!(output)?;
writeln!(output, " let args = vec![")?;
for (name, move_type, _) in ¶ms {
let safe_name = Self::safe_param_name(name);
let bcs_expr = self.config.type_mapper.to_bcs_arg(move_type, &safe_name);
writeln!(output, " {bcs_expr},")?;
}
writeln!(output, " ];")?;
writeln!(output)?;
if func.generic_type_params.is_empty() {
writeln!(output, " let type_args = vec![];")?;
}
writeln!(output)?;
writeln!(
output,
" let entry_fn = EntryFunction::from_function_id(&function_id, type_args, args)?;"
)?;
writeln!(
output,
" Ok(TransactionPayload::EntryFunction(entry_fn))"
)?;
writeln!(output, "}}")?;
writeln!(output)
}
fn write_view_functions(&self, output: &mut String) -> std::fmt::Result {
let view_functions: Vec<_> = self
.abi
.exposed_functions
.iter()
.filter(|f| f.is_view)
.collect();
if view_functions.is_empty() {
return Ok(());
}
writeln!(output, "// =============================================")?;
writeln!(output, "// View Functions")?;
writeln!(output, "// =============================================")?;
writeln!(output)?;
for func in view_functions {
self.write_view_function(output, func)?;
}
Ok(())
}
fn safe_param_name(name: &str) -> String {
let snake = to_snake_case(name);
match snake.as_str() {
"type" => "r#type".to_string(),
"self" => "r#self".to_string(),
"move" => "r#move".to_string(),
"ref" => "r#ref".to_string(),
"mut" => "r#mut".to_string(),
"fn" => "r#fn".to_string(),
"mod" => "r#mod".to_string(),
"use" => "r#use".to_string(),
"pub" => "r#pub".to_string(),
"let" => "r#let".to_string(),
"if" => "r#if".to_string(),
"else" => "r#else".to_string(),
"match" => "r#match".to_string(),
"loop" => "r#loop".to_string(),
"while" => "r#while".to_string(),
"for" => "r#for".to_string(),
"in" => "r#in".to_string(),
"return" => "r#return".to_string(),
"break" => "r#break".to_string(),
"continue" => "r#continue".to_string(),
"async" => "r#async".to_string(),
"await" => "r#await".to_string(),
"struct" => "r#struct".to_string(),
"enum" => "r#enum".to_string(),
"trait" => "r#trait".to_string(),
"impl" => "r#impl".to_string(),
"dyn" => "r#dyn".to_string(),
"const" => "r#const".to_string(),
"static" => "r#static".to_string(),
"unsafe" => "r#unsafe".to_string(),
"extern" => "r#extern".to_string(),
"crate" => "r#crate".to_string(),
"super" => "r#super".to_string(),
"where" => "r#where".to_string(),
"as" => "r#as".to_string(),
"true" => "r#true".to_string(),
"false" => "r#false".to_string(),
_ => snake,
}
}
fn write_view_function(&self, output: &mut String, func: &MoveFunction) -> std::fmt::Result {
let rust_name = format!("view_{}", to_snake_case(&func.name));
let enriched = self.get_enriched_function(func);
let params: Vec<_> = enriched
.params
.iter()
.map(|p| {
let rust_type = self.config.type_mapper.map_type(&p.move_type);
(p.name.clone(), p.move_type.clone(), rust_type)
})
.collect();
let return_type = if func.returns.is_empty() {
"()".to_string()
} else if func.returns.len() == 1 {
self.config.type_mapper.map_type(&func.returns[0]).path
} else {
let types: Vec<String> = func
.returns
.iter()
.map(|r| self.config.type_mapper.map_type(r).path)
.collect();
format!("({})", types.join(", "))
};
if let Some(doc) = &enriched.doc {
for line in doc.lines() {
writeln!(output, "/// {}", sanitize_abi_string(line))?;
}
writeln!(output, "///")?;
} else {
writeln!(
output,
"/// View function: `{}::{}`",
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&func.name)
)?;
writeln!(output, "///")?;
}
if !params.is_empty() {
writeln!(output, "/// # Arguments")?;
writeln!(output, "///")?;
for (name, move_type, rust_type) in ¶ms {
writeln!(
output,
"/// * `{}` - {} (Move type: `{}`)",
sanitize_abi_string(name),
sanitize_abi_string(&rust_type.path),
sanitize_abi_string(move_type)
)?;
}
}
if !enriched.type_param_names.is_empty() {
let type_params = enriched
.type_param_names
.iter()
.map(|s| sanitize_abi_string(s))
.collect::<Vec<_>>()
.join(", ");
writeln!(output, "/// * `type_args` - Type arguments: {type_params}")?;
}
if !func.returns.is_empty() {
writeln!(output, "///")?;
writeln!(output, "/// # Returns")?;
writeln!(output, "///")?;
writeln!(output, "/// `{}`", sanitize_abi_string(&return_type))?;
}
let async_kw = if self.config.async_functions {
"async "
} else {
""
};
write!(output, "pub {async_kw}fn {rust_name}(aptos: &Aptos")?;
for (name, _, rust_type) in ¶ms {
let safe_name = Self::safe_param_name(name);
write!(output, ", {}: {}", safe_name, rust_type.as_arg_type())?;
}
if !enriched.type_param_names.is_empty() {
write!(output, ", type_args: Vec<String>")?;
}
writeln!(output, ") -> AptosResult<Vec<serde_json::Value>> {{")?;
writeln!(output, " let function_id = format!(")?;
writeln!(
output,
" \"{}::{}::{}\",",
sanitize_abi_string(&self.abi.address),
sanitize_abi_string(&self.abi.name),
sanitize_abi_string(&func.name)
)?;
writeln!(output, " );")?;
writeln!(output)?;
if enriched.type_param_names.is_empty() {
writeln!(output, " let type_args: Vec<String> = vec![];")?;
}
writeln!(output, " let args = vec![")?;
for (name, move_type, _) in ¶ms {
let safe_name = Self::safe_param_name(name);
let arg_expr = self.view_arg_json_expr(move_type, &safe_name);
writeln!(output, " {arg_expr},")?;
}
writeln!(output, " ];")?;
writeln!(output)?;
let await_kw = if self.config.async_functions {
".await"
} else {
""
};
writeln!(
output,
" aptos.view(&function_id, type_args, args){await_kw}"
)?;
writeln!(output, "}}")?;
writeln!(output)
}
#[allow(clippy::unused_self)] fn view_arg_json_expr(&self, move_type: &str, var_name: &str) -> String {
match move_type {
"address" => format!("serde_json::json!({var_name}.to_string())"),
"bool" | "u8" | "u16" | "u32" | "u64" | "u128" => {
format!("serde_json::json!({var_name}.to_string())")
}
_ if move_type.starts_with("vector<u8>") => {
format!("serde_json::json!(::aptos_sdk::const_hex::encode({var_name}))")
}
"0x1::string::String" => format!("serde_json::json!({var_name})"),
_ if move_type.ends_with("::string::String") => {
format!("serde_json::json!({var_name})")
}
_ => format!("serde_json::json!({var_name})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::response::{MoveFunctionGenericTypeParam, MoveStructGenericTypeParam};
fn sample_abi() -> MoveModuleABI {
MoveModuleABI {
address: "0x1".to_string(),
name: "coin".to_string(),
exposed_functions: vec![
MoveFunction {
name: "transfer".to_string(),
visibility: "public".to_string(),
is_entry: true,
is_view: false,
generic_type_params: vec![MoveFunctionGenericTypeParam {
constraints: vec![],
}],
params: vec![
"&signer".to_string(),
"address".to_string(),
"u64".to_string(),
],
returns: vec![],
},
MoveFunction {
name: "balance".to_string(),
visibility: "public".to_string(),
is_entry: false,
is_view: true,
generic_type_params: vec![MoveFunctionGenericTypeParam {
constraints: vec![],
}],
params: vec!["address".to_string()],
returns: vec!["u64".to_string()],
},
],
structs: vec![MoveStructDef {
name: "Coin".to_string(),
is_native: false,
abilities: vec!["store".to_string()],
generic_type_params: vec![MoveStructGenericTypeParam {
constraints: vec![],
}],
fields: vec![MoveStructField {
name: "value".to_string(),
typ: "u64".to_string(),
}],
}],
}
}
fn sample_abi_with_events() -> MoveModuleABI {
MoveModuleABI {
address: "0x1".to_string(),
name: "token".to_string(),
exposed_functions: vec![],
structs: vec![
MoveStructDef {
name: "MintEvent".to_string(),
is_native: false,
abilities: vec!["drop".to_string(), "store".to_string()],
generic_type_params: vec![],
fields: vec![
MoveStructField {
name: "id".to_string(),
typ: "u64".to_string(),
},
MoveStructField {
name: "creator".to_string(),
typ: "address".to_string(),
},
],
},
MoveStructDef {
name: "BurnEvent".to_string(),
is_native: false,
abilities: vec!["drop".to_string(), "store".to_string()],
generic_type_params: vec![],
fields: vec![MoveStructField {
name: "id".to_string(),
typ: "u64".to_string(),
}],
},
MoveStructDef {
name: "Token".to_string(),
is_native: false,
abilities: vec!["key".to_string()],
generic_type_params: vec![],
fields: vec![MoveStructField {
name: "value".to_string(),
typ: "u64".to_string(),
}],
},
],
}
}
#[test]
fn test_generate_module() {
let abi = sample_abi();
let generator = ModuleGenerator::new(&abi, GeneratorConfig::default());
let code = generator.generate().unwrap();
assert!(code.contains("Generated Rust bindings"));
assert!(code.contains("0x1::coin"));
assert!(code.contains("MODULE_ADDRESS"));
assert!(code.contains("MODULE_NAME"));
assert!(code.contains("pub struct Coin"));
assert!(code.contains("pub value: u64"));
assert!(code.contains("pub fn transfer"));
assert!(code.contains("pub async fn view_balance"));
}
#[test]
fn test_entry_function_excludes_signer() {
let abi = sample_abi();
let generator = ModuleGenerator::new(&abi, GeneratorConfig::default());
let code = generator.generate().unwrap();
assert!(code.contains("addr: AccountAddress"));
assert!(code.contains("amount: u64"));
assert!(!code.contains("account: AccountAddress"));
assert!(code.contains("pub fn transfer("));
}
#[test]
fn test_entry_function_with_source_info() {
use crate::codegen::move_parser::MoveSourceParser;
let abi = sample_abi();
let source = r"
module 0x1::coin {
/// Transfers coins from sender to recipient.
public entry fun transfer<CoinType>(
from: &signer,
to: address,
value: u64,
) { }
}
";
let source_info = MoveSourceParser::parse(source);
let generator =
ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
let code = generator.generate().unwrap();
assert!(code.contains("to: AccountAddress"));
assert!(code.contains("value: u64"));
assert!(code.contains("Transfers coins from sender to recipient"));
}
#[test]
fn test_generate_events() {
let abi = sample_abi_with_events();
let generator = ModuleGenerator::new(&abi, GeneratorConfig::default());
let code = generator.generate().unwrap();
assert!(code.contains("pub enum ModuleEvent"));
assert!(code.contains("MintEvent(MintEvent)"));
assert!(code.contains("BurnEvent(BurnEvent)"));
assert!(code.contains("Unknown(serde_json::Value)"));
assert!(code.contains("pub mod event_types"));
assert!(code.contains("MINT_EVENT"));
assert!(code.contains("BURN_EVENT"));
assert!(code.contains("pub fn parse_event"));
assert!(code.contains("is_module_event"));
assert!(!code.contains("Token(Token)"));
}
#[test]
fn test_config_disable_events() {
let abi = sample_abi_with_events();
let config = GeneratorConfig::default().with_events(false);
let generator = ModuleGenerator::new(&abi, config);
let code = generator.generate().unwrap();
assert!(!code.contains("pub enum ModuleEvent"));
assert!(!code.contains("pub fn parse_event"));
}
#[test]
fn test_config_disable_structs() {
let abi = sample_abi();
let config = GeneratorConfig::default().with_structs(false);
let generator = ModuleGenerator::new(&abi, config);
let code = generator.generate().unwrap();
assert!(!code.contains("pub struct Coin"));
}
#[test]
fn test_config_sync_functions() {
let abi = sample_abi();
let config = GeneratorConfig::default().with_async(false);
let generator = ModuleGenerator::new(&abi, config);
let code = generator.generate().unwrap();
assert!(code.contains("pub fn view_balance"));
assert!(!code.contains("pub async fn view_balance"));
}
#[test]
fn test_config_no_address_constant() {
let abi = sample_abi();
let config = GeneratorConfig {
include_address_constant: false,
..Default::default()
};
let generator = ModuleGenerator::new(&abi, config);
let code = generator.generate().unwrap();
assert!(!code.contains("MODULE_ADDRESS"));
}
#[test]
fn test_generator_config_builder() {
let config = GeneratorConfig::new()
.with_module_name("custom_name")
.with_entry_functions(false)
.with_view_functions(false)
.with_structs(false)
.with_events(false)
.with_async(false)
.with_builder_pattern(true);
assert_eq!(config.module_name, Some("custom_name".to_string()));
assert!(!config.generate_entry_functions);
assert!(!config.generate_view_functions);
assert!(!config.generate_structs);
assert!(!config.generate_events);
assert!(!config.async_functions);
assert!(config.use_builder_pattern);
}
#[test]
fn test_empty_module() {
let abi = MoveModuleABI {
address: "0x1".to_string(),
name: "empty".to_string(),
exposed_functions: vec![],
structs: vec![],
};
let generator = ModuleGenerator::new(&abi, GeneratorConfig::default());
let code = generator.generate().unwrap();
assert!(code.contains("Generated Rust bindings"));
assert!(code.contains("MODULE_ADDRESS"));
assert!(code.contains("MODULE_NAME"));
}
}