Skip to main content

ic_cdk_bindgen/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2#![doc = include_str!("../README.md")]
3
4use candid::Principal;
5use candid_parser::bindings::rust::{
6    Config as BindgenConfig, ExternalConfig, emit_bindgen, output_handlebar,
7};
8use candid_parser::configs::Configs;
9use candid_parser::pretty_check_file;
10
11use std::fs;
12use std::io::Write;
13use std::path::PathBuf;
14use std::str::FromStr;
15
16/// Config for Candid to Rust bindings generation.
17///
18/// # Choose Bindgen Modes
19///
20/// The bindgen has following modes:
21/// - Types only: Only the types definition will be generated. This is the default behavior with [`Self::new`].
22/// - Static callee: The canister ID is known at compile time. Call [`Self::static_callee`] to set it.
23/// - Dynamic callee: The canister ID is determined at runtime via ICP environment variable. Call [`Self::dynamic_callee`] to set it.
24///
25/// # Generate Bindings
26///
27/// After configuring your bindgen settings through the methods above, you must call
28/// [`Self::generate`] to actually produce the Rust bindings.
29#[derive(Debug)]
30pub struct Config {
31    canister_name: String,
32    candid_path: PathBuf,
33    mode: Mode,
34    type_selector_config_path: Option<PathBuf>, // TODO: Implement type selector config
35}
36
37/// Bindgen mode.
38#[derive(Debug)]
39enum Mode {
40    TypesOnly,
41    StaticCallee { canister_id: Principal },
42    DynamicCallee { env_var_name: String },
43}
44
45impl Config {
46    /// Create a new `Config` instance.
47    ///
48    /// # Arguments
49    /// - `canister_name` - The name of the canister. This will be used as the generated file name.
50    ///   It is important to ensure that this name is valid for use in a file system (no
51    ///   spaces, special characters, or other characters that could cause issues with file paths).
52    /// - `candid_path` - The path to the Candid file.
53    pub fn new<N, P>(canister_name: N, candid_path: P) -> Self
54    where
55        N: Into<String>,
56        P: Into<PathBuf>,
57    {
58        Self {
59            canister_name: canister_name.into(),
60            candid_path: candid_path.into(),
61            mode: Mode::TypesOnly,
62            type_selector_config_path: None,
63        }
64    }
65
66    /// Changes the bindgen mode to "Static callee", where the canister ID is known at compile time.
67    ///
68    /// This mode hardcodes the target canister ID in the generated code, making it suitable
69    /// for deployments where the canister ID is fixed and known at compile time.
70    ///
71    /// # Arguments
72    ///
73    /// - `canister_id` - The Principal ID of the target canister
74    pub fn static_callee<S>(&mut self, canister_id: S) -> &mut Self
75    where
76        S: Into<Principal>,
77    {
78        if !matches!(self.mode, Mode::TypesOnly) {
79            panic!("The bindgen mode has already been set.");
80        }
81        self.mode = Mode::StaticCallee {
82            canister_id: canister_id.into(),
83        };
84        self
85    }
86
87    /// Changes the bindgen mode to "Dynamic callee", where the canister ID is determined at runtime.
88    ///
89    /// This mode allows the canister ID to be resolved dynamically from an Internet Computer (ICP)
90    /// environment variable, making it suitable for deployments where the target canister ID
91    /// may change across environments.
92    ///
93    /// # Arguments
94    ///
95    /// - `env_var_name` - The name of the ICP environment variable containing the canister ID.
96    pub fn dynamic_callee<S>(&mut self, env_var_name: S) -> &mut Self
97    where
98        S: Into<String>,
99    {
100        if !matches!(self.mode, Mode::TypesOnly) {
101            panic!("The bindgen mode has already been set.");
102        }
103        self.mode = Mode::DynamicCallee {
104            env_var_name: env_var_name.into(),
105        };
106        self
107    }
108
109    /// Sets the path to the type selector configuration file.
110    ///
111    /// The "type selector config" is a TOML file that specifies how certain Candid types
112    /// should be mapped to Rust types (attributes, visibility, etc.). Please refer to the
113    /// [specification](https://github.com/dfinity/candid/blob/master/spec/Type-selector.md#rust-binding-configuration)
114    /// for more details.
115    pub fn set_type_selector_config<P>(&mut self, path: P) -> &mut Self
116    where
117        P: Into<PathBuf>,
118    {
119        self.type_selector_config_path = Some(path.into());
120        self
121    }
122
123    /// Generate the bindings.
124    ///
125    /// The generated bindings will be written to the output directory specified by the
126    /// `OUT_DIR` environment variable. The file will be named after the canister name.
127    /// For example, if the canister name is "my_canister", the generated file will be
128    /// located at `$OUT_DIR/my_canister.rs`.
129    pub fn generate(&self) {
130        // 0. Load type selector config if provided
131        let type_selector_configs_str = match &self.type_selector_config_path {
132            Some(p) => {
133                println!("cargo:rerun-if-changed={}", p.display());
134                fs::read_to_string(p).unwrap_or_else(|e| {
135                    panic!(
136                        "failed to read the type selector config file ({}): {}",
137                        p.display(),
138                        e
139                    )
140                })
141            }
142            None => "".to_string(),
143        };
144        let type_selector_configs = Configs::from_str(&type_selector_configs_str)
145            .unwrap_or_else(|e| panic!("failed to parse the type selector config: {}", e));
146        let rust_bindgen_config = BindgenConfig::new(type_selector_configs);
147
148        // 1. Parse the candid file and generate the Output (the struct for bindings)
149        // This tells Cargo to re-run the build-script if the Candid file changes.
150        println!("cargo:rerun-if-changed={}", self.candid_path.display());
151        let (env, actor, prog) = pretty_check_file(&self.candid_path).unwrap_or_else(|e| {
152            panic!(
153                "failed to parse candid file ({}): {}",
154                self.candid_path.display(),
155                e
156            )
157        });
158        // unused are not handled
159        let (output, _unused) = emit_bindgen(&rust_bindgen_config, &env, &actor, &prog);
160
161        // 2. Generate the Rust bindings using the Handlebars template
162        let mut external = ExternalConfig::default();
163        let content = match &self.mode {
164            Mode::StaticCallee { canister_id } => {
165                let template = include_str!("templates/static_callee.hbs");
166                external
167                    .0
168                    .insert("canister_id".to_string(), canister_id.to_string());
169                output_handlebar(output, external, template)
170            }
171            Mode::DynamicCallee { env_var_name } => {
172                let template = include_str!("templates/dynamic_callee.hbs");
173                external
174                    .0
175                    .insert("env_var_name".to_string(), env_var_name.to_string());
176                output_handlebar(output, external, template)
177            }
178            Mode::TypesOnly => {
179                let template = include_str!("templates/types_only.hbs");
180                output_handlebar(output, external, template)
181            }
182        };
183
184        // 3. Write the generated Rust bindings to the output directory
185        let out_dir_str = std::env::var("OUT_DIR")
186            .expect("OUT_DIR should always be set when execute the build.rs script");
187        let out_dir = PathBuf::from(out_dir_str);
188        let generated_path = out_dir.join(format!("{}.rs", self.canister_name));
189        let mut file = fs::File::create(&generated_path).unwrap_or_else(|e| {
190            panic!(
191                "failed to create the output file ({}): {}",
192                generated_path.display(),
193                e
194            )
195        });
196        writeln!(file, "{content}").unwrap_or_else(|e| {
197            panic!(
198                "failed to write to the output file ({}): {}",
199                generated_path.display(),
200                e
201            )
202        });
203    }
204}