1use alloc::string::String;
4
5use crate::{compat::HashMap, value::Value};
6
7#[derive(Debug, Clone, Default)]
25pub struct Context {
26 pub(crate) values: HashMap<String, Value>,
27}
28
29impl Context {
30 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 #[must_use]
38 pub fn with_capacity(capacity: usize) -> Self {
39 Self {
40 values: HashMap::with_capacity(capacity),
41 }
42 }
43
44 #[must_use]
46 pub fn len(&self) -> usize {
47 self.values.len()
48 }
49
50 #[must_use]
52 pub fn is_empty(&self) -> bool {
53 self.values.is_empty()
54 }
55
56 #[must_use]
58 pub fn contains_key(&self, key: &str) -> bool {
59 self.values.contains_key(key)
60 }
61
62 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 #[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 #[must_use]
98 pub fn get(&self, key: &str) -> Option<&Value> {
99 self.values.get(key)
100 }
101
102 #[must_use]
106 pub fn into_inner(self) -> HashMap<String, Value> {
107 self.values
108 }
109}
110
111impl<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 pub fn from_value(val: Value) -> Result<Self, crate::error::TemplateError> {
132 match val {
133 Value::Struct(arc_map) => {
134 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 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
162#[cfg(feature = "cbor")]
167impl Context {
168 pub fn from_cbor(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
174 let val: Value = ciborium::from_reader(data).map_err(|e| {
175 crate::error::TemplateError::syntax(format!("cbor deserialization failed: {e}"))
176 })?;
177 Self::from_value(val)
178 }
179}
180
181#[cfg(feature = "flexbuffers")]
184impl Context {
185 pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
191 let r = flexbuffers::Reader::get_root(data).map_err(|e| {
192 crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
193 })?;
194 let val: Value = serde::Deserialize::deserialize(r).map_err(|e| {
195 crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
196 })?;
197 Self::from_value(val)
198 }
199}
200
201#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn new_context_is_empty() {
211 let ctx = Context::new();
212 assert!(ctx.get("anything").is_none());
213 }
214
215 #[test]
216 fn set_and_get_str() {
217 let mut ctx = Context::new();
218 ctx.set("greeting", "hello");
219 assert_eq!(ctx.get("greeting"), Some(&Value::Str("hello".into())));
220 }
221
222 #[test]
223 fn set_and_get_bool() {
224 let mut ctx = Context::new();
225 ctx.set("flag", true);
226 assert_eq!(ctx.get("flag"), Some(&Value::Bool(true)));
227 }
228
229 #[test]
230 fn set_and_get_int() {
231 let mut ctx = Context::new();
232 ctx.set("count", 42_i64);
233 assert_eq!(ctx.get("count"), Some(&Value::Int(42)));
234 }
235
236 #[test]
237 fn overwrite_value() {
238 let mut ctx = Context::new();
239 ctx.set("k", "first");
240 ctx.set("k", "second");
241 assert_eq!(ctx.get("k"), Some(&Value::Str("second".into())));
242 }
243
244 #[test]
245 fn get_missing_returns_none() {
246 let ctx = Context::new();
247 assert_eq!(ctx.get("nonexistent"), None);
248 }
249
250 #[test]
251 fn default_is_same_as_new() {
252 let a = Context::new();
253 let b = Context::default();
254 assert!(a.values.is_empty());
255 assert!(b.values.is_empty());
256 }
257
258 #[test]
259 #[should_panic(expected = "reserved internal key")]
260 fn set_rejects_internal_kind_key() {
261 let mut ctx = Context::new();
262 ctx.set(crate::consts::ENUM_TAG_KEY, "Variant");
263 }
264
265 #[test]
266 fn set_allows_similar_but_different_keys() {
267 let mut ctx = Context::new();
269 ctx.set("kind", "some_kind");
270 ctx.set("__type__", "some_type");
271 ctx.set("kind_of", "something");
272 assert!(ctx.get("kind").is_some());
273 assert!(ctx.get("__type__").is_some());
274 }
275
276 #[cfg(feature = "cbor")]
277 #[test]
278 fn from_cbor_roundtrip() {
279 use alloc::{collections::BTreeMap, vec::Vec};
280
281 let source = BTreeMap::from([("name", "Alice"), ("role", "admin")]);
282 let mut buf = Vec::new();
283 ciborium::into_writer(&source, &mut buf).expect("cbor encode");
284
285 let ctx = Context::from_cbor(&buf).expect("from_cbor");
286 assert_eq!(ctx.get("name"), Some(&Value::Str("Alice".into())));
287 assert_eq!(ctx.get("role"), Some(&Value::Str("admin".into())));
288 }
289
290 #[cfg(feature = "cbor")]
291 #[test]
292 fn from_cbor_rejects_non_map() {
293 use alloc::vec::Vec;
294
295 let mut buf = Vec::new();
296 ciborium::into_writer(&42_i64, &mut buf).expect("cbor encode");
297 Context::from_cbor(&buf).expect_err("a non-map CBOR value must not produce a Context");
298 }
299
300 #[cfg(feature = "cbor")]
301 #[test]
302 fn from_cbor_rejects_garbage() {
303 Context::from_cbor(&[]).expect_err("empty buffer must error");
304 Context::from_cbor(&[0xde, 0xad, 0xbe, 0xef])
305 .expect_err("garbage bytes must error without panicking");
306 }
307
308 #[cfg(feature = "flexbuffers")]
309 #[test]
310 fn from_flexbuffers_roundtrip() {
311 use serde::Serialize;
312
313 let source = alloc::collections::BTreeMap::from([("name", "Alice"), ("role", "admin")]);
314 let mut ser = flexbuffers::FlexbufferSerializer::new();
315 source.serialize(&mut ser).expect("flexbuffers encode");
316
317 let ctx = Context::from_flexbuffers(ser.view()).expect("from_flexbuffers");
318 assert_eq!(ctx.get("name"), Some(&Value::Str("Alice".into())));
319 assert_eq!(ctx.get("role"), Some(&Value::Str("admin".into())));
320 }
321
322 #[cfg(feature = "flexbuffers")]
323 #[test]
324 fn from_flexbuffers_rejects_garbage() {
325 Context::from_flexbuffers(&[]).expect_err("empty flexbuffer must error");
326 Context::from_flexbuffers(&[0xde, 0xad, 0xbe, 0xef])
327 .expect_err("garbage flexbuffer must error without panicking");
328 }
329}