Skip to main content

md_tmpl/
context.rs

1//! User-facing template rendering context.
2
3use alloc::string::String;
4
5use crate::{compat::HashMap, value::Value};
6
7/// Template rendering context — holds all variables available during rendering.
8///
9/// # Examples
10///
11/// From an iterator of tuples:
12/// ```
13/// use md_tmpl::{Context, Value};
14///
15/// let ctx: Context = vec![
16///     ("name", Value::from("Alice")),
17///     ("count", Value::from(3_i64)),
18/// ]
19/// .into_iter()
20/// .collect();
21///
22/// assert!(ctx.get("name").is_some());
23/// ```
24#[derive(Debug, Clone, Default)]
25pub struct Context {
26    pub(crate) values: HashMap<String, Value>,
27}
28
29impl Context {
30    /// Create an empty context.
31    #[must_use]
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Create an empty context pre-allocated for `capacity` variables.
37    #[must_use]
38    pub fn with_capacity(capacity: usize) -> Self {
39        Self {
40            values: HashMap::with_capacity(capacity),
41        }
42    }
43
44    /// Returns the number of variables in this context.
45    #[must_use]
46    pub fn len(&self) -> usize {
47        self.values.len()
48    }
49
50    /// Returns `true` if this context contains no variables.
51    #[must_use]
52    pub fn is_empty(&self) -> bool {
53        self.values.is_empty()
54    }
55
56    /// Returns `true` if a variable with the given key exists.
57    #[must_use]
58    pub fn contains_key(&self, key: &str) -> bool {
59        self.values.contains_key(key)
60    }
61
62    /// Insert a value into the context.
63    ///
64    /// This is the **dynamic** API — the key is a plain string and type
65    /// mismatches are caught at [`render_ctx()`](crate::Template::render_ctx) time,
66    /// not here. For compile-time type safety, prefer one of:
67    ///
68    /// - `include_template!` (from `md-tmpl-macros`)
69    ///   — generates a strongly-typed parameter struct from your template.
70    /// - [`Template::render`](crate::Template::render) (feature `serde`)
71    ///   — renders directly from any `Serialize` struct.
72    pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
73        self.values.insert(key.into(), value.into());
74    }
75
76    /// Builder-style insert — returns `self` for chaining.
77    #[must_use]
78    pub fn var(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
79        self.set(key, value);
80        self
81    }
82
83    /// Look up a top-level variable.
84    #[must_use]
85    pub fn get(&self, key: &str) -> Option<&Value> {
86        self.values.get(key)
87    }
88
89    /// Consume this context and return the inner variable map.
90    ///
91    /// Useful for converting a context into a [`Value::Struct`](crate::Value::Struct).
92    #[must_use]
93    pub fn into_inner(self) -> HashMap<String, Value> {
94        self.values
95    }
96}
97
98/// Collect `(&str, Value)` tuples into a `Context`.
99impl<K: Into<String>, V: Into<Value>> FromIterator<(K, V)> for Context {
100    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
101        let iter = iter.into_iter();
102        let (lower, _) = iter.size_hint();
103        let mut ctx = Self::with_capacity(lower);
104        for (k, v) in iter {
105            ctx.set(k, v);
106        }
107        ctx
108    }
109}
110
111#[cfg(feature = "serde")]
112impl Context {
113    /// Build a `Context` directly from a [`Value::Struct`](crate::Value::Struct).
114    ///
115    /// # Errors
116    ///
117    /// Returns `TemplateError::Syntax` if the value is not a dict/map.
118    pub fn from_value(val: Value) -> Result<Self, crate::error::TemplateError> {
119        match val {
120            Value::Struct(arc_map) => {
121                // Try to unwrap the Arc to avoid cloning — we're the sole owner
122                // if it was just deserialized or serialized.
123                let values =
124                    alloc::sync::Arc::try_unwrap(arc_map).unwrap_or_else(|arc| (*arc).clone());
125                Ok(Self { values })
126            }
127            other => Err(crate::error::TemplateError::syntax(format!(
128                "expected struct/map, got {}",
129                other.type_name()
130            ))),
131        }
132    }
133
134    /// Build a `Context` from any `Serialize` type that serializes as a map/struct.
135    ///
136    /// # Errors
137    ///
138    /// Returns `TemplateError::Syntax` if the serialized value is not a dict/map.
139    pub fn from_serialize<T: serde::Serialize>(
140        value: &T,
141    ) -> Result<Self, crate::error::TemplateError> {
142        let val = crate::serde_support::to_value(value).map_err(|e| {
143            crate::error::TemplateError::syntax(format!("serde conversion failed: {e}"))
144        })?;
145        Self::from_value(val)
146    }
147
148    /// Build a `Context` from a CBOR binary buffer.
149    ///
150    /// Available in `no_std` — ciborium uses its own `Read` trait with a
151    /// blanket impl for `&[u8]`.
152    ///
153    /// # Errors
154    ///
155    /// Returns `TemplateError::Syntax` if the buffer is invalid or not a dict/map.
156    pub fn from_cbor(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
157        let val: Value = ciborium::from_reader(data).map_err(|e| {
158            crate::error::TemplateError::syntax(format!("cbor deserialization failed: {e}"))
159        })?;
160        Self::from_value(val)
161    }
162}
163
164/// `FlexBuffers` support — requires `std` (the `flexbuffers` crate does not
165/// support `no_std`).
166#[cfg(feature = "std")]
167#[cfg(feature = "serde")]
168impl Context {
169    /// Build a `Context` from a `FlexBuffers` binary buffer.
170    ///
171    /// # Errors
172    ///
173    /// Returns `TemplateError::Syntax` if the buffer is invalid or not a dict/map.
174    pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
175        let r = flexbuffers::Reader::get_root(data).map_err(|e| {
176            crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
177        })?;
178        let val: Value = serde::Deserialize::deserialize(r).map_err(|e| {
179            crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
180        })?;
181        Self::from_value(val)
182    }
183}
184
185// ---------------------------------------------------------------------------
186// Tests
187// ---------------------------------------------------------------------------
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn new_context_is_empty() {
195        let ctx = Context::new();
196        assert!(ctx.get("anything").is_none());
197    }
198
199    #[test]
200    fn set_and_get_str() {
201        let mut ctx = Context::new();
202        ctx.set("greeting", "hello");
203        assert_eq!(ctx.get("greeting"), Some(&Value::Str("hello".into())));
204    }
205
206    #[test]
207    fn set_and_get_bool() {
208        let mut ctx = Context::new();
209        ctx.set("flag", true);
210        assert_eq!(ctx.get("flag"), Some(&Value::Bool(true)));
211    }
212
213    #[test]
214    fn set_and_get_int() {
215        let mut ctx = Context::new();
216        ctx.set("count", 42_i64);
217        assert_eq!(ctx.get("count"), Some(&Value::Int(42)));
218    }
219
220    #[test]
221    fn overwrite_value() {
222        let mut ctx = Context::new();
223        ctx.set("k", "first");
224        ctx.set("k", "second");
225        assert_eq!(ctx.get("k"), Some(&Value::Str("second".into())));
226    }
227
228    #[test]
229    fn get_missing_returns_none() {
230        let ctx = Context::new();
231        assert_eq!(ctx.get("nonexistent"), None);
232    }
233
234    #[test]
235    fn default_is_same_as_new() {
236        let a = Context::new();
237        let b = Context::default();
238        assert!(a.values.is_empty());
239        assert!(b.values.is_empty());
240    }
241}