use crate::api::response::MoveModuleABI;
use crate::codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser};
use crate::error::{AptosError, AptosResult};
use std::fs;
use std::path::Path;
fn is_rust_keyword(name: &str) -> bool {
matches!(
name,
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "dyn"
)
}
fn validate_module_name(name: &str) -> AptosResult<()> {
if name.is_empty() {
return Err(AptosError::Config(
"module name cannot be empty".to_string(),
));
}
let mut chars = name.chars();
let first = chars.next().unwrap(); if !first.is_ascii_alphabetic() && first != '_' {
return Err(AptosError::Config(format!(
"invalid module name '{name}': must start with a letter or underscore"
)));
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(AptosError::Config(format!(
"invalid module name '{name}': must contain only ASCII alphanumeric characters or underscores"
)));
}
if is_rust_keyword(name) {
return Err(AptosError::Config(format!(
"invalid module name '{name}': Rust keywords cannot be used as module names"
)));
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub generator_config: GeneratorConfig,
pub generate_mod_file: bool,
pub print_cargo_instructions: bool,
}
impl Default for BuildConfig {
fn default() -> Self {
Self {
generator_config: GeneratorConfig::default(),
generate_mod_file: true,
print_cargo_instructions: true,
}
}
}
impl BuildConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_mod_file(mut self, enabled: bool) -> Self {
self.generate_mod_file = enabled;
self
}
#[must_use]
pub fn with_generator_config(mut self, config: GeneratorConfig) -> Self {
self.generator_config = config;
self
}
#[must_use]
pub fn with_cargo_instructions(mut self, enabled: bool) -> Self {
self.print_cargo_instructions = enabled;
self
}
}
pub fn generate_from_abi(
abi_path: impl AsRef<Path>,
output_dir: impl AsRef<Path>,
) -> AptosResult<()> {
generate_from_abi_with_config(abi_path, output_dir, BuildConfig::default())
}
pub fn generate_from_abi_with_config(
abi_path: impl AsRef<Path>,
output_dir: impl AsRef<Path>,
config: BuildConfig,
) -> AptosResult<()> {
let abi_path = abi_path.as_ref();
let output_dir = output_dir.as_ref();
let abi_content = fs::read_to_string(abi_path).map_err(|e| {
AptosError::Config(format!(
"Failed to read ABI file {}: {}",
abi_path.display(),
e
))
})?;
let abi: MoveModuleABI = serde_json::from_str(&abi_content)
.map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
validate_module_name(&abi.name)?;
let generator = ModuleGenerator::new(&abi, config.generator_config);
let code = generator.generate()?;
fs::create_dir_all(output_dir)
.map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
let output_filename = format!("{}.rs", abi.name);
let output_path = output_dir.join(&output_filename);
fs::write(&output_path, &code)
.map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
if config.print_cargo_instructions {
println!("cargo:rerun-if-changed={}", abi_path.display());
}
Ok(())
}
pub fn generate_from_abis(
abi_paths: &[impl AsRef<Path>],
output_dir: impl AsRef<Path>,
) -> AptosResult<()> {
generate_from_abis_with_config(abi_paths, output_dir, &BuildConfig::default())
}
pub fn generate_from_abis_with_config(
abi_paths: &[impl AsRef<Path>],
output_dir: impl AsRef<Path>,
config: &BuildConfig,
) -> AptosResult<()> {
let output_dir = output_dir.as_ref();
let mut module_names = Vec::new();
for abi_path in abi_paths {
let abi_path = abi_path.as_ref();
let abi_content = fs::read_to_string(abi_path).map_err(|e| {
AptosError::Config(format!(
"Failed to read ABI file {}: {}",
abi_path.display(),
e
))
})?;
let abi: MoveModuleABI = serde_json::from_str(&abi_content).map_err(|e| {
AptosError::Config(format!(
"Failed to parse ABI JSON from {}: {}",
abi_path.display(),
e
))
})?;
validate_module_name(&abi.name)?;
let generator = ModuleGenerator::new(&abi, config.generator_config.clone());
let code = generator.generate()?;
fs::create_dir_all(output_dir)
.map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
let output_filename = format!("{}.rs", abi.name);
let output_path = output_dir.join(&output_filename);
fs::write(&output_path, &code)
.map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
module_names.push(abi.name);
if config.print_cargo_instructions {
println!("cargo:rerun-if-changed={}", abi_path.display());
}
}
if config.generate_mod_file && !module_names.is_empty() {
let mod_content = generate_mod_file(&module_names);
let mod_path = output_dir.join("mod.rs");
fs::write(&mod_path, mod_content)
.map_err(|e| AptosError::Config(format!("Failed to write mod.rs: {e}")))?;
}
Ok(())
}
pub fn generate_from_abi_with_source(
abi_path: impl AsRef<Path>,
source_path: impl AsRef<Path>,
output_dir: impl AsRef<Path>,
) -> AptosResult<()> {
let abi_path = abi_path.as_ref();
let source_path = source_path.as_ref();
let output_dir = output_dir.as_ref();
let abi_content = fs::read_to_string(abi_path)
.map_err(|e| AptosError::Config(format!("Failed to read ABI file: {e}")))?;
let abi: MoveModuleABI = serde_json::from_str(&abi_content)
.map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
let source_content = fs::read_to_string(source_path)
.map_err(|e| AptosError::Config(format!("Failed to read Move source: {e}")))?;
let source_info = MoveSourceParser::parse(&source_content);
validate_module_name(&abi.name)?;
let generator =
ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
let code = generator.generate()?;
fs::create_dir_all(output_dir)
.map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
let output_filename = format!("{}.rs", abi.name);
let output_path = output_dir.join(&output_filename);
fs::write(&output_path, &code)
.map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
println!("cargo:rerun-if-changed={}", abi_path.display());
println!("cargo:rerun-if-changed={}", source_path.display());
Ok(())
}
fn generate_mod_file(module_names: &[String]) -> String {
use std::fmt::Write as _;
let mut content = String::new();
let _ = writeln!(&mut content, "//! Auto-generated module exports.");
let _ = writeln!(&mut content, "//!");
let _ = writeln!(
&mut content,
"//! This file was auto-generated by aptos-sdk codegen."
);
let _ = writeln!(&mut content, "//! Do not edit manually.");
let _ = writeln!(&mut content);
for name in module_names {
debug_assert!(
!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
"module name should have been validated"
);
let _ = writeln!(&mut content, "pub mod {name};");
}
let _ = writeln!(&mut content);
let _ = writeln!(&mut content, "// Re-exports for convenience");
for name in module_names {
let _ = writeln!(&mut content, "pub use {name}::*;");
}
content
}
pub fn generate_from_directory(
abi_dir: impl AsRef<Path>,
output_dir: impl AsRef<Path>,
) -> AptosResult<()> {
let abi_dir = abi_dir.as_ref();
let entries = fs::read_dir(abi_dir)
.map_err(|e| AptosError::Config(format!("Failed to read ABI directory: {e}")))?;
let abi_paths: Vec<_> = entries
.filter_map(Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.map(|e| e.path())
.collect();
if abi_paths.is_empty() {
return Err(AptosError::Config(format!(
"No JSON files found in {}",
abi_dir.display()
)));
}
let path_refs: Vec<&Path> = abi_paths.iter().map(std::path::PathBuf::as_path).collect();
generate_from_abis(&path_refs, output_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn sample_abi_json() -> &'static str {
r#"{
"address": "0x1",
"name": "coin",
"exposed_functions": [
{
"name": "transfer",
"visibility": "public",
"is_entry": true,
"is_view": false,
"generic_type_params": [{"constraints": []}],
"params": ["&signer", "address", "u64"],
"return": []
}
],
"structs": []
}"#
}
#[test]
fn test_generate_from_abi() {
let temp_dir = TempDir::new().unwrap();
let abi_path = temp_dir.path().join("coin.json");
let output_dir = temp_dir.path().join("generated");
let mut file = fs::File::create(&abi_path).unwrap();
file.write_all(sample_abi_json().as_bytes()).unwrap();
let config = BuildConfig::new().with_cargo_instructions(false);
generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
let output_path = output_dir.join("coin.rs");
assert!(output_path.exists());
let content = fs::read_to_string(&output_path).unwrap();
assert!(content.contains("Generated Rust bindings"));
assert!(content.contains("pub fn transfer"));
}
#[test]
fn test_generate_mod_file() {
let modules = vec!["coin".to_string(), "token".to_string()];
let mod_content = generate_mod_file(&modules);
assert!(mod_content.contains("pub mod coin;"));
assert!(mod_content.contains("pub mod token;"));
assert!(mod_content.contains("pub use coin::*;"));
assert!(mod_content.contains("pub use token::*;"));
}
#[test]
fn test_build_config() {
let config = BuildConfig::new()
.with_mod_file(false)
.with_cargo_instructions(false);
assert!(!config.generate_mod_file);
assert!(!config.print_cargo_instructions);
}
}