1use 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 fn abi_json_with_name(name: &str) -> String {
549 format!(
550 r#"{{
551 "address": "0x1",
552 "name": "{name}",
553 "exposed_functions": [
554 {{
555 "name": "transfer",
556 "visibility": "public",
557 "is_entry": true,
558 "is_view": false,
559 "generic_type_params": [{{"constraints": []}}],
560 "params": ["&signer", "address", "u64"],
561 "return": []
562 }}
563 ],
564 "structs": []
565 }}"#
566 )
567 }
568
569 fn write_file(path: &Path, contents: &str) {
571 if let Some(parent) = path.parent() {
572 fs::create_dir_all(parent).unwrap();
573 }
574 let mut file = fs::File::create(path).unwrap();
575 file.write_all(contents.as_bytes()).unwrap();
576 }
577
578 #[test]
579 fn test_generate_from_abi() {
580 let temp_dir = TempDir::new().unwrap();
581 let abi_path = temp_dir.path().join("coin.json");
582 let output_dir = temp_dir.path().join("generated");
583
584 let mut file = fs::File::create(&abi_path).unwrap();
586 file.write_all(sample_abi_json().as_bytes()).unwrap();
587
588 let config = BuildConfig::new().with_cargo_instructions(false);
590 generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
591
592 let output_path = output_dir.join("coin.rs");
594 assert!(output_path.exists());
595
596 let content = fs::read_to_string(&output_path).unwrap();
598 assert!(content.contains("Generated Rust bindings"));
599 assert!(content.contains("pub fn transfer"));
600 }
601
602 #[test]
603 fn test_generate_mod_file() {
604 let modules = vec!["coin".to_string(), "token".to_string()];
605 let mod_content = generate_mod_file(&modules);
606
607 assert!(mod_content.contains("pub mod coin;"));
608 assert!(mod_content.contains("pub mod token;"));
609 assert!(mod_content.contains("pub use coin::*;"));
610 assert!(mod_content.contains("pub use token::*;"));
611 }
612
613 #[test]
614 fn test_build_config() {
615 let config = BuildConfig::new()
616 .with_mod_file(false)
617 .with_cargo_instructions(false);
618
619 assert!(!config.generate_mod_file);
620 assert!(!config.print_cargo_instructions);
621 }
622
623 #[test]
624 fn test_build_config_defaults() {
625 let config = BuildConfig::default();
626 assert!(config.generate_mod_file);
627 assert!(config.print_cargo_instructions);
628 let cloned = config.clone();
630 assert!(cloned.generate_mod_file);
631 assert!(format!("{config:?}").contains("BuildConfig"));
632 }
633
634 #[test]
635 fn test_build_config_with_generator_config() {
636 let gen_config = GeneratorConfig::default();
637 let config = BuildConfig::new().with_generator_config(gen_config);
638 assert!(config.generate_mod_file);
640 }
641
642 #[test]
645 fn test_validate_module_name_valid() {
646 assert!(validate_module_name("coin").is_ok());
647 assert!(validate_module_name("_private").is_ok());
648 assert!(validate_module_name("token_v2").is_ok());
649 assert!(validate_module_name("MyModule").is_ok());
650 assert!(validate_module_name("a1b2c3").is_ok());
651 }
652
653 #[test]
654 fn test_validate_module_name_empty() {
655 let err = validate_module_name("").unwrap_err();
656 assert!(matches!(err, AptosError::Config(ref m) if m.contains("empty")));
657 }
658
659 #[test]
660 fn test_validate_module_name_bad_first_char() {
661 let err = validate_module_name("1coin").unwrap_err();
663 assert!(matches!(err, AptosError::Config(ref m) if m.contains("must start with")));
664 let err = validate_module_name("../evil").unwrap_err();
666 assert!(matches!(err, AptosError::Config(ref m) if m.contains("must start with")));
667 }
668
669 #[test]
670 fn test_validate_module_name_bad_inner_char() {
671 let err = validate_module_name("coin/evil").unwrap_err();
673 assert!(matches!(err, AptosError::Config(ref m) if m.contains("only ASCII")));
674 let err = validate_module_name("café").unwrap_err();
676 assert!(matches!(err, AptosError::Config(ref m) if m.contains("only ASCII")));
677 }
678
679 #[test]
680 fn test_validate_module_name_keyword() {
681 for kw in ["fn", "mod", "struct", "type", "use", "async", "dyn", "move"] {
682 let err = validate_module_name(kw).unwrap_err();
683 assert!(
684 matches!(err, AptosError::Config(ref m) if m.contains("keyword")),
685 "expected keyword rejection for {kw}"
686 );
687 }
688 }
689
690 #[test]
691 fn test_is_rust_keyword() {
692 assert!(is_rust_keyword("fn"));
693 assert!(is_rust_keyword("Self"));
694 assert!(is_rust_keyword("await"));
695 assert!(!is_rust_keyword("coin"));
696 assert!(!is_rust_keyword("self_ish"));
697 }
698
699 #[test]
702 fn test_generate_from_abi_default_wrapper() {
703 let temp_dir = TempDir::new().unwrap();
704 let abi_path = temp_dir.path().join("coin.json");
705 let output_dir = temp_dir.path().join("generated");
706 write_file(&abi_path, sample_abi_json());
707
708 generate_from_abi(&abi_path, &output_dir).unwrap();
709
710 let content = fs::read_to_string(output_dir.join("coin.rs")).unwrap();
711 assert!(content.contains("pub fn transfer"));
712 }
713
714 #[test]
715 fn test_generate_from_abi_missing_file() {
716 let temp_dir = TempDir::new().unwrap();
717 let abi_path = temp_dir.path().join("does_not_exist.json");
718 let output_dir = temp_dir.path().join("generated");
719
720 let err = generate_from_abi(&abi_path, &output_dir).unwrap_err();
721 assert!(matches!(err, AptosError::Config(ref m) if m.contains("Failed to read ABI file")));
722 }
723
724 #[test]
725 fn test_generate_from_abi_invalid_json() {
726 let temp_dir = TempDir::new().unwrap();
727 let abi_path = temp_dir.path().join("bad.json");
728 let output_dir = temp_dir.path().join("generated");
729 write_file(&abi_path, "{ this is not valid json");
730
731 let err = generate_from_abi(&abi_path, &output_dir).unwrap_err();
732 assert!(matches!(err, AptosError::Config(ref m) if m.contains("Failed to parse ABI JSON")));
733 }
734
735 #[test]
736 fn test_generate_from_abi_rejects_bad_module_name() {
737 let temp_dir = TempDir::new().unwrap();
738 let abi_path = temp_dir.path().join("evil.json");
739 let output_dir = temp_dir.path().join("generated");
740 write_file(&abi_path, &abi_json_with_name("../../evil"));
742
743 let err = generate_from_abi(&abi_path, &output_dir).unwrap_err();
744 assert!(matches!(err, AptosError::Config(ref m) if m.contains("must start with")));
745 assert!(!output_dir.exists());
746 }
747
748 #[test]
751 fn test_generate_from_abis_multiple_with_mod_file() {
752 let temp_dir = TempDir::new().unwrap();
753 let coin_path = temp_dir.path().join("coin.json");
754 let token_path = temp_dir.path().join("token.json");
755 let output_dir = temp_dir.path().join("generated");
756 write_file(&coin_path, &abi_json_with_name("coin"));
757 write_file(&token_path, &abi_json_with_name("token"));
758
759 let config = BuildConfig::new().with_cargo_instructions(false);
760 generate_from_abis_with_config(&[&coin_path, &token_path], &output_dir, &config).unwrap();
761
762 assert!(output_dir.join("coin.rs").exists());
763 assert!(output_dir.join("token.rs").exists());
764 let mod_content = fs::read_to_string(output_dir.join("mod.rs")).unwrap();
765 assert!(mod_content.contains("pub mod coin;"));
766 assert!(mod_content.contains("pub mod token;"));
767 }
768
769 #[test]
770 fn test_generate_from_abis_no_mod_file_when_disabled() {
771 let temp_dir = TempDir::new().unwrap();
772 let coin_path = temp_dir.path().join("coin.json");
773 let output_dir = temp_dir.path().join("generated");
774 write_file(&coin_path, &abi_json_with_name("coin"));
775
776 let config = BuildConfig::new()
777 .with_mod_file(false)
778 .with_cargo_instructions(false);
779 generate_from_abis_with_config(&[&coin_path], &output_dir, &config).unwrap();
780
781 assert!(output_dir.join("coin.rs").exists());
782 assert!(!output_dir.join("mod.rs").exists());
783 }
784
785 #[test]
786 fn test_generate_from_abis_default_wrapper() {
787 let temp_dir = TempDir::new().unwrap();
788 let coin_path = temp_dir.path().join("coin.json");
789 let output_dir = temp_dir.path().join("generated");
790 write_file(&coin_path, &abi_json_with_name("coin"));
791
792 generate_from_abis(&[&coin_path], &output_dir).unwrap();
793 assert!(output_dir.join("coin.rs").exists());
794 assert!(output_dir.join("mod.rs").exists());
796 }
797
798 #[test]
799 fn test_generate_from_abis_missing_file_errors() {
800 let temp_dir = TempDir::new().unwrap();
801 let missing = temp_dir.path().join("missing.json");
802 let output_dir = temp_dir.path().join("generated");
803
804 let err = generate_from_abis(&[&missing], &output_dir).unwrap_err();
805 assert!(matches!(err, AptosError::Config(ref m) if m.contains("Failed to read ABI file")));
806 }
807
808 #[test]
809 fn test_generate_from_abis_invalid_json_errors() {
810 let temp_dir = TempDir::new().unwrap();
811 let bad = temp_dir.path().join("bad.json");
812 let output_dir = temp_dir.path().join("generated");
813 write_file(&bad, "not json");
814
815 let err = generate_from_abis(&[&bad], &output_dir).unwrap_err();
816 assert!(
817 matches!(err, AptosError::Config(ref m) if m.contains("Failed to parse ABI JSON from"))
818 );
819 }
820
821 #[test]
822 fn test_generate_from_abis_bad_module_name_errors() {
823 let temp_dir = TempDir::new().unwrap();
824 let evil = temp_dir.path().join("evil.json");
825 let output_dir = temp_dir.path().join("generated");
826 write_file(&evil, &abi_json_with_name("mod"));
827
828 let err = generate_from_abis(&[&evil], &output_dir).unwrap_err();
829 assert!(matches!(err, AptosError::Config(ref m) if m.contains("keyword")));
830 }
831
832 #[test]
833 fn test_generate_from_abis_empty_input_writes_nothing() {
834 let temp_dir = TempDir::new().unwrap();
835 let output_dir = temp_dir.path().join("generated");
836 let empty: [&Path; 0] = [];
837
838 generate_from_abis(&empty, &output_dir).unwrap();
840 assert!(!output_dir.join("mod.rs").exists());
841 }
842
843 #[test]
846 fn test_generate_from_abi_with_source() {
847 let temp_dir = TempDir::new().unwrap();
848 let abi_path = temp_dir.path().join("coin.json");
849 let source_path = temp_dir.path().join("coin.move");
850 let output_dir = temp_dir.path().join("generated");
851 write_file(&abi_path, sample_abi_json());
852 write_file(
853 &source_path,
854 "module 0x1::coin {\n\
855 /// Transfers coins.\n\
856 public entry fun transfer(from: &signer, to: address, amount: u64) {}\n\
857 }\n",
858 );
859
860 generate_from_abi_with_source(&abi_path, &source_path, &output_dir).unwrap();
861
862 let content = fs::read_to_string(output_dir.join("coin.rs")).unwrap();
863 assert!(content.contains("pub fn transfer"));
864 }
865
866 #[test]
867 fn test_generate_from_abi_with_source_missing_abi() {
868 let temp_dir = TempDir::new().unwrap();
869 let abi_path = temp_dir.path().join("missing.json");
870 let source_path = temp_dir.path().join("coin.move");
871 let output_dir = temp_dir.path().join("generated");
872 write_file(&source_path, "module 0x1::coin {}");
873
874 let err = generate_from_abi_with_source(&abi_path, &source_path, &output_dir).unwrap_err();
875 assert!(matches!(err, AptosError::Config(ref m) if m.contains("Failed to read ABI file")));
876 }
877
878 #[test]
879 fn test_generate_from_abi_with_source_missing_source() {
880 let temp_dir = TempDir::new().unwrap();
881 let abi_path = temp_dir.path().join("coin.json");
882 let source_path = temp_dir.path().join("missing.move");
883 let output_dir = temp_dir.path().join("generated");
884 write_file(&abi_path, sample_abi_json());
885
886 let err = generate_from_abi_with_source(&abi_path, &source_path, &output_dir).unwrap_err();
887 assert!(
888 matches!(err, AptosError::Config(ref m) if m.contains("Failed to read Move source"))
889 );
890 }
891
892 #[test]
893 fn test_generate_from_abi_with_source_bad_module_name() {
894 let temp_dir = TempDir::new().unwrap();
895 let abi_path = temp_dir.path().join("evil.json");
896 let source_path = temp_dir.path().join("evil.move");
897 let output_dir = temp_dir.path().join("generated");
898 write_file(&abi_path, &abi_json_with_name("has space"));
899 write_file(&source_path, "module 0x1::x {}");
900
901 let err = generate_from_abi_with_source(&abi_path, &source_path, &output_dir).unwrap_err();
902 assert!(matches!(err, AptosError::Config(ref m) if m.contains("only ASCII")));
903 }
904
905 #[test]
908 fn test_generate_from_directory_success() {
909 let temp_dir = TempDir::new().unwrap();
910 let abi_dir = temp_dir.path().join("abi");
911 let output_dir = temp_dir.path().join("generated");
912 write_file(&abi_dir.join("coin.json"), &abi_json_with_name("coin"));
913 write_file(&abi_dir.join("token.json"), &abi_json_with_name("token"));
914 write_file(&abi_dir.join("README.txt"), "ignore me");
916
917 generate_from_directory(&abi_dir, &output_dir).unwrap();
918
919 assert!(output_dir.join("coin.rs").exists());
920 assert!(output_dir.join("token.rs").exists());
921 assert!(output_dir.join("mod.rs").exists());
922 }
923
924 #[test]
925 fn test_generate_from_directory_no_json_files() {
926 let temp_dir = TempDir::new().unwrap();
927 let abi_dir = temp_dir.path().join("abi");
928 let output_dir = temp_dir.path().join("generated");
929 fs::create_dir_all(&abi_dir).unwrap();
930 write_file(&abi_dir.join("notes.txt"), "no abis here");
931
932 let err = generate_from_directory(&abi_dir, &output_dir).unwrap_err();
933 assert!(matches!(err, AptosError::Config(ref m) if m.contains("No JSON files found")));
934 }
935
936 #[test]
937 fn test_generate_from_directory_empty_dir() {
938 let temp_dir = TempDir::new().unwrap();
939 let abi_dir = temp_dir.path().join("abi");
940 let output_dir = temp_dir.path().join("generated");
941 fs::create_dir_all(&abi_dir).unwrap();
942
943 let err = generate_from_directory(&abi_dir, &output_dir).unwrap_err();
944 assert!(matches!(err, AptosError::Config(ref m) if m.contains("No JSON files found")));
945 }
946
947 #[test]
948 fn test_generate_from_directory_unreadable_dir() {
949 let temp_dir = TempDir::new().unwrap();
950 let abi_dir = temp_dir.path().join("nonexistent");
952 let output_dir = temp_dir.path().join("generated");
953
954 let err = generate_from_directory(&abi_dir, &output_dir).unwrap_err();
955 assert!(
956 matches!(err, AptosError::Config(ref m) if m.contains("Failed to read ABI directory"))
957 );
958 }
959
960 #[test]
963 fn test_generate_mod_file_header_and_reexports() {
964 let modules = vec!["alpha".to_string()];
965 let content = generate_mod_file(&modules);
966 assert!(content.contains("Auto-generated module exports"));
967 assert!(content.contains("Do not edit manually"));
968 assert!(content.contains("pub mod alpha;"));
969 assert!(content.contains("pub use alpha::*;"));
970 }
971}