use std::{collections::HashMap, fmt, path::Path, sync::Arc};
use thiserror::Error;
use crate::{
diagnostic::LimitKind,
eval::context::{EnvVarMode, EvalLimits},
ir::Value,
};
#[derive(Debug)]
#[non_exhaustive]
pub struct CallCx<'a> {
pub workspace_root: &'a Path,
pub env_vars: &'a EnvVarMode,
pub limits: &'a EvalLimits,
}
impl<'a> CallCx<'a> {
#[must_use]
pub const fn new(
workspace_root: &'a Path,
env_vars: &'a EnvVarMode,
limits: &'a EvalLimits,
) -> Self {
Self {
workspace_root,
env_vars,
limits,
}
}
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum FuncError {
#[error("`{name}`: expected {expected} arg(s), got {got}")]
Arity {
name: Arc<str>,
expected: usize,
got: usize,
},
#[error("`{name}` arg #{index}: expected {expected}, got {got}")]
Type {
name: Arc<str>,
index: usize,
expected: &'static str,
got: &'static str,
},
#[error("`{name}` limit ({kind:?}): observed {observed} > {limit}")]
Limit {
name: Arc<str>,
kind: LimitKind,
observed: u64,
limit: u64,
},
#[error("`{name}` path escape: `{path}`")]
PathEscape {
name: &'static str,
path: std::path::PathBuf,
},
#[error("`{name}`: {message}")]
Other {
name: Arc<str>,
message: Arc<str>,
},
}
pub trait HclFunc: fmt::Debug + Send + Sync + 'static {
fn call(&self, args: &[Value], cx: &CallCx<'_>) -> Result<Value, FuncError>;
}
#[derive(Default)]
pub struct FuncRegistry {
funcs: HashMap<Arc<str>, Arc<dyn HclFunc>>,
}
impl FuncRegistry {
#[must_use]
pub fn get(&self, name: &str) -> Option<&Arc<dyn HclFunc>> {
self.funcs.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.funcs.contains_key(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&Arc<str>, &Arc<dyn HclFunc>)> {
self.funcs.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.funcs.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.funcs.is_empty()
}
#[must_use]
pub fn to_builder(&self) -> FuncRegistryBuilder {
FuncRegistryBuilder {
funcs: self.funcs.clone(),
}
}
#[must_use]
pub fn default_with_stdlib() -> Self {
let mut b = Self::builder();
super::stdlib::register(&mut b);
super::tf_funcs::register(&mut b);
super::files::register(&mut b);
b.build()
}
#[must_use]
pub fn builder() -> FuncRegistryBuilder {
FuncRegistryBuilder::default()
}
}
impl fmt::Debug for FuncRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut names: Vec<&str> = self.funcs.keys().map(Arc::as_ref).collect();
names.sort_unstable();
f.debug_struct("FuncRegistry")
.field("count", &self.funcs.len())
.field("names", &names)
.finish()
}
}
#[derive(Default)]
pub struct FuncRegistryBuilder {
funcs: HashMap<Arc<str>, Arc<dyn HclFunc>>,
}
impl FuncRegistryBuilder {
pub fn register(&mut self, name: impl Into<Arc<str>>, func: Arc<dyn HclFunc>) -> &mut Self {
self.funcs.insert(name.into(), func);
self
}
pub fn unregister(&mut self, name: &str) -> &mut Self {
self.funcs.remove(name);
self
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.funcs.contains_key(name)
}
#[must_use]
pub fn build(self) -> FuncRegistry {
FuncRegistry { funcs: self.funcs }
}
}
impl fmt::Debug for FuncRegistryBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FuncRegistryBuilder")
.field("count", &self.funcs.len())
.finish()
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use std::path::Path;
use super::*;
#[derive(Debug)]
struct Echo;
impl HclFunc for Echo {
fn call(&self, args: &[Value], _cx: &CallCx<'_>) -> Result<Value, FuncError> {
args.first().cloned().ok_or_else(|| FuncError::Arity {
name: Arc::from("echo"),
expected: 1,
got: 0,
})
}
}
fn fake_cx() -> (EvalLimits, EnvVarMode) {
(EvalLimits::default(), EnvVarMode::default())
}
#[test]
fn test_registry_builder_registers_and_unregisters() {
let mut b = FuncRegistry::builder();
b.register("echo", Arc::new(Echo));
assert!(b.contains("echo"));
b.unregister("echo");
assert!(!b.contains("echo"));
}
#[test]
fn test_registry_dispatch_echo() {
let mut b = FuncRegistry::builder();
b.register("echo", Arc::new(Echo));
let r = b.build();
let (limits, env_vars) = fake_cx();
let cx = CallCx {
workspace_root: Path::new("/tmp"),
env_vars: &env_vars,
limits: &limits,
};
let v = r.get("echo").unwrap().call(&[Value::Int(7)], &cx).unwrap();
assert_eq!(v, Value::Int(7));
}
#[test]
fn test_default_with_stdlib_has_known_functions() {
let r = FuncRegistry::default_with_stdlib();
assert!(r.contains("jsonencode"), "{r:?}");
assert!(r.contains("merge"), "{r:?}");
assert!(r.contains("sha256"), "{r:?}");
assert!(r.contains("base64encode"), "{r:?}");
}
#[test]
fn test_func_error_arity_renders_function_name() {
let e = FuncError::Arity {
name: Arc::from("merge"),
expected: 2,
got: 1,
};
let s = format!("{e}");
assert!(s.contains("merge"));
assert!(s.contains('2'));
assert!(s.contains('1'));
}
#[test]
fn test_registry_is_send_sync() {
const fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<FuncRegistry>();
assert_send_sync::<Arc<FuncRegistry>>();
assert_send_sync::<Arc<dyn HclFunc>>();
}
#[test]
fn test_registry_debug_lists_sorted_names() {
let mut b = FuncRegistry::builder();
b.register("z_alpha", Arc::new(Echo));
b.register("a_alpha", Arc::new(Echo));
let r = b.build();
let s = format!("{r:?}");
let z_pos = s.find("z_alpha").expect("z_alpha present");
let a_pos = s.find("a_alpha").expect("a_alpha present");
assert!(a_pos < z_pos, "expected sorted names: {s}");
}
}