use super::{PocketIc, PocketIcCallFailure, RejectResponse, transport};
use ic_platform::Principal;
use ic_wire::Method;
use serde::de::DeserializeOwned;
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WireCallContext {
pub operation: &'static str,
pub canister_id: Principal,
pub caller: Principal,
pub method: &'static str,
pub method_id: u64,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum WireCallError {
Encode {
context: Box<WireCallContext>,
source: ic_wire::Error,
},
Call {
context: Box<WireCallContext>,
source: Box<PocketIcCallFailure>,
},
Decode {
context: Box<WireCallContext>,
bytes: usize,
source: ic_wire::Error,
},
}
impl std::fmt::Display for WireCallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let context = match self {
Self::Encode { context, source } => {
write!(f, "Wire request encoding failed: {source}")?;
context
}
Self::Call { context, source } => {
match source.as_ref() {
PocketIcCallFailure::Rejected(response) => write!(
f,
"PocketIC call rejected ({:?}, {:?})",
response.reject_code, response.error_code
)?,
PocketIcCallFailure::Operation(_) => {
f.write_str("PocketIC transport failed")?;
}
}
context
}
Self::Decode {
context,
bytes,
source,
} => {
write!(f, "Wire response decoding failed (bytes={bytes}): {source}")?;
context
}
};
write!(
f,
" (operation={}, canister={}, caller={}, method={}, method_id={})",
context.operation,
context.canister_id,
context.caller,
context.method,
context.method_id
)
}
}
impl std::error::Error for WireCallError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Encode { source, .. } | Self::Decode { source, .. } => Some(source),
Self::Call { source, .. } => Some(source.as_ref()),
}
}
}
pub fn query_wire<M: Method>(
pic: &PocketIc,
canister_id: Principal,
caller: Principal,
request: &M::Request,
) -> Result<M::Response, WireCallError>
where
M::Response: DeserializeOwned,
{
call_wire::<M>(
context::<M>("query_call", canister_id, caller),
request,
|bytes| pic.query_call(canister_id, caller, M::NAME, bytes),
)
}
pub fn update_wire<M: Method>(
pic: &PocketIc,
canister_id: Principal,
caller: Principal,
request: &M::Request,
) -> Result<M::Response, WireCallError>
where
M::Response: DeserializeOwned,
{
call_wire::<M>(
context::<M>("update_call", canister_id, caller),
request,
|bytes| pic.update_call(canister_id, caller, M::NAME, bytes),
)
}
const fn context<M: Method>(
operation: &'static str,
canister_id: Principal,
caller: Principal,
) -> WireCallContext {
WireCallContext {
operation,
canister_id,
caller,
method: M::NAME,
method_id: M::ID,
}
}
fn call_wire<M: Method>(
context: WireCallContext,
request: &M::Request,
call: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
) -> Result<M::Response, WireCallError>
where
M::Response: DeserializeOwned,
{
let request =
ic_wire::encode_request::<M>(request).map_err(|source| WireCallError::Encode {
context: Box::new(context.clone()),
source,
})?;
let reply = transport::call_or_resume(|| call(request.into_bytes())).map_err(|source| {
WireCallError::Call {
context: Box::new(context.clone()),
source: Box::new(source),
}
})?;
let response = if reply.len() > M::RESPONSE_LIMITS.bytes {
Err(ic_wire::Error::Limit)
} else {
ic_wire::decode_response::<M>(&reply)
};
response.map_err(|source| WireCallError::Decode {
context: Box::new(context),
bytes: reply.len(),
source,
})
}
#[cfg(test)]
mod tests;