Skip to main content

md_tmpl_core/
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_core::{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    ///
73    /// # Panics
74    ///
75    /// Panics if `key` is the reserved internal key `__kind__` — this key
76    /// is used internally for enum variant tagging and must not be set
77    /// directly. Use enum types in the template frontmatter instead.
78    pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
79        let key = key.into();
80        assert!(
81            key != crate::consts::ENUM_TAG_KEY,
82            "cannot set reserved internal key '{}' directly in Context — \
83             use enum types in the template frontmatter instead",
84            crate::consts::ENUM_TAG_KEY,
85        );
86        self.values.insert(key, value.into());
87    }
88
89    /// Builder-style insert — returns `self` for chaining.
90    #[must_use]
91    pub fn var(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
92        self.set(key, value);
93        self
94    }
95
96    /// Look up a top-level variable.
97    #[must_use]
98    pub fn get(&self, key: &str) -> Option<&Value> {
99        self.values.get(key)
100    }
101
102    /// Consume this context and return the inner variable map.
103    ///
104    /// Useful for converting a context into a [`Value::Struct`](crate::Value::Struct).
105    #[must_use]
106    pub fn into_inner(self) -> HashMap<String, Value> {
107        self.values
108    }
109}
110
111/// Collect `(&str, Value)` tuples into a `Context`.
112impl<K: Into<String>, V: Into<Value>> FromIterator<(K, V)> for Context {
113    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
114        let iter = iter.into_iter();
115        let (lower, _) = iter.size_hint();
116        let mut ctx = Self::with_capacity(lower);
117        for (k, v) in iter {
118            ctx.set(k, v);
119        }
120        ctx
121    }
122}
123
124#[cfg(feature = "serde")]
125impl Context {
126    /// Build a `Context` directly from a [`Value::Struct`](crate::Value::Struct).
127    ///
128    /// # Errors
129    ///
130    /// Returns `TemplateError::Syntax` if the value is not a dict/map.
131    pub fn from_value(val: Value) -> Result<Self, crate::error::TemplateError> {
132        match val {
133            Value::Struct(arc_map) => {
134                // Try to unwrap the Arc to avoid cloning — we're the sole owner
135                // if it was just deserialized or serialized.
136                let values =
137                    alloc::sync::Arc::try_unwrap(arc_map).unwrap_or_else(|arc| (*arc).clone());
138                Ok(Self { values })
139            }
140            other => Err(crate::error::TemplateError::syntax(format!(
141                "expected struct/map, got {}",
142                other.type_name()
143            ))),
144        }
145    }
146
147    /// Build a `Context` from any `Serialize` type that serializes as a map/struct.
148    ///
149    /// # Errors
150    ///
151    /// Returns `TemplateError::Syntax` if the serialized value is not a dict/map.
152    pub fn from_serialize<T: serde::Serialize>(
153        value: &T,
154    ) -> Result<Self, crate::error::TemplateError> {
155        let val = crate::serde_support::to_value(value).map_err(|e| {
156            crate::error::TemplateError::syntax(format!("serde conversion failed: {e}"))
157        })?;
158        Self::from_value(val)
159    }
160
161    /// Build a `Context` from a CBOR binary buffer.
162    ///
163    /// Available in `no_std` — ciborium uses its own `Read` trait with a
164    /// blanket impl for `&[u8]`.
165    ///
166    /// # Errors
167    ///
168    /// Returns `TemplateError::Syntax` if the buffer is invalid or not a dict/map.
169    pub fn from_cbor(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
170        let val: Value = ciborium::from_reader(data).map_err(|e| {
171            crate::error::TemplateError::syntax(format!("cbor deserialization failed: {e}"))
172        })?;
173        Self::from_value(val)
174    }
175}
176
177/// `FlexBuffers` support — requires `std` (the `flexbuffers` crate does not
178/// support `no_std`).
179#[cfg(feature = "std")]
180#[cfg(feature = "serde")]
181impl Context {
182    /// Build a `Context` from a `FlexBuffers` binary buffer.
183    ///
184    /// # Errors
185    ///
186    /// Returns `TemplateError::Syntax` if the buffer is invalid or not a dict/map.
187    pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
188        let r = flexbuffers::Reader::get_root(data).map_err(|e| {
189            crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
190        })?;
191        let val: Value = serde::Deserialize::deserialize(r).map_err(|e| {
192            crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
193        })?;
194        Self::from_value(val)
195    }
196}
197
198// ---------------------------------------------------------------------------
199// Tests
200// ---------------------------------------------------------------------------
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn new_context_is_empty() {
208        let ctx = Context::new();
209        assert!(ctx.get("anything").is_none());
210    }
211
212    #[test]
213    fn set_and_get_str() {
214        let mut ctx = Context::new();
215        ctx.set("greeting", "hello");
216        assert_eq!(ctx.get("greeting"), Some(&Value::Str("hello".into())));
217    }
218
219    #[test]
220    fn set_and_get_bool() {
221        let mut ctx = Context::new();
222        ctx.set("flag", true);
223        assert_eq!(ctx.get("flag"), Some(&Value::Bool(true)));
224    }
225
226    #[test]
227    fn set_and_get_int() {
228        let mut ctx = Context::new();
229        ctx.set("count", 42_i64);
230        assert_eq!(ctx.get("count"), Some(&Value::Int(42)));
231    }
232
233    #[test]
234    fn overwrite_value() {
235        let mut ctx = Context::new();
236        ctx.set("k", "first");
237        ctx.set("k", "second");
238        assert_eq!(ctx.get("k"), Some(&Value::Str("second".into())));
239    }
240
241    #[test]
242    fn get_missing_returns_none() {
243        let ctx = Context::new();
244        assert_eq!(ctx.get("nonexistent"), None);
245    }
246
247    #[test]
248    fn default_is_same_as_new() {
249        let a = Context::new();
250        let b = Context::default();
251        assert!(a.values.is_empty());
252        assert!(b.values.is_empty());
253    }
254
255    #[test]
256    #[should_panic(expected = "reserved internal key")]
257    fn set_rejects_internal_kind_key() {
258        let mut ctx = Context::new();
259        ctx.set(crate::consts::ENUM_TAG_KEY, "Variant");
260    }
261
262    #[test]
263    fn set_allows_similar_but_different_keys() {
264        // Keys that look similar but aren't ENUM_TAG_KEY should work fine.
265        let mut ctx = Context::new();
266        ctx.set("kind", "some_kind");
267        ctx.set("__type__", "some_type");
268        ctx.set("kind_of", "something");
269        assert!(ctx.get("kind").is_some());
270        assert!(ctx.get("__type__").is_some());
271    }
272}