1use std::{collections::HashMap, fmt, path::Path, sync::Arc};
13
14use thiserror::Error;
15
16use crate::{
17 diagnostic::LimitKind,
18 eval::context::{EnvVarMode, EvalLimits},
19 ir::Value,
20};
21
22#[derive(Debug)]
29#[non_exhaustive]
30pub struct CallCx<'a> {
31 pub workspace_root: &'a Path,
33 pub env_vars: &'a EnvVarMode,
35 pub limits: &'a EvalLimits,
38}
39
40impl<'a> CallCx<'a> {
41 #[must_use]
43 pub const fn new(
44 workspace_root: &'a Path,
45 env_vars: &'a EnvVarMode,
46 limits: &'a EvalLimits,
47 ) -> Self {
48 Self {
49 workspace_root,
50 env_vars,
51 limits,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Error, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum FuncError {
67 #[error("`{name}`: expected {expected} arg(s), got {got}")]
70 Arity {
71 name: Arc<str>,
73 expected: usize,
75 got: usize,
77 },
78
79 #[error("`{name}` arg #{index}: expected {expected}, got {got}")]
81 Type {
82 name: Arc<str>,
84 index: usize,
86 expected: &'static str,
88 got: &'static str,
90 },
91
92 #[error("`{name}` limit ({kind:?}): observed {observed} > {limit}")]
94 Limit {
95 name: Arc<str>,
97 kind: LimitKind,
99 observed: u64,
101 limit: u64,
103 },
104
105 #[error("`{name}` path escape: `{path}`")]
108 PathEscape {
109 name: &'static str,
111 path: std::path::PathBuf,
113 },
114
115 #[error("`{name}`: {message}")]
118 Other {
119 name: Arc<str>,
121 message: Arc<str>,
123 },
124}
125
126pub trait HclFunc: fmt::Debug + Send + Sync + 'static {
134 fn call(&self, args: &[Value], cx: &CallCx<'_>) -> Result<Value, FuncError>;
147}
148
149#[derive(Default)]
156pub struct FuncRegistry {
157 funcs: HashMap<Arc<str>, Arc<dyn HclFunc>>,
158}
159
160impl FuncRegistry {
161 #[must_use]
163 pub fn get(&self, name: &str) -> Option<&Arc<dyn HclFunc>> {
164 self.funcs.get(name)
165 }
166
167 #[must_use]
169 pub fn contains(&self, name: &str) -> bool {
170 self.funcs.contains_key(name)
171 }
172
173 pub fn iter(&self) -> impl Iterator<Item = (&Arc<str>, &Arc<dyn HclFunc>)> {
176 self.funcs.iter()
177 }
178
179 #[must_use]
181 pub fn len(&self) -> usize {
182 self.funcs.len()
183 }
184
185 #[must_use]
187 pub fn is_empty(&self) -> bool {
188 self.funcs.is_empty()
189 }
190
191 #[must_use]
194 pub fn to_builder(&self) -> FuncRegistryBuilder {
195 FuncRegistryBuilder {
196 funcs: self.funcs.clone(),
197 }
198 }
199
200 #[must_use]
206 pub fn default_with_stdlib() -> Self {
207 let mut b = Self::builder();
208 super::stdlib::register(&mut b);
209 super::tf_funcs::register(&mut b);
210 super::files::register(&mut b);
211 b.build()
212 }
213
214 #[must_use]
216 pub fn builder() -> FuncRegistryBuilder {
217 FuncRegistryBuilder::default()
218 }
219}
220
221impl fmt::Debug for FuncRegistry {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 let mut names: Vec<&str> = self.funcs.keys().map(Arc::as_ref).collect();
224 names.sort_unstable();
225 f.debug_struct("FuncRegistry")
226 .field("count", &self.funcs.len())
227 .field("names", &names)
228 .finish()
229 }
230}
231
232#[derive(Default)]
234pub struct FuncRegistryBuilder {
235 funcs: HashMap<Arc<str>, Arc<dyn HclFunc>>,
236}
237
238impl FuncRegistryBuilder {
239 pub fn register(&mut self, name: impl Into<Arc<str>>, func: Arc<dyn HclFunc>) -> &mut Self {
242 self.funcs.insert(name.into(), func);
243 self
244 }
245
246 pub fn unregister(&mut self, name: &str) -> &mut Self {
248 self.funcs.remove(name);
249 self
250 }
251
252 #[must_use]
254 pub fn contains(&self, name: &str) -> bool {
255 self.funcs.contains_key(name)
256 }
257
258 #[must_use]
260 pub fn build(self) -> FuncRegistry {
261 FuncRegistry { funcs: self.funcs }
262 }
263}
264
265impl fmt::Debug for FuncRegistryBuilder {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 f.debug_struct("FuncRegistryBuilder")
268 .field("count", &self.funcs.len())
269 .finish()
270 }
271}
272
273#[cfg(test)]
274#[allow(
275 clippy::unwrap_used,
276 clippy::expect_used,
277 clippy::panic,
278 clippy::indexing_slicing
279)]
280mod tests {
281 use std::path::Path;
282
283 use super::*;
284
285 #[derive(Debug)]
286 struct Echo;
287 impl HclFunc for Echo {
288 fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
289 args.first().cloned().ok_or_else(|| FuncError::Arity {
290 name: Arc::from("echo"),
291 expected: 1,
292 got: 0,
293 })
294 }
295 }
296
297 fn fake_cx() -> (EvalLimits, EnvVarMode) {
298 (EvalLimits::default(), EnvVarMode::default())
299 }
300
301 #[test]
302 fn test_registry_builder_registers_and_unregisters() {
303 let mut b = FuncRegistry::builder();
304 b.register("echo", Arc::new(Echo));
305 assert!(b.contains("echo"));
306 b.unregister("echo");
307 assert!(!b.contains("echo"));
308 }
309
310 #[test]
311 fn test_registry_dispatch_echo() {
312 let mut b = FuncRegistry::builder();
313 b.register("echo", Arc::new(Echo));
314 let r = b.build();
315 let (limits, env_vars) = fake_cx();
316 let cx = CallCx {
317 workspace_root: Path::new("/tmp"),
318 env_vars: &env_vars,
319 limits: &limits,
320 };
321 let v = r.get("echo").unwrap().call(&[Value::Int(7)], &cx).unwrap();
322 assert_eq!(v, Value::Int(7));
323 }
324
325 #[test]
326 fn test_default_with_stdlib_has_known_functions() {
327 let r = FuncRegistry::default_with_stdlib();
328 assert!(r.contains("jsonencode"), "{r:?}");
330 assert!(r.contains("merge"), "{r:?}");
331 assert!(r.contains("sha256"), "{r:?}");
332 assert!(r.contains("base64encode"), "{r:?}");
333 }
334
335 #[test]
336 fn test_func_error_arity_renders_function_name() {
337 let e = FuncError::Arity {
338 name: Arc::from("merge"),
339 expected: 2,
340 got: 1,
341 };
342 let s = format!("{e}");
343 assert!(s.contains("merge"));
344 assert!(s.contains('2'));
345 assert!(s.contains('1'));
346 }
347
348 #[test]
349 fn test_registry_is_send_sync() {
350 const fn assert_send_sync<T: Send + Sync + 'static>() {}
351 assert_send_sync::<FuncRegistry>();
352 assert_send_sync::<Arc<FuncRegistry>>();
353 assert_send_sync::<Arc<dyn HclFunc>>();
354 }
355
356 #[test]
357 fn test_registry_debug_lists_sorted_names() {
358 let mut b = FuncRegistry::builder();
359 b.register("z_alpha", Arc::new(Echo));
360 b.register("a_alpha", Arc::new(Echo));
361 let r = b.build();
362 let s = format!("{r:?}");
363 let z_pos = s.find("z_alpha").expect("z_alpha present");
364 let a_pos = s.find("a_alpha").expect("a_alpha present");
365 assert!(a_pos < z_pos, "expected sorted names: {s}");
366 }
367}