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}