use super::{PocketIcCallFailure, transport};
use ic_platform::pocket_ic::{PocketIc, RejectResponse};
use ic_platform::principal::ExternalPrincipal as Principal;
use ic_wire::{Limits, native};
use serde::{Serialize, de::DeserializeOwned};
#[derive(Clone, Copy, Debug)]
pub struct WireCallSpec {
pub method: &'static str,
pub method_id: u64,
pub request_limits: Limits,
pub response_limits: Limits,
}
#[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<Request, Response>(
pic: &PocketIc,
canister_id: Principal,
caller: Principal,
spec: WireCallSpec,
request: &Request,
) -> Result<Response, WireCallError>
where
Request: Serialize + ?Sized,
Response: DeserializeOwned,
{
call_wire(
context("query_call", spec, canister_id, caller),
spec,
request,
|bytes| pic.query_call(canister_id, caller, spec.method, bytes),
)
}
pub fn update_wire<Request, Response>(
pic: &PocketIc,
canister_id: Principal,
caller: Principal,
spec: WireCallSpec,
request: &Request,
) -> Result<Response, WireCallError>
where
Request: Serialize + ?Sized,
Response: DeserializeOwned,
{
call_wire(
context("update_call", spec, canister_id, caller),
spec,
request,
|bytes| pic.update_call(canister_id, caller, spec.method, bytes),
)
}
const fn context(
operation: &'static str,
spec: WireCallSpec,
canister_id: Principal,
caller: Principal,
) -> WireCallContext {
WireCallContext {
operation,
canister_id,
caller,
method: spec.method,
method_id: spec.method_id,
}
}
fn call_wire<Request, Response>(
context: WireCallContext,
spec: WireCallSpec,
request: &Request,
call: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
) -> Result<Response, WireCallError>
where
Request: Serialize + ?Sized,
Response: DeserializeOwned,
{
let request =
native::encode_request(spec.method_id, request, spec.request_limits).map_err(|source| {
WireCallError::Encode {
context: Box::new(context.clone()),
source,
}
})?;
let reply =
transport::try_pocket_ic_call(|| call(request)).map_err(|source| WireCallError::Call {
context: Box::new(context.clone()),
source: Box::new(source),
})?;
native::decode_response(spec.method_id, &reply, spec.response_limits).map_err(|source| {
WireCallError::Decode {
context: Box::new(context),
bytes: reply.len(),
source,
}
})
}
#[cfg(test)]
mod tests;