Skip to main content

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