aptos_sdk/codegen/
build_helper.rs1use crate::api::response::MoveModuleABI;
46use crate::codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser};
47use crate::error::{AptosError, AptosResult};
48use std::fs;
49use std::path::Path;
50
51fn is_rust_keyword(name: &str) -> bool {
53 matches!(
54 name,
55 "as" | "break"
56 | "const"
57 | "continue"
58 | "crate"
59 | "else"
60 | "enum"
61 | "extern"
62 | "false"
63 | "fn"
64 | "for"
65 | "if"
66 | "impl"
67 | "in"
68 | "let"
69 | "loop"
70 | "match"
71 | "mod"
72 | "move"
73 | "mut"
74 | "pub"
75 | "ref"
76 | "return"
77 | "self"
78 | "Self"
79 | "static"
80 | "struct"
81 | "super"
82 | "trait"
83 | "true"
84 | "type"
85 | "unsafe"
86 | "use"
87 | "where"
88 | "while"
89 | "async"
90 | "await"
91 | "dyn"
92 )
93}
94
95fn validate_module_name(name: &str) -> AptosResult<()> {
103 if name.is_empty() {
104 return Err(AptosError::Config(
105 "module name cannot be empty".to_string(),
106 ));
107 }
108
109 let mut chars = name.chars();
112 let first = chars.next().unwrap(); if !first.is_ascii_alphabetic() && first != '_' {
114 return Err(AptosError::Config(format!(
115 "invalid module name '{name}': must start with a letter or underscore"
116 )));
117 }
118
119 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
120 return Err(AptosError::Config(format!(
121 "invalid module name '{name}': must contain only ASCII alphanumeric characters or underscores"
122 )));
123 }
124
125 if is_rust_keyword(name) {
126 return Err(AptosError::Config(format!(
127 "invalid module name '{name}': Rust keywords cannot be used as module names"
128 )));
129 }
130
131 Ok(())
132}
133
134#[derive(Debug, Clone)]
136pub struct BuildConfig {
137 pub generator_config: GeneratorConfig,
139 pub generate_mod_file: bool,
141 pub print_cargo_instructions: bool,
143}
144
145impl Default for BuildConfig {
146 fn default() -> Self {
147 Self {
148 generator_config: GeneratorConfig::default(),
149 generate_mod_file: true,
150 print_cargo_instructions: true,
151 }
152 }
153}
154
155impl BuildConfig {
156 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
164 pub fn with_mod_file(mut self, enabled: bool) -> Self {
165 self.generate_mod_file = enabled;
166 self
167 }
168
169 #[must_use]
171 pub fn with_generator_config(mut self, config: GeneratorConfig) -> Self {
172 self.generator_config = config;
173 self
174 }
175
176 #[must_use]
178 pub fn with_cargo_instructions(mut self, enabled: bool) -> Self {
179 self.print_cargo_instructions = enabled;
180 self
181 }
182}
183
184pub fn generate_from_abi(
206 abi_path: impl AsRef<Path>,
207 output_dir: impl AsRef<Path>,
208) -> AptosResult<()> {
209 generate_from_abi_with_config(abi_path, output_dir, BuildConfig::default())
210}
211
212pub fn generate_from_abi_with_config(
223 abi_path: impl AsRef<Path>,
224 output_dir: impl AsRef<Path>,
225 config: BuildConfig,
226) -> AptosResult<()> {
227 let abi_path = abi_path.as_ref();
228 let output_dir = output_dir.as_ref();
229
230 let abi_content = fs::read_to_string(abi_path).map_err(|e| {
232 AptosError::Config(format!(
233 "Failed to read ABI file {}: {}",
234 abi_path.display(),
235 e
236 ))
237 })?;
238
239 let abi: MoveModuleABI = serde_json::from_str(&abi_content)
240 .map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
241
242 validate_module_name(&abi.name)?;
244
245 let generator = ModuleGenerator::new(&abi, config.generator_config);
247 let code = generator.generate()?;
248
249 fs::create_dir_all(output_dir)
251 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
252
253 let output_filename = format!("{}.rs", abi.name);
255 let output_path = output_dir.join(&output_filename);
256
257 fs::write(&output_path, &code)
258 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
259
260 if config.print_cargo_instructions {
261 println!("cargo:rerun-if-changed={}", abi_path.display());
262 }
263
264 Ok(())
265}
266
267pub fn generate_from_abis(
295 abi_paths: &[impl AsRef<Path>],
296 output_dir: impl AsRef<Path>,
297) -> AptosResult<()> {
298 generate_from_abis_with_config(abi_paths, output_dir, &BuildConfig::default())
299}
300
301pub fn generate_from_abis_with_config(
313 abi_paths: &[impl AsRef<Path>],
314 output_dir: impl AsRef<Path>,
315 config: &BuildConfig,
316) -> AptosResult<()> {
317 let output_dir = output_dir.as_ref();
318 let mut module_names = Vec::new();
319
320 for abi_path in abi_paths {
322 let abi_path = abi_path.as_ref();
323
324 let abi_content = fs::read_to_string(abi_path).map_err(|e| {
325 AptosError::Config(format!(
326 "Failed to read ABI file {}: {}",
327 abi_path.display(),
328 e
329 ))
330 })?;
331
332 let abi: MoveModuleABI = serde_json::from_str(&abi_content).map_err(|e| {
333 AptosError::Config(format!(
334 "Failed to parse ABI JSON from {}: {}",
335 abi_path.display(),
336 e
337 ))
338 })?;
339
340 validate_module_name(&abi.name)?;
342
343 let generator = ModuleGenerator::new(&abi, config.generator_config.clone());
344 let code = generator.generate()?;
345
346 fs::create_dir_all(output_dir)
348 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
349
350 let output_filename = format!("{}.rs", abi.name);
352 let output_path = output_dir.join(&output_filename);
353
354 fs::write(&output_path, &code)
355 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
356
357 module_names.push(abi.name);
358
359 if config.print_cargo_instructions {
360 println!("cargo:rerun-if-changed={}", abi_path.display());
361 }
362 }
363
364 if config.generate_mod_file && !module_names.is_empty() {
366 let mod_content = generate_mod_file(&module_names);
367 let mod_path = output_dir.join("mod.rs");
368
369 fs::write(&mod_path, mod_content)
370 .map_err(|e| AptosError::Config(format!("Failed to write mod.rs: {e}")))?;
371 }
372
373 Ok(())
374}
375
376pub fn generate_from_abi_with_source(
394 abi_path: impl AsRef<Path>,
395 source_path: impl AsRef<Path>,
396 output_dir: impl AsRef<Path>,
397) -> AptosResult<()> {
398 let abi_path = abi_path.as_ref();
399 let source_path = source_path.as_ref();
400 let output_dir = output_dir.as_ref();
401
402 let abi_content = fs::read_to_string(abi_path)
404 .map_err(|e| AptosError::Config(format!("Failed to read ABI file: {e}")))?;
405
406 let abi: MoveModuleABI = serde_json::from_str(&abi_content)
407 .map_err(|e| AptosError::Config(format!("Failed to parse ABI JSON: {e}")))?;
408
409 let source_content = fs::read_to_string(source_path)
411 .map_err(|e| AptosError::Config(format!("Failed to read Move source: {e}")))?;
412
413 let source_info = MoveSourceParser::parse(&source_content);
414
415 validate_module_name(&abi.name)?;
417
418 let generator =
420 ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
421 let code = generator.generate()?;
422
423 fs::create_dir_all(output_dir)
425 .map_err(|e| AptosError::Config(format!("Failed to create output directory: {e}")))?;
426
427 let output_filename = format!("{}.rs", abi.name);
429 let output_path = output_dir.join(&output_filename);
430
431 fs::write(&output_path, &code)
432 .map_err(|e| AptosError::Config(format!("Failed to write output file: {e}")))?;
433
434 println!("cargo:rerun-if-changed={}", abi_path.display());
435 println!("cargo:rerun-if-changed={}", source_path.display());
436
437 Ok(())
438}
439
440fn generate_mod_file(module_names: &[String]) -> String {
442 use std::fmt::Write as _;
443 let mut content = String::new();
444 let _ = writeln!(&mut content, "//! Auto-generated module exports.");
445 let _ = writeln!(&mut content, "//!");
446 let _ = writeln!(
447 &mut content,
448 "//! This file was auto-generated by aptos-sdk codegen."
449 );
450 let _ = writeln!(&mut content, "//! Do not edit manually.");
451 let _ = writeln!(&mut content);
452
453 for name in module_names {
454 debug_assert!(
457 !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
458 "module name should have been validated"
459 );
460 let _ = writeln!(&mut content, "pub mod {name};");
461 }
462 let _ = writeln!(&mut content);
463
464 let _ = writeln!(&mut content, "// Re-exports for convenience");
466 for name in module_names {
467 let _ = writeln!(&mut content, "pub use {name}::*;");
468 }
469
470 content
471}
472
473pub fn generate_from_directory(
496 abi_dir: impl AsRef<Path>,
497 output_dir: impl AsRef<Path>,
498) -> AptosResult<()> {
499 let abi_dir = abi_dir.as_ref();
500
501 let entries = fs::read_dir(abi_dir)
502 .map_err(|e| AptosError::Config(format!("Failed to read ABI directory: {e}")))?;
503
504 let abi_paths: Vec<_> = entries
505 .filter_map(Result::ok)
506 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
507 .map(|e| e.path())
508 .collect();
509
510 if abi_paths.is_empty() {
511 return Err(AptosError::Config(format!(
512 "No JSON files found in {}",
513 abi_dir.display()
514 )));
515 }
516
517 let path_refs: Vec<&Path> = abi_paths.iter().map(std::path::PathBuf::as_path).collect();
519 generate_from_abis(&path_refs, output_dir)
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use std::io::Write;
526 use tempfile::TempDir;
527
528 fn sample_abi_json() -> &'static str {
529 r#"{
530 "address": "0x1",
531 "name": "coin",
532 "exposed_functions": [
533 {
534 "name": "transfer",
535 "visibility": "public",
536 "is_entry": true,
537 "is_view": false,
538 "generic_type_params": [{"constraints": []}],
539 "params": ["&signer", "address", "u64"],
540 "return": []
541 }
542 ],
543 "structs": []
544 }"#
545 }
546
547 #[test]
548 fn test_generate_from_abi() {
549 let temp_dir = TempDir::new().unwrap();
550 let abi_path = temp_dir.path().join("coin.json");
551 let output_dir = temp_dir.path().join("generated");
552
553 let mut file = fs::File::create(&abi_path).unwrap();
555 file.write_all(sample_abi_json().as_bytes()).unwrap();
556
557 let config = BuildConfig::new().with_cargo_instructions(false);
559 generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
560
561 let output_path = output_dir.join("coin.rs");
563 assert!(output_path.exists());
564
565 let content = fs::read_to_string(&output_path).unwrap();
567 assert!(content.contains("Generated Rust bindings"));
568 assert!(content.contains("pub fn transfer"));
569 }
570
571 #[test]
572 fn test_generate_mod_file() {
573 let modules = vec!["coin".to_string(), "token".to_string()];
574 let mod_content = generate_mod_file(&modules);
575
576 assert!(mod_content.contains("pub mod coin;"));
577 assert!(mod_content.contains("pub mod token;"));
578 assert!(mod_content.contains("pub use coin::*;"));
579 assert!(mod_content.contains("pub use token::*;"));
580 }
581
582 #[test]
583 fn test_build_config() {
584 let config = BuildConfig::new()
585 .with_mod_file(false)
586 .with_cargo_instructions(false);
587
588 assert!(!config.generate_mod_file);
589 assert!(!config.print_cargo_instructions);
590 }
591}