use crate::interface::{ExportedMethod, MethodMode};
use candid_parser::{IDLProg, TypeEnv, syntax::Dec, types::FuncMode};
use std::fmt;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ServiceLimits {
pub source_bytes: usize,
pub methods: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceMethod {
pub name: String,
pub mode: MethodMode,
}
impl ServiceMethod {
#[must_use]
pub fn as_export(&self) -> ExportedMethod<'_> {
ExportedMethod {
name: &self.name,
mode: self.mode.export_mode(),
}
}
}
#[derive(Debug)]
pub struct ParseError {
source: candid_parser::Error,
}
impl fmt::Display for ParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.source.fmt(formatter)
}
}
impl std::error::Error for ParseError {}
#[derive(Debug)]
pub enum ServiceError {
InputLimit {
actual: usize,
limit: usize,
},
MethodLimit {
actual: usize,
limit: usize,
},
ImportsUnsupported,
MissingService,
Parse(ParseError),
Allocation(std::collections::TryReserveError),
}
impl fmt::Display for ServiceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputLimit { actual, limit } => write!(
formatter,
"Candid source has {actual} bytes, exceeding {limit}"
),
Self::MethodLimit { actual, limit } => write!(
formatter,
"Candid service has {actual} methods, exceeding {limit}"
),
Self::ImportsUnsupported => {
formatter.write_str("Candid imports require caller resolution")
}
Self::MissingService => formatter.write_str("Candid document has no service"),
Self::Parse(source) => source.fmt(formatter),
Self::Allocation(_) => formatter.write_str("Candid inventory allocation failed"),
}
}
}
impl std::error::Error for ServiceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(source) => Some(source),
Self::Allocation(source) => Some(source),
_ => None,
}
}
}
const fn parse_error(source: candid_parser::Error) -> ServiceError {
ServiceError::Parse(ParseError { source })
}
pub fn inspect(source: &str, limits: ServiceLimits) -> Result<Vec<ServiceMethod>, ServiceError> {
if source.len() > limits.source_bytes {
return Err(ServiceError::InputLimit {
actual: source.len(),
limit: limits.source_bytes,
});
}
let program = source.parse::<IDLProg>().map_err(parse_error)?;
if program
.decs
.iter()
.any(|declaration| matches!(declaration, Dec::ImportType(_) | Dec::ImportServ(_)))
{
return Err(ServiceError::ImportsUnsupported);
}
let mut environment = TypeEnv::new();
let actor = candid_parser::check_prog(&mut environment, &program)
.map_err(parse_error)?
.ok_or(ServiceError::MissingService)?;
let methods = environment
.as_service(&actor)
.map_err(|error| parse_error(error.into()))?;
if methods.len() > limits.methods {
return Err(ServiceError::MethodLimit {
actual: methods.len(),
limit: limits.methods,
});
}
let mut result = Vec::new();
result
.try_reserve_exact(methods.len())
.map_err(ServiceError::Allocation)?;
for (name, ty) in methods {
let function = environment
.as_func(ty)
.map_err(|error| parse_error(error.into()))?;
let mode = match function.modes.first() {
None => MethodMode::Update,
Some(FuncMode::Query) => MethodMode::Query,
Some(FuncMode::CompositeQuery) => MethodMode::CompositeQuery,
Some(FuncMode::Oneway) => MethodMode::Oneway,
};
let mut owned_name = String::new();
owned_name
.try_reserve_exact(name.len())
.map_err(ServiceError::Allocation)?;
owned_name.push_str(name);
result.push(ServiceMethod {
name: owned_name,
mode,
});
}
Ok(result)
}