movement_sdk/codegen/
build_helper.rs1use crate::api::response::MoveModuleABI;
46use crate::codegen::{GeneratorConfig, ModuleGenerator, MoveSourceParser};
47use crate::error::{MovementError, MovementResult};
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) -> MovementResult<()> {
103 if name.is_empty() {
104 return Err(MovementError::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(MovementError::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(MovementError::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(MovementError::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) -> MovementResult<()> {
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) -> MovementResult<()> {
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 MovementError::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| MovementError::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| MovementError::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| MovementError::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) -> MovementResult<()> {
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) -> MovementResult<()> {
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 MovementError::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 MovementError::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).map_err(|e| {
348 MovementError::Config(format!("Failed to create output directory: {e}"))
349 })?;
350
351 let output_filename = format!("{}.rs", abi.name);
353 let output_path = output_dir.join(&output_filename);
354
355 fs::write(&output_path, &code)
356 .map_err(|e| MovementError::Config(format!("Failed to write output file: {e}")))?;
357
358 module_names.push(abi.name);
359
360 if config.print_cargo_instructions {
361 println!("cargo:rerun-if-changed={}", abi_path.display());
362 }
363 }
364
365 if config.generate_mod_file && !module_names.is_empty() {
367 let mod_content = generate_mod_file(&module_names);
368 let mod_path = output_dir.join("mod.rs");
369
370 fs::write(&mod_path, mod_content)
371 .map_err(|e| MovementError::Config(format!("Failed to write mod.rs: {e}")))?;
372 }
373
374 Ok(())
375}
376
377pub fn generate_from_abi_with_source(
395 abi_path: impl AsRef<Path>,
396 source_path: impl AsRef<Path>,
397 output_dir: impl AsRef<Path>,
398) -> MovementResult<()> {
399 let abi_path = abi_path.as_ref();
400 let source_path = source_path.as_ref();
401 let output_dir = output_dir.as_ref();
402
403 let abi_content = fs::read_to_string(abi_path)
405 .map_err(|e| MovementError::Config(format!("Failed to read ABI file: {e}")))?;
406
407 let abi: MoveModuleABI = serde_json::from_str(&abi_content)
408 .map_err(|e| MovementError::Config(format!("Failed to parse ABI JSON: {e}")))?;
409
410 let source_content = fs::read_to_string(source_path)
412 .map_err(|e| MovementError::Config(format!("Failed to read Move source: {e}")))?;
413
414 let source_info = MoveSourceParser::parse(&source_content);
415
416 validate_module_name(&abi.name)?;
418
419 let generator =
421 ModuleGenerator::new(&abi, GeneratorConfig::default()).with_source_info(source_info);
422 let code = generator.generate()?;
423
424 fs::create_dir_all(output_dir)
426 .map_err(|e| MovementError::Config(format!("Failed to create output directory: {e}")))?;
427
428 let output_filename = format!("{}.rs", abi.name);
430 let output_path = output_dir.join(&output_filename);
431
432 fs::write(&output_path, &code)
433 .map_err(|e| MovementError::Config(format!("Failed to write output file: {e}")))?;
434
435 println!("cargo:rerun-if-changed={}", abi_path.display());
436 println!("cargo:rerun-if-changed={}", source_path.display());
437
438 Ok(())
439}
440
441fn generate_mod_file(module_names: &[String]) -> String {
443 use std::fmt::Write as _;
444 let mut content = String::new();
445 let _ = writeln!(&mut content, "//! Auto-generated module exports.");
446 let _ = writeln!(&mut content, "//!");
447 let _ = writeln!(
448 &mut content,
449 "//! This file was auto-generated by movement-sdk codegen."
450 );
451 let _ = writeln!(&mut content, "//! Do not edit manually.");
452 let _ = writeln!(&mut content);
453
454 for name in module_names {
455 debug_assert!(
458 !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
459 "module name should have been validated"
460 );
461 let _ = writeln!(&mut content, "pub mod {name};");
462 }
463 let _ = writeln!(&mut content);
464
465 let _ = writeln!(&mut content, "// Re-exports for convenience");
467 for name in module_names {
468 let _ = writeln!(&mut content, "pub use {name}::*;");
469 }
470
471 content
472}
473
474pub fn generate_from_directory(
497 abi_dir: impl AsRef<Path>,
498 output_dir: impl AsRef<Path>,
499) -> MovementResult<()> {
500 let abi_dir = abi_dir.as_ref();
501
502 let entries = fs::read_dir(abi_dir)
503 .map_err(|e| MovementError::Config(format!("Failed to read ABI directory: {e}")))?;
504
505 let abi_paths: Vec<_> = entries
506 .filter_map(Result::ok)
507 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
508 .map(|e| e.path())
509 .collect();
510
511 if abi_paths.is_empty() {
512 return Err(MovementError::Config(format!(
513 "No JSON files found in {}",
514 abi_dir.display()
515 )));
516 }
517
518 let path_refs: Vec<&Path> = abi_paths.iter().map(std::path::PathBuf::as_path).collect();
520 generate_from_abis(&path_refs, output_dir)
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use std::io::Write;
527 use tempfile::TempDir;
528
529 fn sample_abi_json() -> &'static str {
530 r#"{
531 "address": "0x1",
532 "name": "coin",
533 "exposed_functions": [
534 {
535 "name": "transfer",
536 "visibility": "public",
537 "is_entry": true,
538 "is_view": false,
539 "generic_type_params": [{"constraints": []}],
540 "params": ["&signer", "address", "u64"],
541 "return": []
542 }
543 ],
544 "structs": []
545 }"#
546 }
547
548 #[test]
549 fn test_generate_from_abi() {
550 let temp_dir = TempDir::new().unwrap();
551 let abi_path = temp_dir.path().join("coin.json");
552 let output_dir = temp_dir.path().join("generated");
553
554 let mut file = fs::File::create(&abi_path).unwrap();
556 file.write_all(sample_abi_json().as_bytes()).unwrap();
557
558 let config = BuildConfig::new().with_cargo_instructions(false);
560 generate_from_abi_with_config(&abi_path, &output_dir, config).unwrap();
561
562 let output_path = output_dir.join("coin.rs");
564 assert!(output_path.exists());
565
566 let content = fs::read_to_string(&output_path).unwrap();
568 assert!(content.contains("Generated Rust bindings"));
569 assert!(content.contains("pub fn transfer"));
570 }
571
572 #[test]
573 fn test_generate_mod_file() {
574 let modules = vec!["coin".to_string(), "token".to_string()];
575 let mod_content = generate_mod_file(&modules);
576
577 assert!(mod_content.contains("pub mod coin;"));
578 assert!(mod_content.contains("pub mod token;"));
579 assert!(mod_content.contains("pub use coin::*;"));
580 assert!(mod_content.contains("pub use token::*;"));
581 }
582
583 #[test]
584 fn test_build_config() {
585 let config = BuildConfig::new()
586 .with_mod_file(false)
587 .with_cargo_instructions(false);
588
589 assert!(!config.generate_mod_file);
590 assert!(!config.print_cargo_instructions);
591 }
592}