Skip to main content

tsx_forge/
context.rs

1//! ForgeContext — a context builder for forge template rendering.
2
3use serde::Serialize;
4use serde_json;
5
6/// A typed context passed to `Engine::render()`.
7/// Wraps `tera::Context` and provides a builder-style API.
8pub struct ForgeContext {
9    inner: tera::Context,
10}
11
12impl ForgeContext {
13    pub fn new() -> Self {
14        Self {
15            inner: tera::Context::new(),
16        }
17    }
18
19    /// Insert a serializable value (builder style, consumes self).
20    pub fn insert<T: Serialize + ?Sized>(mut self, key: &str, value: &T) -> Self {
21        self.inner.insert(key, value);
22        self
23    }
24
25    /// Insert a serializable value (mutable style, for use in loops).
26    pub fn insert_mut<T: Serialize + ?Sized>(&mut self, key: &str, value: &T) {
27        self.inner.insert(key, value);
28    }
29
30    /// Register a named slot with content to be injected into the template.
31    ///
32    /// Slots use thread-local storage and are populated just before the render
33    /// call inside `Engine::render()`. This method stages them on the context
34    /// so that `Engine` can apply them before delegating to Tera.
35    pub fn slot(mut self, name: &str, content: &str) -> Self {
36        // Store slots as a plain JSON map under "__slots__" key so we can
37        // iterate them in engine.rs at render time without leaking internals.
38        let mut current = self
39            .inner
40            .get("__slots__")
41            .and_then(|v| v.as_object().cloned())
42            .unwrap_or_default();
43        current.insert(
44            name.to_string(),
45            serde_json::Value::String(content.to_string()),
46        );
47        self.inner.insert("__slots__", &current);
48        self
49    }
50
51    /// Register a provided value to be made available via `inject(key=...)` in templates.
52    /// Values are stored under `__provides__` and applied to the thread-local store
53    /// at render time by `Engine::render()`.
54    pub fn provide(mut self, key: &str, value: &str) -> Self {
55        let mut current = self
56            .inner
57            .get("__provides__")
58            .and_then(|v| v.as_object().cloned())
59            .unwrap_or_default();
60        current.insert(
61            key.to_string(),
62            serde_json::Value::String(value.to_string()),
63        );
64        self.inner.insert("__provides__", &current);
65        self
66    }
67
68    /// Return the provides map from this context (for engine use at render time).
69    pub(crate) fn provides(&self) -> Option<serde_json::Map<String, serde_json::Value>> {
70        self.inner
71            .get("__provides__")
72            .and_then(|v| v.as_object().cloned())
73    }
74
75    /// Return the slot map from this context (for engine use at render time).
76    pub(crate) fn slots(&self) -> Option<serde_json::Map<String, serde_json::Value>> {
77        self.inner
78            .get("__slots__")
79            .and_then(|v| v.as_object().cloned())
80    }
81
82    /// Build a context from a serializable struct.
83    pub fn from_serialize<T: Serialize>(data: &T) -> anyhow::Result<Self> {
84        let inner = tera::Context::from_serialize(data)
85            .map_err(|e| anyhow::anyhow!("Context serialization failed: {e}"))?;
86        Ok(Self { inner })
87    }
88
89    pub(crate) fn as_tera(&self) -> &tera::Context {
90        &self.inner
91    }
92}
93
94impl Default for ForgeContext {
95    fn default() -> Self {
96        Self::new()
97    }
98}