Skip to main content

determa_state/
value.rs

1//! The dynamic value type used for esvs, event payloads, and CEL evaluation.
2//!
3//! Maps keep sorted keys for deterministic JSON serialization (conformance compares
4//! JSON structurally, so key order is irrelevant, but stable output helps humans).
5
6use std::collections::BTreeMap;
7
8/// A Determa State runtime value. Mirrors the esv/payload `type` set (§4.4) plus `null`.
9///
10/// `Value` serializes as its **canonical JSON/native form** (an `Int(3)` is `3`, a
11/// `Bool(true)` is `true`, …), never as a tagged enum — so no engine-internal wrapper
12/// type leaks across any boundary (library, snapshot §8, CLI `--json` §13.4, observer
13/// §8) per SPEC §5.1.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Value {
16    Null,
17    Bool(bool),
18    Int(i64),
19    Float(f64),
20    Str(String),
21    List(Vec<Value>),
22    Map(BTreeMap<String, Value>),
23}
24
25impl Value {
26    pub fn type_name(&self) -> &'static str {
27        match self {
28            Value::Null => "null",
29            Value::Bool(_) => "bool",
30            Value::Int(_) => "int",
31            Value::Float(_) => "float",
32            Value::Str(_) => "string",
33            Value::List(_) => "list",
34            Value::Map(_) => "map",
35        }
36    }
37
38    /// Is this value concretely of the declared esv/payload `type`?
39    /// `null` satisfies any type (an unset variable / optional payload field).
40    pub fn matches_type(&self, ty: &str) -> bool {
41        match (self, ty) {
42            (Value::Null, _) => true,
43            (Value::Bool(_), "bool") => true,
44            (Value::Int(_), "int") => true,
45            (Value::Float(_), "float") => true,
46            (Value::Str(_), "string") => true,
47            (Value::List(_), "list") => true,
48            (Value::Map(_), "map") => true,
49            _ => false,
50        }
51    }
52
53    pub fn as_bool(&self) -> Option<bool> {
54        match self {
55            Value::Bool(b) => Some(*b),
56            _ => None,
57        }
58    }
59
60    pub fn truthy(&self) -> bool {
61        match self {
62            Value::Bool(b) => *b,
63            Value::Null => false,
64            Value::Int(i) => *i != 0,
65            Value::Float(f) => *f != 0.0,
66            Value::Str(s) => !s.is_empty(),
67            Value::List(l) => !l.is_empty(),
68            Value::Map(m) => !m.is_empty(),
69        }
70    }
71
72    pub fn as_str_value(&self) -> Option<&str> {
73        match self {
74            Value::Str(s) => Some(s.as_str()),
75            _ => None,
76        }
77    }
78
79    /// Coerce a value to a declared type for assignment / payload delivery.
80    /// Returns None if the value is not coercible (used to reject bad payloads).
81    pub fn coerce_to(&self, ty: &str) -> Option<Value> {
82        match (self, ty) {
83            (Value::Null, _) => Some(Value::Null),
84            (Value::Bool(b), "bool") => Some(Value::Bool(*b)),
85            (Value::Int(i), "int") => Some(Value::Int(*i)),
86            (Value::Float(f), "float") => Some(Value::Float(*f)),
87            (Value::Str(s), "string") => Some(Value::Str(s.clone())),
88            (Value::List(l), "list") => Some(Value::List(l.clone())),
89            (Value::Map(m), "map") => Some(Value::Map(m.clone())),
90            // int literal used where float is declared
91            (Value::Int(i), "float") => Some(Value::Float(*i as f64)),
92            // numeric strings are NOT coerced (only CLI --payload k=v does string coercion)
93            _ => None,
94        }
95    }
96}
97
98// Canonical JSON/native (de)serialization (SPEC §5.1): a Value is its underlying
99// JSON value, never a tagged wrapper.
100impl serde::Serialize for Value {
101    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
102        self.to_json().serialize(s)
103    }
104}
105
106impl<'de> serde::Deserialize<'de> for Value {
107    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
108        serde_json::Value::deserialize(d).map(|j| Value::from_json(&j))
109    }
110}
111
112impl Value {
113    pub fn from_yaml(v: &serde_yaml::Value) -> Value {
114        use serde_yaml::Value::*;
115        match v {
116            Null => Value::Null,
117            Bool(b) => Value::Bool(*b),
118            Number(n) => {
119                if let Some(i) = n.as_i64() {
120                    Value::Int(i)
121                } else if let Some(u) = n.as_u64() {
122                    Value::Int(u as i64)
123                } else {
124                    Value::Float(n.as_f64().unwrap_or(f64::NAN))
125                }
126            }
127            String(s) => Value::Str(s.clone()),
128            Sequence(seq) => Value::List(seq.iter().map(Value::from_yaml).collect()),
129            Mapping(m) => {
130                let mut map = BTreeMap::new();
131                for (k, v) in m {
132                    let key = match k {
133                        String(s) => s.clone(),
134                        Bool(b) => b.to_string(),
135                        Number(n) => n.to_string(),
136                        Null => "null".to_string(),
137                        _ => continue,
138                    };
139                    map.insert(key, Value::from_yaml(v));
140                }
141                Value::Map(map)
142            }
143            Tagged(t) => Value::from_yaml(&t.value),
144        }
145    }
146
147    pub fn to_json(&self) -> serde_json::Value {
148        match self {
149            Value::Null => serde_json::Value::Null,
150            Value::Bool(b) => serde_json::Value::Bool(*b),
151            Value::Int(i) => serde_json::Value::Number((*i as i64).into()),
152            Value::Float(f) => serde_json::Number::from_f64(*f)
153                .map(serde_json::Value::Number)
154                .unwrap_or(serde_json::Value::Null),
155            Value::Str(s) => serde_json::Value::String(s.clone()),
156            Value::List(l) => serde_json::Value::Array(l.iter().map(Value::to_json).collect()),
157            Value::Map(m) => {
158                let mut o = serde_json::Map::new();
159                for (k, v) in m {
160                    o.insert(k.clone(), v.to_json());
161                }
162                serde_json::Value::Object(o)
163            }
164        }
165    }
166
167    pub fn from_json(v: &serde_json::Value) -> Value {
168        match v {
169            serde_json::Value::Null => Value::Null,
170            serde_json::Value::Bool(b) => Value::Bool(*b),
171            serde_json::Value::Number(n) => {
172                if let Some(i) = n.as_i64() {
173                    Value::Int(i)
174                } else if let Some(u) = n.as_u64() {
175                    Value::Int(u as i64)
176                } else {
177                    Value::Float(n.as_f64().unwrap_or(f64::NAN))
178                }
179            }
180            serde_json::Value::String(s) => Value::Str(s.clone()),
181            serde_json::Value::Array(a) => {
182                Value::List(a.iter().map(Value::from_json).collect())
183            }
184            serde_json::Value::Object(o) => {
185                let mut m = BTreeMap::new();
186                for (k, v) in o {
187                    m.insert(k.clone(), Value::from_json(v));
188                }
189                Value::Map(m)
190            }
191        }
192    }
193}
194
195/// Build a single-field map, common for payloads.
196pub fn map1(k: &str, v: Value) -> Value {
197    let mut m = BTreeMap::new();
198    m.insert(k.to_string(), v);
199    Value::Map(m)
200}