Skip to main content

md_tmpl_core/
value.rs

1//! Template value types.
2
3use alloc::{
4    string::{String, ToString},
5    sync::Arc,
6    vec::Vec,
7};
8use core::fmt;
9
10use crate::compat::HashMap;
11
12/// A value that can be inserted into a template.
13#[derive(Debug, Clone)]
14pub enum Value {
15    /// A plain string.
16    Str(String),
17    /// A boolean.
18    Bool(bool),
19    /// A 64-bit integer.
20    Int(i64),
21    /// A 64-bit float.
22    Float(f64),
23    /// An ordered list of values.
24    List(Arc<Vec<Value>>),
25    /// A string-keyed map of values.
26    Struct(Arc<HashMap<String, Value>>),
27    /// A pre-compiled template.
28    Tmpl(Arc<crate::template::Template>),
29    /// An absent/null value — transparent representation of `Option::None`.
30    None,
31}
32
33impl PartialEq for Value {
34    fn eq(&self, other: &Self) -> bool {
35        match (self, other) {
36            (Self::Str(a), Self::Str(b)) => a == b,
37            (Self::Bool(a), Self::Bool(b)) => a == b,
38            (Self::Int(a), Self::Int(b)) => a == b,
39            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
40            (Self::List(a), Self::List(b)) => a == b,
41            (Self::Struct(a), Self::Struct(b)) => a == b,
42            (Self::Tmpl(a), Self::Tmpl(b)) => Arc::ptr_eq(a, b),
43            (Self::None, Self::None) => true,
44            _ => false,
45        }
46    }
47}
48
49impl Eq for Value {}
50
51impl fmt::Display for Value {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::Str(s) => f.write_str(s),
55            Self::Bool(b) => write!(f, "{b}"),
56            Self::Int(i) => {
57                let mut buf = itoa::Buffer::new();
58                f.write_str(buf.format(*i))
59            }
60            Self::Float(v) => write!(f, "{v}"),
61            Self::List(items) => write!(f, "[<list of {}>]", items.len()),
62            Self::Struct(map) => write!(f, "{{<struct of {}>}}", map.len()),
63            Self::Tmpl(_) => write!(f, "<template>"),
64            Self::None => Ok(()),
65        }
66    }
67}
68
69impl Value {
70    /// Returns `true` if the value is considered "truthy".
71    #[must_use]
72    pub fn is_truthy(&self) -> bool {
73        match self {
74            Self::Str(s) => !s.is_empty(),
75            Self::Bool(b) => *b,
76            Self::Int(i) => *i != 0,
77            Self::Float(f) => *f != 0.0,
78            Self::List(v) => !v.is_empty(),
79            Self::Struct(m) => !m.is_empty(),
80            Self::Tmpl(_) => true,
81            Self::None => false,
82        }
83    }
84    /// Returns the type name as a static string.
85    #[must_use]
86    pub fn type_name(&self) -> &'static str {
87        match self {
88            Self::Str(_) => crate::consts::TYPE_STR,
89            Self::Bool(_) => crate::consts::TYPE_BOOL,
90            Self::Int(_) => crate::consts::TYPE_INT,
91            Self::Float(_) => crate::consts::TYPE_FLOAT,
92            Self::List(_) => crate::consts::TYPE_LIST,
93            Self::Struct(_) => crate::consts::TYPE_STRUCT,
94            Self::Tmpl(_) => crate::consts::TYPE_TMPL,
95            Self::None => crate::consts::TYPE_NONE,
96        }
97    }
98
99    /// Returns user-visible field names for error diagnostics.
100    ///
101    /// For [`Struct`](Self::Struct) values, returns all keys except
102    /// internal ones (e.g., `__kind__`). For other variants, returns
103    /// an empty vec.
104    #[must_use]
105    pub(crate) fn field_names_hint(&self) -> Vec<&str> {
106        match self {
107            Self::Struct(m) => m
108                .keys()
109                .filter(|k| k.as_str() != crate::consts::ENUM_TAG_KEY)
110                .map(String::as_str)
111                .collect(),
112            _ => Vec::new(),
113        }
114    }
115    /// Access a field on a Struct value.
116    ///
117    /// The internal enum tag key ([`ENUM_TAG_KEY`](crate::consts::ENUM_TAG_KEY))
118    /// is hidden — use `str(value)` to extract the variant name instead.
119    #[inline]
120    #[must_use]
121    pub fn get_field(&self, key: &str) -> Option<&Value> {
122        match self {
123            Self::Struct(m) => {
124                // Hide the internal enum tag key from template-level access.
125                if key == crate::consts::ENUM_TAG_KEY {
126                    return None;
127                }
128                m.get(key)
129            }
130            _ => None,
131        }
132    }
133
134    /// Access a field without the `ENUM_TAG_KEY` guard.
135    ///
136    /// This is safe because compiled template paths are validated at analysis
137    /// time — user templates can never reference `__kind__` directly.
138    /// Used by the render hot path to avoid a string comparison per access.
139    #[inline]
140    #[must_use]
141    pub(crate) fn get_field_unchecked(&self, key: &str) -> Option<&Value> {
142        debug_assert!(
143            key != crate::consts::ENUM_TAG_KEY,
144            "get_field_unchecked called with internal ENUM_TAG_KEY '{key}' — \
145             this should have been rejected at compile time",
146        );
147        match self {
148            Self::Struct(m) => m.get(key),
149            _ => None,
150        }
151    }
152
153    /// Returns `true` if this is a `Str` variant.
154    #[must_use]
155    pub fn is_str(&self) -> bool {
156        matches!(self, Self::Str(_))
157    }
158
159    /// Returns `true` if this is an `Int` variant.
160    #[must_use]
161    pub fn is_int(&self) -> bool {
162        matches!(self, Self::Int(_))
163    }
164
165    /// Returns `true` if this is a `Float` variant.
166    #[must_use]
167    pub fn is_float(&self) -> bool {
168        matches!(self, Self::Float(_))
169    }
170
171    /// Returns `true` if this is a `Bool` variant.
172    #[must_use]
173    pub fn is_bool(&self) -> bool {
174        matches!(self, Self::Bool(_))
175    }
176
177    /// Returns `true` if this is a `List` variant.
178    #[must_use]
179    pub fn is_list(&self) -> bool {
180        matches!(self, Self::List(_))
181    }
182
183    /// Returns `true` if this is a `Struct` variant.
184    #[must_use]
185    pub fn is_struct(&self) -> bool {
186        matches!(self, Self::Struct(_))
187    }
188
189    /// Returns the inner `&str` if this is a `Str` variant.
190    #[must_use]
191    pub fn as_str(&self) -> Option<&str> {
192        match self {
193            Self::Str(s) => Some(s),
194            _ => None,
195        }
196    }
197
198    /// Returns the inner `i64` if this is an `Int` variant.
199    #[must_use]
200    pub fn as_int(&self) -> Option<i64> {
201        match self {
202            Self::Int(i) => Some(*i),
203            _ => None,
204        }
205    }
206
207    /// Returns the inner `f64` if this is a `Float` variant.
208    #[must_use]
209    pub fn as_float(&self) -> Option<f64> {
210        match self {
211            Self::Float(f) => Some(*f),
212            _ => None,
213        }
214    }
215
216    /// Returns the inner `bool` if this is a `Bool` variant.
217    #[must_use]
218    pub fn as_bool(&self) -> Option<bool> {
219        match self {
220            Self::Bool(b) => Some(*b),
221            _ => None,
222        }
223    }
224
225    /// Returns a slice of the inner list if this is a `List` variant.
226    #[must_use]
227    pub fn as_list(&self) -> Option<&[Value]> {
228        match self {
229            Self::List(v) => Some(v),
230            _ => None,
231        }
232    }
233
234    /// Returns a reference to the inner map if this is a `Struct` variant.
235    #[must_use]
236    pub fn as_struct(&self) -> Option<&HashMap<String, Value>> {
237        match self {
238            Self::Struct(m) => Some(m),
239            _ => None,
240        }
241    }
242
243    /// Returns a reference to the inner template if this is a `Tmpl` variant.
244    #[must_use]
245    pub fn as_tmpl(&self) -> Option<&Arc<crate::template::Template>> {
246        match self {
247            Self::Tmpl(t) => Some(t),
248            _ => None,
249        }
250    }
251
252    /// Create a `Struct` from an iterator of key-value pairs.
253    ///
254    /// Accepts arrays, slices, vecs — anything iterable.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use md_tmpl_core::Value;
260    ///
261    /// let v = Value::new_struct([("name", "Alice"), ("role", "admin")]);
262    /// assert_eq!(v.get_field("name").unwrap().to_string(), "Alice");
263    /// ```
264    #[must_use]
265    pub fn new_struct<I, K, V>(pairs: I) -> Self
266    where
267        I: IntoIterator<Item = (K, V)>,
268        K: Into<String>,
269        V: Into<Value>,
270    {
271        Self::Struct(Arc::new(
272            pairs
273                .into_iter()
274                .map(|(k, v)| (k.into(), v.into()))
275                .collect(),
276        ))
277    }
278
279    /// Create a `List` from an iterator of values.
280    ///
281    /// Accepts arrays, slices, vecs — anything iterable.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use md_tmpl_core::Value;
287    ///
288    /// let v = Value::list([
289    ///     Value::new_struct([("label", "alpha")]),
290    ///     Value::new_struct([("label", "beta")]),
291    /// ]);
292    /// assert_eq!(v.type_name(), "list");
293    /// ```
294    #[must_use]
295    pub fn list<I, V>(items: I) -> Self
296    where
297        I: IntoIterator<Item = V>,
298        V: Into<Value>,
299    {
300        Self::List(Arc::new(items.into_iter().map(Into::into).collect()))
301    }
302}
303
304#[cfg(feature = "serde")]
305impl Value {
306    /// Create a `Value` from any `Serialize` type.
307    ///
308    /// This is the same as [`to_value`](crate::to_value) but available as a
309    /// method on `Value` for convenience.
310    ///
311    /// # Errors
312    ///
313    /// Returns an error if serialization fails.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use md_tmpl_core::Value;
319    /// use serde::Serialize;
320    ///
321    /// #[derive(Serialize)]
322    /// struct Agent {
323    ///     name: String,
324    /// }
325    ///
326    /// let val = Value::from_serialize(&Agent {
327    ///     name: "Alice".into(),
328    /// })
329    /// .unwrap();
330    /// assert_eq!(val.get_field("name").unwrap().as_str(), Some("Alice"));
331    /// ```
332    pub fn from_serialize<T: serde::Serialize>(
333        value: &T,
334    ) -> Result<Self, crate::serde_support::SerError> {
335        crate::serde_support::to_value(value)
336    }
337
338    /// Deserialize this `Value` into a Rust type.
339    ///
340    /// This is the same as [`from_value`](crate::from_value) but available as
341    /// a method on `Value` for convenience.
342    ///
343    /// # Errors
344    ///
345    /// Returns an error if the value shape doesn't match `T`.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use md_tmpl_core::Value;
351    /// use serde::Deserialize;
352    ///
353    /// #[derive(Deserialize, Debug, PartialEq)]
354    /// struct Agent {
355    ///     name: String,
356    /// }
357    ///
358    /// let val = Value::new_struct([("name", Value::Str("Alice".into()))]);
359    /// let agent: Agent = val.deserialize_into().unwrap();
360    /// assert_eq!(
361    ///     agent,
362    ///     Agent {
363    ///         name: "Alice".into()
364    ///     }
365    /// );
366    /// ```
367    pub fn deserialize_into<'de, T: serde::Deserialize<'de>>(
368        &'de self,
369    ) -> Result<T, crate::serde_support::DeError> {
370        crate::serde_support::from_value(self)
371    }
372}
373
374/// `FlexBuffers` support — requires `std` (the `flexbuffers` crate does not
375/// support `no_std`).
376#[cfg(feature = "std")]
377#[cfg(feature = "serde")]
378impl Value {
379    /// Create a `Value` from a `FlexBuffers` binary buffer.
380    ///
381    /// # Errors
382    ///
383    /// Returns an error if the buffer is invalid or deserialization fails.
384    pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
385        let r = flexbuffers::Reader::get_root(data).map_err(|e| {
386            crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
387        })?;
388        serde::Deserialize::deserialize(r).map_err(|e| {
389            crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
390        })
391    }
392}
393
394// ---------------------------------------------------------------------------
395// From conversions
396// ---------------------------------------------------------------------------
397
398impl From<&str> for Value {
399    fn from(s: &str) -> Self {
400        Self::Str(s.to_string())
401    }
402}
403
404impl From<String> for Value {
405    fn from(s: String) -> Self {
406        Self::Str(s)
407    }
408}
409
410impl From<bool> for Value {
411    fn from(b: bool) -> Self {
412        Self::Bool(b)
413    }
414}
415
416impl From<i64> for Value {
417    fn from(i: i64) -> Self {
418        Self::Int(i)
419    }
420}
421
422impl From<i32> for Value {
423    fn from(i: i32) -> Self {
424        Self::Int(i64::from(i))
425    }
426}
427
428impl From<u32> for Value {
429    fn from(i: u32) -> Self {
430        Self::Int(i64::from(i))
431    }
432}
433
434impl TryFrom<u64> for Value {
435    type Error = core::num::TryFromIntError;
436    fn try_from(i: u64) -> Result<Self, Self::Error> {
437        Ok(Self::Int(i64::try_from(i)?))
438    }
439}
440
441impl TryFrom<usize> for Value {
442    type Error = core::num::TryFromIntError;
443    fn try_from(i: usize) -> Result<Self, Self::Error> {
444        Ok(Self::Int(i64::try_from(i)?))
445    }
446}
447
448impl From<f64> for Value {
449    fn from(f: f64) -> Self {
450        Self::Float(f)
451    }
452}
453
454impl From<f32> for Value {
455    fn from(f: f32) -> Self {
456        Self::Float(f64::from(f))
457    }
458}
459
460impl From<Vec<Value>> for Value {
461    fn from(v: Vec<Value>) -> Self {
462        Self::List(Arc::new(v))
463    }
464}
465
466impl From<HashMap<String, Value>> for Value {
467    fn from(m: HashMap<String, Value>) -> Self {
468        Self::Struct(Arc::new(m))
469    }
470}
471
472impl From<crate::template::Template> for Value {
473    fn from(t: crate::template::Template) -> Self {
474        Self::Tmpl(Arc::new(t))
475    }
476}
477
478impl From<Arc<crate::template::Template>> for Value {
479    fn from(t: Arc<crate::template::Template>) -> Self {
480        Self::Tmpl(t)
481    }
482}
483
484impl From<&crate::template::Template> for Value {
485    fn from(t: &crate::template::Template) -> Self {
486        Self::Tmpl(Arc::new(t.clone()))
487    }
488}
489
490// ---------------------------------------------------------------------------
491// TryFrom conversions (consuming)
492// ---------------------------------------------------------------------------
493
494/// Error returned when a [`Value`] is the wrong variant for a conversion.
495#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct ValueTypeError {
497    /// The expected type name.
498    pub expected: &'static str,
499    /// The actual type name of the value.
500    pub actual: &'static str,
501}
502
503impl fmt::Display for ValueTypeError {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        write!(f, "expected {}, got {}", self.expected, self.actual)
506    }
507}
508
509impl core::error::Error for ValueTypeError {}
510
511impl TryFrom<Value> for String {
512    type Error = ValueTypeError;
513    fn try_from(v: Value) -> Result<Self, Self::Error> {
514        match v {
515            Value::Str(s) => Ok(s),
516            other => Err(ValueTypeError {
517                expected: crate::consts::TYPE_STR,
518                actual: other.type_name(),
519            }),
520        }
521    }
522}
523
524impl TryFrom<Value> for i64 {
525    type Error = ValueTypeError;
526    fn try_from(v: Value) -> Result<Self, Self::Error> {
527        match v {
528            Value::Int(i) => Ok(i),
529            other => Err(ValueTypeError {
530                expected: crate::consts::TYPE_INT,
531                actual: other.type_name(),
532            }),
533        }
534    }
535}
536
537impl TryFrom<Value> for f64 {
538    type Error = ValueTypeError;
539    fn try_from(v: Value) -> Result<Self, Self::Error> {
540        match v {
541            Value::Float(f) => Ok(f),
542            other => Err(ValueTypeError {
543                expected: crate::consts::TYPE_FLOAT,
544                actual: other.type_name(),
545            }),
546        }
547    }
548}
549
550impl TryFrom<Value> for bool {
551    type Error = ValueTypeError;
552    fn try_from(v: Value) -> Result<Self, Self::Error> {
553        match v {
554            Value::Bool(b) => Ok(b),
555            other => Err(ValueTypeError {
556                expected: crate::consts::TYPE_BOOL,
557                actual: other.type_name(),
558            }),
559        }
560    }
561}
562
563impl TryFrom<Value> for Vec<Value> {
564    type Error = ValueTypeError;
565    fn try_from(v: Value) -> Result<Self, Self::Error> {
566        match v {
567            Value::List(l) => Ok(Arc::try_unwrap(l).unwrap_or_else(|arc| (*arc).clone())),
568            other => Err(ValueTypeError {
569                expected: crate::consts::TYPE_LIST,
570                actual: other.type_name(),
571            }),
572        }
573    }
574}
575
576impl<S: core::hash::BuildHasher + Default> TryFrom<Value> for HashMap<String, Value, S> {
577    type Error = ValueTypeError;
578    fn try_from(v: Value) -> Result<Self, Self::Error> {
579        match v {
580            Value::Struct(m) => {
581                let owned = Arc::try_unwrap(m).unwrap_or_else(|arc| (*arc).clone());
582                Ok(owned.into_iter().collect())
583            }
584            other => Err(ValueTypeError {
585                expected: crate::consts::TYPE_STRUCT,
586                actual: other.type_name(),
587            }),
588        }
589    }
590}
591
592// ---------------------------------------------------------------------------
593// Tests
594// ---------------------------------------------------------------------------
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599
600    // -- Display --
601
602    #[test]
603    fn display_str() {
604        assert_eq!(Value::Str("hello".into()).to_string(), "hello");
605    }
606
607    #[test]
608    fn display_bool() {
609        assert_eq!(Value::Bool(true).to_string(), "true");
610        assert_eq!(Value::Bool(false).to_string(), "false");
611    }
612
613    #[test]
614    fn display_int() {
615        assert_eq!(Value::Int(42).to_string(), "42");
616        assert_eq!(Value::Int(-7).to_string(), "-7");
617    }
618
619    #[test]
620    fn display_float() {
621        assert_eq!(Value::Float(3.25).to_string(), "3.25");
622    }
623
624    #[test]
625    fn display_list() {
626        let list = Value::List(Arc::new(vec![Value::Int(1)]));
627        assert_eq!(list.to_string(), "[<list of 1>]");
628        assert_eq!(Value::List(Arc::new(vec![])).to_string(), "[<list of 0>]");
629    }
630
631    #[test]
632    fn display_dict() {
633        let dict = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
634        assert_eq!(dict.to_string(), "{<struct of 1>}");
635        assert_eq!(
636            Value::Struct(Arc::new(HashMap::new())).to_string(),
637            "{<struct of 0>}"
638        );
639    }
640
641    // -- is_truthy --
642
643    #[test]
644    fn truthy_str() {
645        assert!(Value::Str("hello".into()).is_truthy());
646        assert!(!Value::Str(String::new()).is_truthy());
647    }
648
649    #[test]
650    fn truthy_bool() {
651        assert!(Value::Bool(true).is_truthy());
652        assert!(!Value::Bool(false).is_truthy());
653    }
654
655    #[test]
656    fn truthy_int() {
657        assert!(Value::Int(1).is_truthy());
658        assert!(Value::Int(-1).is_truthy());
659        assert!(!Value::Int(0).is_truthy());
660    }
661
662    #[test]
663    fn truthy_float() {
664        assert!(Value::Float(0.1).is_truthy());
665        assert!(!Value::Float(0.0).is_truthy());
666    }
667
668    #[test]
669    fn truthy_list() {
670        assert!(Value::List(Arc::new(vec![Value::Int(1)])).is_truthy());
671        assert!(!Value::List(Arc::new(vec![])).is_truthy());
672    }
673
674    #[test]
675    fn truthy_dict() {
676        let populated = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
677        assert!(populated.is_truthy());
678        assert!(!Value::Struct(Arc::new(HashMap::new())).is_truthy());
679    }
680
681    // -- type_name --
682
683    #[test]
684    fn type_names() {
685        assert_eq!(Value::Str("x".into()).type_name(), "str");
686        assert_eq!(Value::Bool(true).type_name(), "bool");
687        assert_eq!(Value::Int(0).type_name(), "int");
688        assert_eq!(Value::Float(0.0).type_name(), "float");
689        assert_eq!(Value::List(Arc::new(vec![])).type_name(), "list");
690        assert_eq!(
691            Value::Struct(Arc::new(HashMap::new())).type_name(),
692            "struct"
693        );
694    }
695
696    // -- get_field --
697
698    #[test]
699    fn get_field_on_dict() {
700        let dict = Value::Struct(Arc::new(HashMap::from([
701            ("name".into(), Value::Str("Alice".into())),
702            ("score".into(), Value::Int(95)),
703        ])));
704        assert_eq!(dict.get_field("name"), Some(&Value::Str("Alice".into())));
705        assert_eq!(dict.get_field("score"), Some(&Value::Int(95)));
706        assert_eq!(dict.get_field("missing"), None);
707    }
708
709    #[test]
710    fn get_field_on_non_dict_returns_none() {
711        assert_eq!(Value::Str("x".into()).get_field("any"), None);
712        assert_eq!(Value::Int(1).get_field("any"), None);
713        assert_eq!(Value::List(Arc::new(vec![])).get_field("any"), None);
714    }
715
716    // -- From conversions --
717
718    #[test]
719    fn from_str_ref() {
720        let v: Value = "hello".into();
721        assert_eq!(v, Value::Str("hello".into()));
722    }
723
724    #[test]
725    fn from_string() {
726        let v: Value = String::from("world").into();
727        assert_eq!(v, Value::Str("world".into()));
728    }
729
730    #[test]
731    fn from_bool() {
732        let v: Value = true.into();
733        assert_eq!(v, Value::Bool(true));
734    }
735
736    #[test]
737    fn from_i64() {
738        let v: Value = 42_i64.into();
739        assert_eq!(v, Value::Int(42));
740    }
741
742    #[test]
743    fn from_i32() {
744        let v: Value = 7_i32.into();
745        assert_eq!(v, Value::Int(7));
746    }
747
748    #[test]
749    fn from_u32() {
750        let v: Value = 100_u32.into();
751        assert_eq!(v, Value::Int(100));
752    }
753
754    #[test]
755    fn try_from_u64() {
756        let v = Value::try_from(999_u64).unwrap();
757        assert_eq!(v, Value::Int(999));
758    }
759
760    #[test]
761    fn try_from_u64_overflow() {
762        let result = Value::try_from(u64::MAX);
763        assert!(result.is_err(), "u64::MAX should not fit in i64");
764    }
765
766    #[test]
767    fn try_from_usize() {
768        let v = Value::try_from(5_usize).unwrap();
769        assert_eq!(v, Value::Int(5));
770    }
771
772    #[test]
773    fn from_f64() {
774        let v: Value = 2.5_f64.into();
775        assert_eq!(v, Value::Float(2.5));
776    }
777
778    #[test]
779    fn from_f32() {
780        let v: Value = 1.5_f32.into();
781        // f32 → f64 conversion
782        assert!(matches!(v, Value::Float(f) if (f - 1.5).abs() < f64::EPSILON));
783    }
784
785    #[test]
786    fn from_vec_value() {
787        let items = vec![Value::Int(1), Value::Str("two".into())];
788        let v: Value = items.into();
789        assert!(matches!(v, Value::List(ref l) if l.len() == 2));
790    }
791
792    #[test]
793    fn from_hashmap_value() {
794        let map = HashMap::from([("k".into(), Value::Bool(true))]);
795        let v: Value = map.into();
796        assert_eq!(v.get_field("k"), Some(&Value::Bool(true)));
797    }
798
799    // -- as_* accessors --
800
801    #[test]
802    fn as_str_returns_some_for_str() {
803        assert_eq!(Value::Str("hello".into()).as_str(), Some("hello"));
804    }
805
806    #[test]
807    fn as_str_returns_none_for_non_str() {
808        assert_eq!(Value::Int(42).as_str(), None);
809    }
810
811    #[test]
812    fn as_int_returns_some_for_int() {
813        assert_eq!(Value::Int(42).as_int(), Some(42));
814    }
815
816    #[test]
817    fn as_int_returns_none_for_non_int() {
818        assert_eq!(Value::Str("42".into()).as_int(), None);
819    }
820
821    #[test]
822    fn as_float_returns_some_for_float() {
823        assert_eq!(Value::Float(3.25).as_float(), Some(3.25));
824    }
825
826    #[test]
827    fn as_float_returns_none_for_non_float() {
828        assert_eq!(Value::Int(3).as_float(), None);
829    }
830
831    #[test]
832    fn as_bool_returns_some_for_bool() {
833        assert_eq!(Value::Bool(true).as_bool(), Some(true));
834    }
835
836    #[test]
837    fn as_bool_returns_none_for_non_bool() {
838        assert_eq!(Value::Str("true".into()).as_bool(), None);
839    }
840
841    #[test]
842    fn as_list_returns_some_for_list() {
843        let items = vec![Value::Int(1), Value::Int(2)];
844        let v = Value::List(Arc::new(items.clone()));
845        assert_eq!(v.as_list(), Some(items.as_slice()));
846    }
847
848    #[test]
849    fn as_list_returns_none_for_non_list() {
850        assert_eq!(Value::Int(1).as_list(), None);
851    }
852
853    #[test]
854    fn as_struct_returns_some_for_dict() {
855        let map = HashMap::from([("k".into(), Value::Int(1))]);
856        let v = Value::Struct(Arc::new(map.clone()));
857        assert_eq!(v.as_struct(), Some(&map));
858    }
859
860    #[test]
861    fn as_struct_returns_none_for_non_dict() {
862        assert_eq!(Value::Int(1).as_struct(), None);
863    }
864
865    // -- TryFrom conversions --
866
867    #[test]
868    fn try_from_str_success() {
869        let v = Value::Str("hello".into());
870        assert_eq!(String::try_from(v).unwrap(), "hello");
871    }
872
873    #[test]
874    fn try_from_str_failure_has_message() {
875        let v = Value::Int(42);
876        let err = String::try_from(v).unwrap_err();
877        assert_eq!(err.expected, "str");
878        assert_eq!(err.actual, "int");
879        assert_eq!(err.to_string(), "expected str, got int");
880    }
881
882    #[test]
883    fn try_from_i64_success() {
884        let v = Value::Int(99);
885        assert_eq!(i64::try_from(v).unwrap(), 99);
886    }
887
888    #[test]
889    fn try_from_i64_failure() {
890        let v = Value::Str("99".into());
891        let err = i64::try_from(v).expect_err("Str should not convert to i64");
892        assert_eq!(err.expected, "int");
893        assert_eq!(err.actual, "str");
894    }
895
896    #[test]
897    fn try_from_f64_success() {
898        let v = Value::Float(2.5);
899        assert!((f64::try_from(v).unwrap() - 2.5).abs() < f64::EPSILON);
900    }
901
902    #[test]
903    fn try_from_bool_success() {
904        let v = Value::Bool(false);
905        assert!(!bool::try_from(v).unwrap());
906    }
907
908    #[test]
909    fn try_from_vec_success() {
910        let v = Value::List(Arc::new(vec![Value::Int(1)]));
911        let list = Vec::<Value>::try_from(v).unwrap();
912        assert_eq!(list.len(), 1);
913    }
914
915    #[test]
916    fn try_from_hashmap_success() {
917        let v = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
918        let map = HashMap::<String, Value>::try_from(v).unwrap();
919        assert_eq!(map.len(), 1);
920    }
921
922    #[test]
923    fn from_template_owned() {
924        let tmpl = crate::Template::from_source(
925            r"---
926params: [x = str]
927---
928{{ x }}",
929        )
930        .unwrap();
931        let val = Value::from(tmpl);
932        assert!(matches!(val, Value::Tmpl(_)));
933        assert_eq!(val.type_name(), "tmpl");
934    }
935
936    #[test]
937    fn from_template_ref() {
938        let tmpl = crate::Template::from_source(
939            r"---
940params: [x = str]
941---
942{{ x }}",
943        )
944        .unwrap();
945        let val = Value::from(&tmpl);
946        assert!(matches!(val, Value::Tmpl(_)));
947    }
948
949    #[test]
950    fn from_template_arc() {
951        let tmpl = crate::Template::from_source(
952            r"---
953params: [x = str]
954---
955{{ x }}",
956        )
957        .unwrap();
958        let arc = Arc::new(tmpl);
959        let val = Value::from(arc);
960        assert!(matches!(val, Value::Tmpl(_)));
961    }
962
963    #[test]
964    fn context_set_with_template() {
965        let tmpl = crate::Template::from_source(
966            r"---
967params: [x = str]
968---
969{{ x }}",
970        )
971        .unwrap();
972        let mut ctx = crate::Context::new();
973        // Should compile — From<Template> for Value
974        ctx.set("widget", tmpl);
975        assert!(ctx.get("widget").unwrap().as_tmpl().is_some());
976    }
977
978    #[test]
979    fn context_set_with_template_ref() {
980        let tmpl = crate::Template::from_source(
981            r"---
982params: [x = str]
983---
984{{ x }}",
985        )
986        .unwrap();
987        let mut ctx = crate::Context::new();
988        // Should compile — From<&Template> for Value
989        ctx.set("widget", &tmpl);
990        assert!(ctx.get("widget").unwrap().as_tmpl().is_some());
991    }
992}