use alloc::string::String;
use crate::{compat::HashMap, value::Value};
#[derive(Debug, Clone, Default)]
pub struct Context {
pub(crate) values: HashMap<String, Value>,
}
impl Context {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
values: HashMap::with_capacity(capacity),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
self.values.contains_key(key)
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
let key = key.into();
assert!(
key != crate::consts::ENUM_TAG_KEY,
"cannot set reserved internal key '{}' directly in Context — \
use enum types in the template frontmatter instead",
crate::consts::ENUM_TAG_KEY,
);
self.values.insert(key, value.into());
}
#[must_use]
pub fn var(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.set(key, value);
self
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.values.get(key)
}
#[must_use]
pub fn into_inner(self) -> HashMap<String, Value> {
self.values
}
}
impl<K: Into<String>, V: Into<Value>> FromIterator<(K, V)> for Context {
fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut ctx = Self::with_capacity(lower);
for (k, v) in iter {
ctx.set(k, v);
}
ctx
}
}
#[cfg(feature = "serde")]
impl Context {
pub fn from_value(val: Value) -> Result<Self, crate::error::TemplateError> {
match val {
Value::Struct(arc_map) => {
let values =
alloc::sync::Arc::try_unwrap(arc_map).unwrap_or_else(|arc| (*arc).clone());
Ok(Self { values })
}
other => Err(crate::error::TemplateError::syntax(format!(
"expected struct/map, got {}",
other.type_name()
))),
}
}
pub fn from_serialize<T: serde::Serialize>(
value: &T,
) -> Result<Self, crate::error::TemplateError> {
let val = crate::serde_support::to_value(value).map_err(|e| {
crate::error::TemplateError::syntax(format!("serde conversion failed: {e}"))
})?;
Self::from_value(val)
}
}
#[cfg(feature = "cbor")]
impl Context {
pub fn from_cbor(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
let val: Value = ciborium::from_reader(data).map_err(|e| {
crate::error::TemplateError::syntax(format!("cbor deserialization failed: {e}"))
})?;
Self::from_value(val)
}
}
#[cfg(feature = "flexbuffers")]
impl Context {
pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
let r = flexbuffers::Reader::get_root(data).map_err(|e| {
crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
})?;
let val: Value = serde::Deserialize::deserialize(r).map_err(|e| {
crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
})?;
Self::from_value(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_context_is_empty() {
let ctx = Context::new();
assert!(ctx.get("anything").is_none());
}
#[test]
fn set_and_get_str() {
let mut ctx = Context::new();
ctx.set("greeting", "hello");
assert_eq!(ctx.get("greeting"), Some(&Value::Str("hello".into())));
}
#[test]
fn set_and_get_bool() {
let mut ctx = Context::new();
ctx.set("flag", true);
assert_eq!(ctx.get("flag"), Some(&Value::Bool(true)));
}
#[test]
fn set_and_get_int() {
let mut ctx = Context::new();
ctx.set("count", 42_i64);
assert_eq!(ctx.get("count"), Some(&Value::Int(42)));
}
#[test]
fn overwrite_value() {
let mut ctx = Context::new();
ctx.set("k", "first");
ctx.set("k", "second");
assert_eq!(ctx.get("k"), Some(&Value::Str("second".into())));
}
#[test]
fn get_missing_returns_none() {
let ctx = Context::new();
assert_eq!(ctx.get("nonexistent"), None);
}
#[test]
fn default_is_same_as_new() {
let a = Context::new();
let b = Context::default();
assert!(a.values.is_empty());
assert!(b.values.is_empty());
}
#[test]
#[should_panic(expected = "reserved internal key")]
fn set_rejects_internal_kind_key() {
let mut ctx = Context::new();
ctx.set(crate::consts::ENUM_TAG_KEY, "Variant");
}
#[test]
fn set_allows_similar_but_different_keys() {
let mut ctx = Context::new();
ctx.set("kind", "some_kind");
ctx.set("__type__", "some_type");
ctx.set("kind_of", "something");
assert!(ctx.get("kind").is_some());
assert!(ctx.get("__type__").is_some());
}
#[cfg(feature = "cbor")]
#[test]
fn from_cbor_roundtrip() {
use alloc::{collections::BTreeMap, vec::Vec};
let source = BTreeMap::from([("name", "Alice"), ("role", "admin")]);
let mut buf = Vec::new();
ciborium::into_writer(&source, &mut buf).expect("cbor encode");
let ctx = Context::from_cbor(&buf).expect("from_cbor");
assert_eq!(ctx.get("name"), Some(&Value::Str("Alice".into())));
assert_eq!(ctx.get("role"), Some(&Value::Str("admin".into())));
}
#[cfg(feature = "cbor")]
#[test]
fn from_cbor_rejects_non_map() {
use alloc::vec::Vec;
let mut buf = Vec::new();
ciborium::into_writer(&42_i64, &mut buf).expect("cbor encode");
Context::from_cbor(&buf).expect_err("a non-map CBOR value must not produce a Context");
}
#[cfg(feature = "cbor")]
#[test]
fn from_cbor_rejects_garbage() {
Context::from_cbor(&[]).expect_err("empty buffer must error");
Context::from_cbor(&[0xde, 0xad, 0xbe, 0xef])
.expect_err("garbage bytes must error without panicking");
}
#[cfg(feature = "flexbuffers")]
#[test]
fn from_flexbuffers_roundtrip() {
use serde::Serialize;
let source = alloc::collections::BTreeMap::from([("name", "Alice"), ("role", "admin")]);
let mut ser = flexbuffers::FlexbufferSerializer::new();
source.serialize(&mut ser).expect("flexbuffers encode");
let ctx = Context::from_flexbuffers(ser.view()).expect("from_flexbuffers");
assert_eq!(ctx.get("name"), Some(&Value::Str("Alice".into())));
assert_eq!(ctx.get("role"), Some(&Value::Str("admin".into())));
}
#[cfg(feature = "flexbuffers")]
#[test]
fn from_flexbuffers_rejects_garbage() {
Context::from_flexbuffers(&[]).expect_err("empty flexbuffer must error");
Context::from_flexbuffers(&[0xde, 0xad, 0xbe, 0xef])
.expect_err("garbage flexbuffer must error without panicking");
}
}