Skip to main content

canic_core/api/call/
mod.rs

1//! Module: api::call
2//!
3//! Responsibility: expose Canic's instrumented inter-canister call builder.
4//! Does not own: call policy, transport mechanics, or protected Canic RPC.
5//! Boundary: maps the IC call workflow and its typed failures into the public API.
6
7use crate::{
8    dto::error::Error,
9    workflow::ic::call::{
10        CallBuilder as WorkflowCallBuilder, CallResult as WorkflowCallResult, CallWorkflow,
11    },
12};
13use candid::{
14    CandidType, Principal,
15    utils::{ArgumentDecoder, ArgumentEncoder},
16};
17use serde::de::DeserializeOwned;
18use std::borrow::Cow;
19
20/// Entry point for constructing an instrumented inter-canister call.
21pub struct Call;
22
23impl Call {
24    /// Construct a call that uses the IC's bounded-wait behavior.
25    #[must_use]
26    pub fn bounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder<'static> {
27        CallBuilder {
28            inner: CallWorkflow::bounded_wait(canister_id, method),
29        }
30    }
31
32    /// Construct a call that uses the IC's unbounded-wait behavior.
33    #[must_use]
34    pub fn unbounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder<'static> {
35        CallBuilder {
36            inner: CallWorkflow::unbounded_wait(canister_id, method),
37        }
38    }
39}
40
41/// Public builder for one instrumented inter-canister call.
42pub struct CallBuilder<'a> {
43    inner: WorkflowCallBuilder<'a>,
44}
45
46impl CallBuilder<'_> {
47    /// Encode one Candid argument.
48    pub fn with_arg<A>(self, arg: A) -> Result<Self, Error>
49    where
50        A: CandidType,
51    {
52        Ok(Self {
53            inner: self.inner.with_arg(arg).map_err(Error::from)?,
54        })
55    }
56
57    /// Encode a Candid argument tuple.
58    pub fn with_args<A>(self, args: A) -> Result<Self, Error>
59    where
60        A: ArgumentEncoder,
61    {
62        Ok(Self {
63            inner: self.inner.with_args(args).map_err(Error::from)?,
64        })
65    }
66
67    /// Supply pre-encoded Candid arguments without validating them.
68    #[must_use]
69    pub fn with_raw_args<'b>(self, args: impl Into<Cow<'b, [u8]>>) -> CallBuilder<'b> {
70        CallBuilder {
71            inner: self.inner.with_raw_args(args),
72        }
73    }
74
75    /// Attach cycles to the call.
76    #[must_use]
77    pub fn with_cycles(self, cycles: u128) -> Self {
78        Self {
79            inner: self.inner.with_cycles(cycles),
80        }
81    }
82
83    /// Execute the configured call.
84    pub async fn execute(self) -> Result<CallResult, Error> {
85        Ok(CallResult {
86            inner: self.inner.execute().await.map_err(Error::from)?,
87        })
88    }
89
90    /// Execute the configured call and decode one Candid response value.
91    pub async fn execute_candid<R>(self) -> Result<R, Error>
92    where
93        R: CandidType + DeserializeOwned,
94    {
95        self.execute().await?.candid()
96    }
97
98    /// Execute the configured call and decode a Candid response tuple.
99    pub async fn execute_candid_tuple<R>(self) -> Result<R, Error>
100    where
101        R: for<'de> ArgumentDecoder<'de>,
102    {
103        self.execute().await?.candid_tuple()
104    }
105}
106
107/// Public response wrapper with typed Candid decoding.
108pub struct CallResult {
109    inner: WorkflowCallResult,
110}
111
112impl CallResult {
113    /// Decode the response as one Candid value.
114    pub fn candid<R>(&self) -> Result<R, Error>
115    where
116        R: CandidType + DeserializeOwned,
117    {
118        self.inner.candid().map_err(Error::from)
119    }
120
121    /// Decode the response as a Candid tuple.
122    pub fn candid_tuple<R>(&self) -> Result<R, Error>
123    where
124        R: for<'de> ArgumentDecoder<'de>,
125    {
126        self.inner.candid_tuple().map_err(Error::from)
127    }
128}
129
130// -----------------------------------------------------------------------------
131// Tests
132// -----------------------------------------------------------------------------
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::ops::runtime::metrics::inter_canister_call::{
138        InterCanisterCallMetricKey, InterCanisterCallMetrics,
139    };
140    use std::collections::HashMap;
141
142    #[test]
143    fn public_builder_routes_through_instrumented_call_authority() {
144        InterCanisterCallMetrics::reset();
145        let target = Principal::from_slice(&[7; 29]);
146
147        let _builder = Call::bounded_wait(target, "example")
148            .with_arg(42_u64)
149            .expect("encode one argument")
150            .with_cycles(1_000);
151
152        let counts: HashMap<_, _> = InterCanisterCallMetrics::snapshot()
153            .entries
154            .into_iter()
155            .collect();
156        assert_eq!(
157            counts.get(&InterCanisterCallMetricKey {
158                target,
159                method: "example".to_string(),
160            }),
161            Some(&1)
162        );
163    }
164}