ic_host_tools/candid/service/
mod.rs1use crate::interface::{ExportedMethod, MethodMode};
8use ic_platform::candid_parser::{self, Dec, FuncMode, IDLProg, TypeEnv};
9use std::fmt;
10
11#[cfg(test)]
12mod tests;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct ServiceLimits {
21 pub source_bytes: usize,
23 pub methods: usize,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct ServiceMethod {
30 pub name: String,
32 pub mode: MethodMode,
34}
35
36impl ServiceMethod {
37 #[must_use]
40 pub fn as_export(&self) -> ExportedMethod<'_> {
41 ExportedMethod {
42 name: &self.name,
43 mode: self.mode.export_mode(),
44 }
45 }
46}
47
48#[derive(Debug)]
50pub struct ParseError {
51 source: candid_parser::Error,
52}
53
54impl fmt::Display for ParseError {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 self.source.fmt(formatter)
57 }
58}
59impl std::error::Error for ParseError {}
60
61#[derive(Debug)]
63pub enum ServiceError {
64 InputLimit {
66 actual: usize,
68 limit: usize,
70 },
71 MethodLimit {
73 actual: usize,
75 limit: usize,
77 },
78 ImportsUnsupported,
80 MissingService,
82 Parse(ParseError),
84 Allocation(std::collections::TryReserveError),
86}
87
88impl fmt::Display for ServiceError {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::InputLimit { actual, limit } => write!(
92 formatter,
93 "Candid source has {actual} bytes, exceeding {limit}"
94 ),
95 Self::MethodLimit { actual, limit } => write!(
96 formatter,
97 "Candid service has {actual} methods, exceeding {limit}"
98 ),
99 Self::ImportsUnsupported => {
100 formatter.write_str("Candid imports require caller resolution")
101 }
102 Self::MissingService => formatter.write_str("Candid document has no service"),
103 Self::Parse(source) => source.fmt(formatter),
104 Self::Allocation(_) => formatter.write_str("Candid inventory allocation failed"),
105 }
106 }
107}
108
109impl std::error::Error for ServiceError {
110 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111 match self {
112 Self::Parse(source) => Some(source),
113 Self::Allocation(source) => Some(source),
114 _ => None,
115 }
116 }
117}
118
119const fn parse_error(source: candid_parser::Error) -> ServiceError {
120 ServiceError::Parse(ParseError { source })
121}
122
123pub fn inspect(source: &str, limits: ServiceLimits) -> Result<Vec<ServiceMethod>, ServiceError> {
134 if source.len() > limits.source_bytes {
135 return Err(ServiceError::InputLimit {
136 actual: source.len(),
137 limit: limits.source_bytes,
138 });
139 }
140 let program = source.parse::<IDLProg>().map_err(parse_error)?;
141 if program
142 .decs
143 .iter()
144 .any(|declaration| matches!(declaration, Dec::ImportType(_) | Dec::ImportServ(_)))
145 {
146 return Err(ServiceError::ImportsUnsupported);
147 }
148 let mut environment = TypeEnv::new();
149 let actor = candid_parser::check_prog(&mut environment, &program)
150 .map_err(parse_error)?
151 .ok_or(ServiceError::MissingService)?;
152 let methods = environment
153 .as_service(&actor)
154 .map_err(|error| parse_error(error.into()))?;
155 if methods.len() > limits.methods {
156 return Err(ServiceError::MethodLimit {
157 actual: methods.len(),
158 limit: limits.methods,
159 });
160 }
161 let mut result = Vec::new();
162 result
163 .try_reserve_exact(methods.len())
164 .map_err(ServiceError::Allocation)?;
165 for (name, ty) in methods {
166 let function = environment
167 .as_func(ty)
168 .map_err(|error| parse_error(error.into()))?;
169 let mode = match function.modes.first() {
170 None => MethodMode::Update,
171 Some(FuncMode::Query) => MethodMode::Query,
172 Some(FuncMode::CompositeQuery) => MethodMode::CompositeQuery,
173 Some(FuncMode::Oneway) => MethodMode::Oneway,
174 };
175 let mut owned_name = String::new();
176 owned_name
177 .try_reserve_exact(name.len())
178 .map_err(ServiceError::Allocation)?;
179 owned_name.push_str(name);
180 result.push(ServiceMethod {
181 name: owned_name,
182 mode,
183 });
184 }
185 Ok(result)
186}