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//! ## Example
9//!
10//! ```sh
11//! generate-did <canister_name>
12//! ```
13
14use std::process::Command;
15use std::path::PathBuf;
16use anyhow::{Result, Context};
17use thiserror::Error;
18
19/// Errors that can occur during DID generation.
20#[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
30/// A struct for generating Candid (.did) files for Internet Computer canisters.
31///
32/// Most users should use the CLI (`generate-did <canister_name>`) instead of this struct directly.
33pub struct DidGenerator {
34    canister_dir: PathBuf,
35    canister_name: String,
36}
37
38impl DidGenerator {
39    /// Creates a new DidGenerator instance.
40    ///
41    /// # Arguments
42    ///
43    /// * `canister_dir` - The path to the canister directory
44    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    /// Generates the .did file for the specified canister.
53    ///
54    /// This function:
55    /// 1. Builds the Rust canister
56    /// 2. Extracts the Candid interface using candid-extractor
57    /// 3. Writes the interface to a .did file
58    ///
59    /// # Returns
60    ///
61    /// * `Result<()>` - Ok(()) if successful, Err if any step fails
62    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        // Build the Rust canister
69        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        // Verify the WASM file exists
82        if !wasm_path.exists() {
83            return Err(DidGeneratorError::BuildError(
84                format!("WASM file not found at: {}", wasm_path.display())
85            ).into());
86        }
87
88        // Generate the Candid file
89        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        // Write the output to the .did file
101        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    // Helper macro for cleanup (only used in tests)
120    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        // Ensure the test canister directory exists
136        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        // Clean up generated files
145        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        // Verify that the .did file was created
167        let did_path = Path::new("src/test_canister/test_canister.did");
168        assert!(did_path.exists(), "DID file was not created");
169
170        // Read and verify the content of the .did file
171        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}