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}