1use std::process::Command;
33use std::path::PathBuf;
34use anyhow::{Result, Context};
35use thiserror::Error;
36
37#[derive(Error, Debug)]
39pub enum DidGeneratorError {
40 #[error("Failed to build canister: {0}")]
41 BuildError(String),
42 #[error("Failed to generate Candid file: {0}")]
43 CandidGenerationError(String),
44 #[error("Failed to write .did file: {0}")]
45 FileWriteError(String),
46}
47
48pub struct DidGenerator {
52 canister_dir: PathBuf,
53 canister_name: String,
54}
55
56impl DidGenerator {
57 pub fn new(canister_dir: PathBuf) -> Self {
63 let canister_name = canister_dir.file_name().unwrap().to_string_lossy().to_string();
64 Self {
65 canister_dir,
66 canister_name,
67 }
68 }
69
70 pub fn generate(&self) -> Result<()> {
81 println!("Generating .did file for canister: {}...", self.canister_name);
82
83 let did_path = self.canister_dir.join(format!("{}.did", self.canister_name));
84
85 let build_status = Command::new("cargo")
86 .current_dir(&self.canister_dir)
87 .args(["build", "--target", "wasm32-unknown-unknown", "--release"])
88 .status()
89 .context("Failed to execute cargo build command")?;
90
91 if !build_status.success() {
92 return Err(DidGeneratorError::BuildError(
93 "Failed to build canister".to_string(),
94 ).into());
95 }
96
97 let wasm_path = self.find_wasm_file()?;
98
99 println!("Found WASM file at: {}", wasm_path.display());
100
101 let output = Command::new("candid-extractor")
102 .arg(&wasm_path)
103 .output()
104 .context("Failed to execute candid-extractor")?;
105
106 if !output.status.success() {
107 return Err(DidGeneratorError::CandidGenerationError(
108 String::from_utf8_lossy(&output.stderr).to_string(),
109 ).into());
110 }
111
112 std::fs::write(&did_path, output.stdout)
113 .context(format!("Failed to write .did file to {}", did_path.display()))?;
114
115 println!(
116 "Candid file generated successfully: {}",
117 did_path.display()
118 );
119
120 Ok(())
121 }
122
123 fn find_wasm_file(&self) -> Result<PathBuf> {
125 let wasm_filename = format!("{}.wasm", self.canister_name);
126
127 let canister_wasm = self.canister_dir
128 .join("target/wasm32-unknown-unknown/release")
129 .join(&wasm_filename);
130
131 if canister_wasm.exists() {
132 return Ok(canister_wasm);
133 }
134
135 let mut current_dir = self.canister_dir.clone();
136 while let Some(parent) = current_dir.parent() {
137 let root_wasm = parent
138 .join("target/wasm32-unknown-unknown/release")
139 .join(&wasm_filename);
140
141 if root_wasm.exists() {
142 return Ok(root_wasm);
143 }
144
145 if parent == current_dir {
146 break;
147 }
148 current_dir = parent.to_path_buf();
149 }
150
151 Err(DidGeneratorError::BuildError(
152 format!("WASM file not found for canister '{}'. Tried:\n- {}\n- project root target directory",
153 self.canister_name,
154 self.canister_dir.join("target/wasm32-unknown-unknown/release").join(&wasm_filename).display())
155 ).into())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::fs;
163 use std::path::Path;
164
165 macro_rules! defer {
166 ($e:expr) => {
167 let _defer = Defer(Some(|| { let _ = $e; }));
168 };
169 }
170 struct Defer<F: FnOnce()>(Option<F>);
171 impl<F: FnOnce()> Drop for Defer<F> {
172 fn drop(&mut self) {
173 if let Some(f) = self.0.take() {
174 f();
175 }
176 }
177 }
178
179 fn setup_test_environment() -> Result<()> {
180 let test_canister_dir = Path::new("src/test_canister");
181 if !test_canister_dir.exists() {
182 fs::create_dir_all(test_canister_dir)?;
183 }
184 Ok(())
185 }
186
187 fn cleanup_test_environment() -> Result<()> {
188 let did_file = Path::new("src/test_canister/test_canister.did");
189 if did_file.exists() {
190 fs::remove_file(did_file)?;
191 }
192 Ok(())
193 }
194
195 #[test]
196 fn test_did_generator_creation() {
197 let generator = DidGenerator::new("test_canister".into());
198 assert_eq!(generator.canister_name, "test_canister");
199 }
200
201 #[test]
202 fn test_did_generation() -> Result<()> {
203 setup_test_environment()?;
204 defer!(cleanup_test_environment());
205
206 let generator = DidGenerator::new("test_canister".into());
207 generator.generate()?;
208
209 let did_path = Path::new("src/test_canister/test_canister.did");
210 assert!(did_path.exists(), "DID file was not created");
211
212 let did_content = fs::read_to_string(did_path)?;
213 assert!(!did_content.is_empty(), "DID file is empty");
214 assert!(did_content.contains("type User"), "DID file should contain User type");
215 assert!(did_content.contains("service"), "DID file should contain service definition");
216
217 Ok(())
218 }
219}