generate_did/
lib.rs

1//!
2//! # generate-did
3//!
4//! `generate-did` is a CLI tool to generate Candid (`.did`) files for Internet Computer Rust canisters.
5//!
6//! See the [README](https://github.com/Stephen-Kimoi/generate-did) for CLI usage and installation instructions.
7//! 
8//! ## Installing candid-extractor
9//! 
10//! Install the `candid-extractor` crate
11//! ```sh
12//! cargo install candid-extractor
13//! ```
14//! 
15//! Call the `export_candid` macro at the end of your lib.rs file
16//! ```rust
17//! // Enable Candid export
18//! ic_cdk::export_candid!();
19//! ```
20//!
21//! ## Install generate-did
22//! 
23//! ```sh
24//! cargo install generate-did
25//! ```
26//! Then run this command in the root of the canister project.
27//!
28//! ```sh
29//! generate-did <canister_name>
30//! ```
31
32use std::process::Command;
33use std::path::PathBuf;
34use anyhow::{Result, Context};
35use thiserror::Error;
36
37/// Errors that can occur during DID generation.
38#[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
48/// A struct for generating Candid (.did) files for Internet Computer canisters.
49///
50/// Most users should use the CLI (`generate-did <canister_name>`) instead of this struct directly.
51pub struct DidGenerator {
52    canister_dir: PathBuf,
53    canister_name: String,
54}
55
56impl DidGenerator {
57    /// Creates a new DidGenerator instance.
58    ///
59    /// # Arguments
60    ///
61    /// * `canister_dir` - The path to the canister directory
62    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    /// Generates the .did file for the specified canister.
71    ///
72    /// This function:
73    /// 1. Builds the Rust canister
74    /// 2. Extracts the Candid interface using candid-extractor
75    /// 3. Writes the interface to a .did file
76    ///
77    /// # Returns
78    ///
79    /// * `Result<()>` - Ok(()) if successful, Err if any step fails
80    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    /// Find the WASM file in the appropriate location
124    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}