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
91/// Public response wrapper with typed Candid decoding.
92pub struct CallResult {
93    inner: WorkflowCallResult,
94}
95
96impl CallResult {
97    /// Decode the response as one Candid value.
98    pub fn candid<R>(&self) -> Result<R, Error>
99    where
100        R: CandidType + DeserializeOwned,
101    {
102        self.inner.candid().map_err(Error::from)
103    }
104
105    /// Decode the response as a Candid tuple.
106    pub fn candid_tuple<R>(&self) -> Result<R, Error>
107    where
108        R: for<'de> ArgumentDecoder<'de>,
109    {
110        self.inner.candid_tuple().map_err(Error::from)
111    }
112}
113
114// -----------------------------------------------------------------------------
115// Tests
116// -----------------------------------------------------------------------------
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::ops::runtime::metrics::inter_canister_call::{
122        InterCanisterCallMetricKey, InterCanisterCallMetrics,
123    };
124    use std::collections::HashMap;
125
126    #[test]
127    fn public_builder_routes_through_instrumented_call_authority() {
128        InterCanisterCallMetrics::reset();
129        let target = Principal::from_slice(&[7; 29]);
130
131        let _builder = Call::bounded_wait(target, "example")
132            .with_arg(42_u64)
133            .expect("encode one argument")
134            .with_cycles(1_000);
135
136        let counts: HashMap<_, _> = InterCanisterCallMetrics::snapshot()
137            .entries
138            .into_iter()
139            .collect();
140        assert_eq!(
141            counts.get(&InterCanisterCallMetricKey {
142                target,
143                method: "example".to_string(),
144            }),
145            Some(&1)
146        );
147    }
148}