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 — behind the `flexbuffers` feature, which implies
375/// `std` and `serde` (the `flexbuffers` crate does not support `no_std`).
376#[cfg(feature = "flexbuffers")]
377impl Value {
378    /// Create a `Value` from a `FlexBuffers` binary buffer.
379    ///
380    /// # Errors
381    ///
382    /// Returns an error if the buffer is invalid or deserialization fails.
383    pub fn from_flexbuffers(data: &[u8]) -> Result<Self, crate::error::TemplateError> {
384        let r = flexbuffers::Reader::get_root(data).map_err(|e| {
385            crate::error::TemplateError::syntax(format!("flexbuffers root error: {e}"))
386        })?;
387        serde::Deserialize::deserialize(r).map_err(|e| {
388            crate::error::TemplateError::syntax(format!("flexbuffers deserialization failed: {e}"))
389        })
390    }
391}
392
393// ---------------------------------------------------------------------------
394// From conversions
395// ---------------------------------------------------------------------------
396
397impl From<&str> for Value {
398    fn from(s: &str) -> Self {
399        Self::Str(s.to_string())
400    }
401}
402
403impl From<String> for Value {
404    fn from(s: String) -> Self {
405        Self::Str(s)
406    }
407}
408
409impl From<bool> for Value {
410    fn from(b: bool) -> Self {
411        Self::Bool(b)
412    }
413}
414
415impl From<i64> for Value {
416    fn from(i: i64) -> Self {
417        Self::Int(i)
418    }
419}
420
421impl From<i32> for Value {
422    fn from(i: i32) -> Self {
423        Self::Int(i64::from(i))
424    }
425}
426
427impl From<u32> for Value {
428    fn from(i: u32) -> Self {
429        Self::Int(i64::from(i))
430    }
431}
432
433impl TryFrom<u64> for Value {
434    type Error = core::num::TryFromIntError;
435    fn try_from(i: u64) -> Result<Self, Self::Error> {
436        Ok(Self::Int(i64::try_from(i)?))
437    }
438}
439
440impl TryFrom<usize> for Value {
441    type Error = core::num::TryFromIntError;
442    fn try_from(i: usize) -> Result<Self, Self::Error> {
443        Ok(Self::Int(i64::try_from(i)?))
444    }
445}
446
447impl From<f64> for Value {
448    fn from(f: f64) -> Self {
449        Self::Float(f)
450    }
451}
452
453impl From<f32> for Value {
454    fn from(f: f32) -> Self {
455        Self::Float(f64::from(f))
456    }
457}
458
459impl From<Vec<Value>> for Value {
460    fn from(v: Vec<Value>) -> Self {
461        Self::List(Arc::new(v))
462    }
463}
464
465impl From<HashMap<String, Value>> for Value {
466    fn from(m: HashMap<String, Value>) -> Self {
467        Self::Struct(Arc::new(m))
468    }
469}
470
471impl From<crate::template::Template> for Value {
472    fn from(t: crate::template::Template) -> Self {
473        Self::Tmpl(Arc::new(t))
474    }
475}
476
477impl From<Arc<crate::template::Template>> for Value {
478    fn from(t: Arc<crate::template::Template>) -> Self {
479        Self::Tmpl(t)
480    }
481}
482
483impl From<&crate::template::Template> for Value {
484    fn from(t: &crate::template::Template) -> Self {
485        Self::Tmpl(Arc::new(t.clone()))
486    }
487}
488
489// ---------------------------------------------------------------------------
490// TryFrom conversions (consuming)
491// ---------------------------------------------------------------------------
492
493/// Error returned when a [`Value`] is the wrong variant for a conversion.
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct ValueTypeError {
496    /// The expected type name.
497    pub expected: &'static str,
498    /// The actual type name of the value.
499    pub actual: &'static str,
500}
501
502impl fmt::Display for ValueTypeError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        write!(f, "expected {}, got {}", self.expected, self.actual)
505    }
506}
507
508impl core::error::Error for ValueTypeError {}
509
510impl TryFrom<Value> for String {
511    type Error = ValueTypeError;
512    fn try_from(v: Value) -> Result<Self, Self::Error> {
513        match v {
514            Value::Str(s) => Ok(s),
515            other => Err(ValueTypeError {
516                expected: crate::consts::TYPE_STR,
517                actual: other.type_name(),
518            }),
519        }
520    }
521}
522
523impl TryFrom<Value> for i64 {
524    type Error = ValueTypeError;
525    fn try_from(v: Value) -> Result<Self, Self::Error> {
526        match v {
527            Value::Int(i) => Ok(i),
528            other => Err(ValueTypeError {
529                expected: crate::consts::TYPE_INT,
530                actual: other.type_name(),
531            }),
532        }
533    }
534}
535
536impl TryFrom<Value> for f64 {
537    type Error = ValueTypeError;
538    fn try_from(v: Value) -> Result<Self, Self::Error> {
539        match v {
540            Value::Float(f) => Ok(f),
541            other => Err(ValueTypeError {
542                expected: crate::consts::TYPE_FLOAT,
543                actual: other.type_name(),
544            }),
545        }
546    }
547}
548
549impl TryFrom<Value> for bool {
550    type Error = ValueTypeError;
551    fn try_from(v: Value) -> Result<Self, Self::Error> {
552        match v {
553            Value::Bool(b) => Ok(b),
554            other => Err(ValueTypeError {
555                expected: crate::consts::TYPE_BOOL,
556                actual: other.type_name(),
557            }),
558        }
559    }
560}
561
562impl TryFrom<Value> for Vec<Value> {
563    type Error = ValueTypeError;
564    fn try_from(v: Value) -> Result<Self, Self::Error> {
565        match v {
566            Value::List(l) => Ok(Arc::try_unwrap(l).unwrap_or_else(|arc| (*arc).clone())),
567            other => Err(ValueTypeError {
568                expected: crate::consts::TYPE_LIST,
569                actual: other.type_name(),
570            }),
571        }
572    }
573}
574
575impl<S: core::hash::BuildHasher + Default> TryFrom<Value> for HashMap<String, Value, S> {
576    type Error = ValueTypeError;
577    fn try_from(v: Value) -> Result<Self, Self::Error> {
578        match v {
579            Value::Struct(m) => {
580                let owned = Arc::try_unwrap(m).unwrap_or_else(|arc| (*arc).clone());
581                Ok(owned.into_iter().collect())
582            }
583            other => Err(ValueTypeError {
584                expected: crate::consts::TYPE_STRUCT,
585                actual: other.type_name(),
586            }),
587        }
588    }
589}
590
591// ---------------------------------------------------------------------------
592// Tests
593// ---------------------------------------------------------------------------
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    // -- Display --
600
601    #[test]
602    fn display_str() {
603        assert_eq!(Value::Str("hello".into()).to_string(), "hello");
604    }
605
606    #[test]
607    fn display_bool() {
608        assert_eq!(Value::Bool(true).to_string(), "true");
609        assert_eq!(Value::Bool(false).to_string(), "false");
610    }
611
612    #[test]
613    fn display_int() {
614        assert_eq!(Value::Int(42).to_string(), "42");
615        assert_eq!(Value::Int(-7).to_string(), "-7");
616    }
617
618    #[test]
619    fn display_float() {
620        assert_eq!(Value::Float(3.25).to_string(), "3.25");
621    }
622
623    #[test]
624    fn display_list() {
625        let list = Value::List(Arc::new(vec![Value::Int(1)]));
626        assert_eq!(list.to_string(), "[<list of 1>]");
627        assert_eq!(Value::List(Arc::new(vec![])).to_string(), "[<list of 0>]");
628    }
629
630    #[test]
631    fn display_dict() {
632        let dict = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
633        assert_eq!(dict.to_string(), "{<struct of 1>}");
634        assert_eq!(
635            Value::Struct(Arc::new(HashMap::new())).to_string(),
636            "{<struct of 0>}"
637        );
638    }
639
640    // -- FlexBuffers --
641
642    #[cfg(feature = "flexbuffers")]
643    #[test]
644    fn from_flexbuffers_roundtrip() {
645        use serde::Serialize;
646
647        let source = alloc::collections::BTreeMap::from([("name", "Alice"), ("role", "admin")]);
648        let mut ser = flexbuffers::FlexbufferSerializer::new();
649        source.serialize(&mut ser).expect("flexbuffers encode");
650
651        match Value::from_flexbuffers(ser.view()).expect("from_flexbuffers") {
652            Value::Struct(map) => {
653                assert_eq!(map.get("name"), Some(&Value::Str("Alice".into())));
654                assert_eq!(map.get("role"), Some(&Value::Str("admin".into())));
655            }
656            _ => panic!("expected a struct value"),
657        }
658    }
659
660    #[cfg(feature = "flexbuffers")]
661    #[test]
662    fn from_flexbuffers_rejects_garbage() {
663        Value::from_flexbuffers(&[0xde, 0xad, 0xbe, 0xef])
664            .expect_err("garbage flexbuffer must error without panicking");
665    }
666
667    // -- is_truthy --
668
669    #[test]
670    fn truthy_str() {
671        assert!(Value::Str("hello".into()).is_truthy());
672        assert!(!Value::Str(String::new()).is_truthy());
673    }
674
675    #[test]
676    fn truthy_bool() {
677        assert!(Value::Bool(true).is_truthy());
678        assert!(!Value::Bool(false).is_truthy());
679    }
680
681    #[test]
682    fn truthy_int() {
683        assert!(Value::Int(1).is_truthy());
684        assert!(Value::Int(-1).is_truthy());
685        assert!(!Value::Int(0).is_truthy());
686    }
687
688    #[test]
689    fn truthy_float() {
690        assert!(Value::Float(0.1).is_truthy());
691        assert!(!Value::Float(0.0).is_truthy());
692    }
693
694    #[test]
695    fn truthy_list() {
696        assert!(Value::List(Arc::new(vec![Value::Int(1)])).is_truthy());
697        assert!(!Value::List(Arc::new(vec![])).is_truthy());
698    }
699
700    #[test]
701    fn truthy_dict() {
702        let populated = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
703        assert!(populated.is_truthy());
704        assert!(!Value::Struct(Arc::new(HashMap::new())).is_truthy());
705    }
706
707    // -- type_name --
708
709    #[test]
710    fn type_names() {
711        assert_eq!(Value::Str("x".into()).type_name(), "str");
712        assert_eq!(Value::Bool(true).type_name(), "bool");
713        assert_eq!(Value::Int(0).type_name(), "int");
714        assert_eq!(Value::Float(0.0).type_name(), "float");
715        assert_eq!(Value::List(Arc::new(vec![])).type_name(), "list");
716        assert_eq!(
717            Value::Struct(Arc::new(HashMap::new())).type_name(),
718            "struct"
719        );
720    }
721
722    // -- get_field --
723
724    #[test]
725    fn get_field_on_dict() {
726        let dict = Value::Struct(Arc::new(HashMap::from([
727            ("name".into(), Value::Str("Alice".into())),
728            ("score".into(), Value::Int(95)),
729        ])));
730        assert_eq!(dict.get_field("name"), Some(&Value::Str("Alice".into())));
731        assert_eq!(dict.get_field("score"), Some(&Value::Int(95)));
732        assert_eq!(dict.get_field("missing"), None);
733    }
734
735    #[test]
736    fn get_field_on_non_dict_returns_none() {
737        assert_eq!(Value::Str("x".into()).get_field("any"), None);
738        assert_eq!(Value::Int(1).get_field("any"), None);
739        assert_eq!(Value::List(Arc::new(vec![])).get_field("any"), None);
740    }
741
742    // -- From conversions --
743
744    #[test]
745    fn from_str_ref() {
746        let v: Value = "hello".into();
747        assert_eq!(v, Value::Str("hello".into()));
748    }
749
750    #[test]
751    fn from_string() {
752        let v: Value = String::from("world").into();
753        assert_eq!(v, Value::Str("world".into()));
754    }
755
756    #[test]
757    fn from_bool() {
758        let v: Value = true.into();
759        assert_eq!(v, Value::Bool(true));
760    }
761
762    #[test]
763    fn from_i64() {
764        let v: Value = 42_i64.into();
765        assert_eq!(v, Value::Int(42));
766    }
767
768    #[test]
769    fn from_i32() {
770        let v: Value = 7_i32.into();
771        assert_eq!(v, Value::Int(7));
772    }
773
774    #[test]
775    fn from_u32() {
776        let v: Value = 100_u32.into();
777        assert_eq!(v, Value::Int(100));
778    }
779
780    #[test]
781    fn try_from_u64() {
782        let v = Value::try_from(999_u64).unwrap();
783        assert_eq!(v, Value::Int(999));
784    }
785
786    #[test]
787    fn try_from_u64_overflow() {
788        let result = Value::try_from(u64::MAX);
789        assert!(result.is_err(), "u64::MAX should not fit in i64");
790    }
791
792    #[test]
793    fn try_from_usize() {
794        let v = Value::try_from(5_usize).unwrap();
795        assert_eq!(v, Value::Int(5));
796    }
797
798    #[test]
799    fn from_f64() {
800        let v: Value = 2.5_f64.into();
801        assert_eq!(v, Value::Float(2.5));
802    }
803
804    #[test]
805    fn from_f32() {
806        let v: Value = 1.5_f32.into();
807        // f32 → f64 conversion
808        assert!(matches!(v, Value::Float(f) if (f - 1.5).abs() < f64::EPSILON));
809    }
810
811    #[test]
812    fn from_vec_value() {
813        let items = vec![Value::Int(1), Value::Str("two".into())];
814        let v: Value = items.into();
815        assert!(matches!(v, Value::List(ref l) if l.len() == 2));
816    }
817
818    #[test]
819    fn from_hashmap_value() {
820        let map = HashMap::from([("k".into(), Value::Bool(true))]);
821        let v: Value = map.into();
822        assert_eq!(v.get_field("k"), Some(&Value::Bool(true)));
823    }
824
825    // -- as_* accessors --
826
827    #[test]
828    fn as_str_returns_some_for_str() {
829        assert_eq!(Value::Str("hello".into()).as_str(), Some("hello"));
830    }
831
832    #[test]
833    fn as_str_returns_none_for_non_str() {
834        assert_eq!(Value::Int(42).as_str(), None);
835    }
836
837    #[test]
838    fn as_int_returns_some_for_int() {
839        assert_eq!(Value::Int(42).as_int(), Some(42));
840    }
841
842    #[test]
843    fn as_int_returns_none_for_non_int() {
844        assert_eq!(Value::Str("42".into()).as_int(), None);
845    }
846
847    #[test]
848    fn as_float_returns_some_for_float() {
849        assert_eq!(Value::Float(3.25).as_float(), Some(3.25));
850    }
851
852    #[test]
853    fn as_float_returns_none_for_non_float() {
854        assert_eq!(Value::Int(3).as_float(), None);
855    }
856
857    #[test]
858    fn as_bool_returns_some_for_bool() {
859        assert_eq!(Value::Bool(true).as_bool(), Some(true));
860    }
861
862    #[test]
863    fn as_bool_returns_none_for_non_bool() {
864        assert_eq!(Value::Str("true".into()).as_bool(), None);
865    }
866
867    #[test]
868    fn as_list_returns_some_for_list() {
869        let items = vec![Value::Int(1), Value::Int(2)];
870        let v = Value::List(Arc::new(items.clone()));
871        assert_eq!(v.as_list(), Some(items.as_slice()));
872    }
873
874    #[test]
875    fn as_list_returns_none_for_non_list() {
876        assert_eq!(Value::Int(1).as_list(), None);
877    }
878
879    #[test]
880    fn as_struct_returns_some_for_dict() {
881        let map = HashMap::from([("k".into(), Value::Int(1))]);
882        let v = Value::Struct(Arc::new(map.clone()));
883        assert_eq!(v.as_struct(), Some(&map));
884    }
885
886    #[test]
887    fn as_struct_returns_none_for_non_dict() {
888        assert_eq!(Value::Int(1).as_struct(), None);
889    }
890
891    // -- TryFrom conversions --
892
893    #[test]
894    fn try_from_str_success() {
895        let v = Value::Str("hello".into());
896        assert_eq!(String::try_from(v).unwrap(), "hello");
897    }
898
899    #[test]
900    fn try_from_str_failure_has_message() {
901        let v = Value::Int(42);
902        let err = String::try_from(v).unwrap_err();
903        assert_eq!(err.expected, "str");
904        assert_eq!(err.actual, "int");
905        assert_eq!(err.to_string(), "expected str, got int");
906    }
907
908    #[test]
909    fn try_from_i64_success() {
910        let v = Value::Int(99);
911        assert_eq!(i64::try_from(v).unwrap(), 99);
912    }
913
914    #[test]
915    fn try_from_i64_failure() {
916        let v = Value::Str("99".into());
917        let err = i64::try_from(v).expect_err("Str should not convert to i64");
918        assert_eq!(err.expected, "int");
919        assert_eq!(err.actual, "str");
920    }
921
922    #[test]
923    fn try_from_f64_success() {
924        let v = Value::Float(2.5);
925        assert!((f64::try_from(v).unwrap() - 2.5).abs() < f64::EPSILON);
926    }
927
928    #[test]
929    fn try_from_bool_success() {
930        let v = Value::Bool(false);
931        assert!(!bool::try_from(v).unwrap());
932    }
933
934    #[test]
935    fn try_from_vec_success() {
936        let v = Value::List(Arc::new(vec![Value::Int(1)]));
937        let list = Vec::<Value>::try_from(v).unwrap();
938        assert_eq!(list.len(), 1);
939    }
940
941    #[test]
942    fn try_from_hashmap_success() {
943        let v = Value::Struct(Arc::new(HashMap::from([("k".into(), Value::Int(1))])));
944        let map = HashMap::<String, Value>::try_from(v).unwrap();
945        assert_eq!(map.len(), 1);
946    }
947
948    #[test]
949    fn from_template_owned() {
950        let tmpl = crate::Template::from_source(
951            r"---
952params: [x = str]
953---
954{{ x }}",
955        )
956        .unwrap();
957        let val = Value::from(tmpl);
958        assert!(matches!(val, Value::Tmpl(_)));
959        assert_eq!(val.type_name(), "tmpl");
960    }
961
962    #[test]
963    fn from_template_ref() {
964        let tmpl = crate::Template::from_source(
965            r"---
966params: [x = str]
967---
968{{ x }}",
969        )
970        .unwrap();
971        let val = Value::from(&tmpl);
972        assert!(matches!(val, Value::Tmpl(_)));
973    }
974
975    #[test]
976    fn from_template_arc() {
977        let tmpl = crate::Template::from_source(
978            r"---
979params: [x = str]
980---
981{{ x }}",
982        )
983        .unwrap();
984        let arc = Arc::new(tmpl);
985        let val = Value::from(arc);
986        assert!(matches!(val, Value::Tmpl(_)));
987    }
988
989    #[test]
990    fn context_set_with_template() {
991        let tmpl = crate::Template::from_source(
992            r"---
993params: [x = str]
994---
995{{ x }}",
996        )
997        .unwrap();
998        let mut ctx = crate::Context::new();
999        // Should compile — From<Template> for Value
1000        ctx.set("widget", tmpl);
1001        assert!(ctx.get("widget").unwrap().as_tmpl().is_some());
1002    }
1003
1004    #[test]
1005    fn context_set_with_template_ref() {
1006        let tmpl = crate::Template::from_source(
1007            r"---
1008params: [x = str]
1009---
1010{{ x }}",
1011        )
1012        .unwrap();
1013        let mut ctx = crate::Context::new();
1014        // Should compile — From<&Template> for Value
1015        ctx.set("widget", &tmpl);
1016        assert!(ctx.get("widget").unwrap().as_tmpl().is_some());
1017    }
1018}