1use std::process::Command;
15use std::path::PathBuf;
16use anyhow::{Result, Context};
17use thiserror::Error;
18
19#[derive(Error, Debug)]
21pub enum DidGeneratorError {
22 #[error("Failed to build canister: {0}")]
23 BuildError(String),
24 #[error("Failed to generate Candid file: {0}")]
25 CandidGenerationError(String),
26 #[error("Failed to write .did file: {0}")]
27 FileWriteError(String),
28}
29
30pub struct DidGenerator {
34 canister_dir: PathBuf,
35 canister_name: String,
36}
37
38impl DidGenerator {
39 pub fn new(canister_dir: PathBuf) -> Self {
45 let canister_name = canister_dir.file_name().unwrap().to_string_lossy().to_string();
46 Self {
47 canister_dir,
48 canister_name,
49 }
50 }
51
52 pub fn generate(&self) -> Result<()> {
63 println!("Generating .did file for canister: {}...", self.canister_name);
64
65 let wasm_path = self.canister_dir.join("target/wasm32-unknown-unknown/release").join(format!("{}.wasm", self.canister_name));
66 let did_path = self.canister_dir.join(format!("{}.did", self.canister_name));
67
68 let build_status = Command::new("cargo")
70 .current_dir(&self.canister_dir)
71 .args(["build", "--target", "wasm32-unknown-unknown", "--release"])
72 .status()
73 .context("Failed to execute cargo build command")?;
74
75 if !build_status.success() {
76 return Err(DidGeneratorError::BuildError(
77 "Failed to build canister".to_string(),
78 ).into());
79 }
80
81 if !wasm_path.exists() {
83 return Err(DidGeneratorError::BuildError(
84 format!("WASM file not found at: {}", wasm_path.display())
85 ).into());
86 }
87
88 let output = Command::new("candid-extractor")
90 .arg(&wasm_path)
91 .output()
92 .context("Failed to execute candid-extractor")?;
93
94 if !output.status.success() {
95 return Err(DidGeneratorError::CandidGenerationError(
96 String::from_utf8_lossy(&output.stderr).to_string(),
97 ).into());
98 }
99
100 std::fs::write(&did_path, output.stdout)
102 .context(format!("Failed to write .did file to {}", did_path.display()))?;
103
104 println!(
105 "Candid file generated successfully: {}",
106 did_path.display()
107 );
108
109 Ok(())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::fs;
117 use std::path::Path;
118
119 macro_rules! defer {
121 ($e:expr) => {
122 let _defer = Defer(Some(|| { let _ = $e; }));
123 };
124 }
125 struct Defer<F: FnOnce()>(Option<F>);
126 impl<F: FnOnce()> Drop for Defer<F> {
127 fn drop(&mut self) {
128 if let Some(f) = self.0.take() {
129 f();
130 }
131 }
132 }
133
134 fn setup_test_environment() -> Result<()> {
135 let test_canister_dir = Path::new("src/test_canister");
137 if !test_canister_dir.exists() {
138 fs::create_dir_all(test_canister_dir)?;
139 }
140 Ok(())
141 }
142
143 fn cleanup_test_environment() -> Result<()> {
144 let did_file = Path::new("src/test_canister/test_canister.did");
146 if did_file.exists() {
147 fs::remove_file(did_file)?;
148 }
149 Ok(())
150 }
151
152 #[test]
153 fn test_did_generator_creation() {
154 let generator = DidGenerator::new("test_canister".into());
155 assert_eq!(generator.canister_name, "test_canister");
156 }
157
158 #[test]
159 fn test_did_generation() -> Result<()> {
160 setup_test_environment()?;
161 defer!(cleanup_test_environment());
162
163 let generator = DidGenerator::new("test_canister".into());
164 generator.generate()?;
165
166 let did_path = Path::new("src/test_canister/test_canister.did");
168 assert!(did_path.exists(), "DID file was not created");
169
170 let did_content = fs::read_to_string(did_path)?;
172 assert!(!did_content.is_empty(), "DID file is empty");
173 assert!(did_content.contains("type User"), "DID file should contain User type");
174 assert!(did_content.contains("service"), "DID file should contain service definition");
175
176 Ok(())
177 }
178}