Skip to main content

camel_template/
bundle.rs

1//! Component bundle for the `template:` scheme (ADR-0047 Stage 2, Phase 4 /
2//! Task 4.5).
3//!
4//! [`TemplateBundle`] owns the `[components.template]` TOML key. It folds the
5//! two operator-facing limit layers — acquisition `limits` and render
6//! `render-limits` — and registers a single [`TemplateComponent`] into the
7//! context. Both layers default when absent, so an empty `[template]` block
8//! (or no block at all) yields the ADR-0047 defaults. This makes both limit
9//! layers operator-configurable (design "Two limit layers").
10//!
11//! Mirrors the shape of `camel-file`'s `FileBundle`: deserialize the raw block
12//! into a config struct, then construct the component and register it.
13
14use std::sync::Arc;
15
16use camel_component_api::{CamelError, ComponentBundle, ComponentRegistrar};
17use camel_language_api::MinijinjaLimitsConfig;
18use serde::Deserialize;
19
20use crate::component::TemplateComponent;
21use crate::config::ExternalTemplateLimitsConfig;
22
23/// Operator-facing `[components.template]` block: two independent limit layers.
24///
25/// - `limits`        → [`ExternalTemplateLimitsConfig`] (acquisition bounds:
26///   closure size, include count/depth, template size, reload timeout).
27/// - `render-limits` → [`MinijinjaLimitsConfig`] (render-time bounds: source,
28///   context, output size, fuel, recursion, execution timeout).
29///
30/// Both are `#[serde(default)]`, so a missing sub-table falls back to the
31/// per-struct defaults (ADR-0047 §4.1).
32#[derive(Debug, Default, Deserialize)]
33#[serde(rename_all = "kebab-case", deny_unknown_fields)]
34pub struct TemplateBundleConfig {
35    /// Acquisition limits applied while building the template closure.
36    #[serde(default)]
37    pub limits: ExternalTemplateLimitsConfig,
38
39    /// Render limits applied per render inside the MiniJinja environment.
40    #[serde(default)]
41    pub render_limits: MinijinjaLimitsConfig,
42}
43
44/// Bundle that registers the `template:` scheme from the `[template]` config
45/// block.
46///
47/// Constructed by `camel-cli` via `register_bundle!(camel_template::TemplateBundle)`
48/// — always-on (no feature gate), like the other built-in transform components.
49pub struct TemplateBundle {
50    config: TemplateBundleConfig,
51}
52
53impl ComponentBundle for TemplateBundle {
54    fn config_key() -> &'static str {
55        "template"
56    }
57
58    fn from_toml(value: toml::Value) -> Result<Self, CamelError> {
59        let config: TemplateBundleConfig = value
60            .try_into()
61            .map_err(|e: toml::de::Error| CamelError::Config(e.to_string()))?;
62        Ok(Self { config })
63    }
64
65    fn register_all(self, ctx: &mut dyn ComponentRegistrar) {
66        let component = TemplateComponent::new(self.config.limits, self.config.render_limits);
67        ctx.register_component_dyn(Arc::new(component));
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    struct TestRegistrar {
76        schemes: Vec<String>,
77    }
78
79    impl ComponentRegistrar for TestRegistrar {
80        fn register_component_dyn(&mut self, component: Arc<dyn camel_component_api::Component>) {
81            self.schemes.push(component.scheme().to_string());
82        }
83    }
84
85    #[test]
86    fn config_key_is_template() {
87        assert_eq!(TemplateBundle::config_key(), "template");
88    }
89
90    #[test]
91    fn from_toml_empty_uses_defaults() {
92        let value: toml::Value = toml::from_str("").unwrap();
93        let bundle = TemplateBundle::from_toml(value);
94        assert!(bundle.is_ok(), "empty TOML must use defaults");
95    }
96
97    #[test]
98    fn from_toml_parses_both_limit_layers() {
99        let raw = r#"
100limits.max-include-count = 8
101render-limits.fuel = 1234
102"#;
103        let value: toml::Value = toml::from_str(raw).unwrap();
104        let bundle = TemplateBundle::from_toml(value).expect("both limit layers must deserialize");
105        assert_eq!(bundle.config.limits.max_include_count, Some(8));
106        assert_eq!(bundle.config.render_limits.fuel, Some(1234));
107    }
108
109    #[test]
110    fn from_toml_rejects_unknown_field() {
111        let raw = "bogus = 1\n";
112        let value: toml::Value = toml::from_str(raw).unwrap();
113        let result = TemplateBundle::from_toml(value);
114        assert!(result.is_err(), "deny_unknown_fields must reject `bogus`");
115    }
116
117    #[test]
118    fn register_all_registers_template_scheme() {
119        let bundle = TemplateBundle::from_toml(toml::Value::Table(toml::map::Map::new())).unwrap();
120        let mut registrar = TestRegistrar { schemes: vec![] };
121        bundle.register_all(&mut registrar);
122        assert_eq!(registrar.schemes, vec!["template"]);
123    }
124}