Skip to main content

tfparser_core/ir/
value.rs

1//! Resolved values: the result of evaluating an [`Expression`] against an
2//! evaluator context.
3//!
4//! [`Expression`]: crate::ir::Expression
5//!
6//! Per [10-data-model.md § 2.3], we keep `Number` and `Int` as separate
7//! variants for the integer-literal fast path; `Number` is the upstream
8//! HCL2 representation. The two are *not* numerically interconvertible at
9//! the IR level — round-tripping a literal preserves its source form.
10//!
11//! `Map` is an *ordered* `Vec<(Arc<str>, Value)>` rather than a `HashMap`:
12//! insertion order is semantically meaningful for the canonical JSON we
13//! emit ([20-parquet-exporter.md § 3.3]).
14//!
15//! [10-data-model.md § 2.3]: ../../specs/10-data-model.md
16//! [20-parquet-exporter.md § 3.3]: ../../specs/20-parquet-exporter.md
17
18use std::sync::Arc;
19
20use serde::{Deserialize, Serialize};
21
22/// Insertion-ordered map of string key → [`Value`].
23pub type Map = Vec<(Arc<str>, Value)>;
24
25/// A fully-resolved HCL value.
26///
27/// `Value` is what a resolved [`Expression`] reduces to. Anything that
28/// cannot resolve statically stays as [`Expression::Unresolved`].
29///
30/// # Equality
31///
32/// `Value` is **`PartialEq` but not `Eq` or `Hash`** because [`Value::Number`]
33/// wraps an `f64` and `f64::NAN != f64::NAN`. Treat `Value` as a value type,
34/// not as a map / set key. If you need a hashable key, hash the canonical
35/// JSON form ([20-parquet-exporter.md § 3.3](../../specs/20-parquet-exporter.md))
36/// or wrap the number in your own total-order container.
37///
38/// [`Expression`]: crate::ir::Expression
39/// [`Expression::Unresolved`]: crate::ir::Expression::Unresolved
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase", tag = "kind", content = "value")]
42#[non_exhaustive]
43pub enum Value {
44    /// HCL `null`.
45    Null,
46
47    /// Boolean.
48    Bool(bool),
49
50    /// Integer literal (fast path; preserved verbatim from the source).
51    Int(i64),
52
53    /// Floating-point number — the default HCL2 numeric representation.
54    Number(f64),
55
56    /// UTF-8 string (interned-friendly via `Arc<str>`).
57    Str(Arc<str>),
58
59    /// Heterogeneous list.
60    List(Vec<Value>),
61
62    /// Insertion-ordered map.
63    Map(Map),
64}
65
66impl Value {
67    /// Returns the contained string slice, if `self` is a [`Value::Str`].
68    #[must_use]
69    pub fn as_str(&self) -> Option<&str> {
70        match self {
71            Self::Str(s) => Some(s),
72            _ => None,
73        }
74    }
75
76    /// Returns the contained boolean, if `self` is a [`Value::Bool`].
77    #[must_use]
78    pub fn as_bool(&self) -> Option<bool> {
79        match *self {
80            Self::Bool(b) => Some(b),
81            _ => None,
82        }
83    }
84
85    /// Returns the contained integer, if `self` is a [`Value::Int`].
86    #[must_use]
87    pub fn as_int(&self) -> Option<i64> {
88        match *self {
89            Self::Int(n) => Some(n),
90            _ => None,
91        }
92    }
93
94    /// Whether this value contains no information.
95    #[must_use]
96    pub fn is_null(&self) -> bool {
97        matches!(self, Self::Null)
98    }
99}
100
101#[cfg(test)]
102#[allow(
103    clippy::unwrap_used,
104    clippy::expect_used,
105    clippy::panic,
106    clippy::indexing_slicing
107)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_should_round_trip_value_via_serde_json() {
113        let v = Value::Map(vec![
114            (Arc::from("a"), Value::Int(42)),
115            (Arc::from("b"), Value::Str(Arc::from("hello"))),
116            (
117                Arc::from("c"),
118                Value::List(vec![Value::Bool(true), Value::Null]),
119            ),
120        ]);
121        let json = serde_json::to_string(&v).unwrap();
122        let back: Value = serde_json::from_str(&json).unwrap();
123        assert_eq!(v, back);
124    }
125
126    #[test]
127    fn test_should_offer_typed_accessors() {
128        assert_eq!(Value::Str(Arc::from("x")).as_str(), Some("x"));
129        assert_eq!(Value::Bool(true).as_bool(), Some(true));
130        assert_eq!(Value::Int(7).as_int(), Some(7));
131        assert!(Value::Null.is_null());
132    }
133
134    #[test]
135    fn test_should_preserve_map_insertion_order() {
136        let m: Map = vec![
137            (Arc::from("z"), Value::Int(1)),
138            (Arc::from("a"), Value::Int(2)),
139            (Arc::from("m"), Value::Int(3)),
140        ];
141        let Value::Map(got) = Value::Map(m) else {
142            panic!("constructed a Map, did not get one back");
143        };
144        let keys: Vec<&str> = got.iter().map(|(k, _)| k.as_ref()).collect();
145        assert_eq!(keys, vec!["z", "a", "m"]);
146    }
147}