Skip to main content

atman_runtime/
value.rs

1use std::path::PathBuf;
2
3use crate::error::RuntimeError;
4use crate::hunk::EditProposal;
5use crate::message::Message;
6
7#[derive(Debug, Clone)]
8pub enum Value {
9    Unit,
10    Bool(bool),
11    Int(i64),
12    Float(f64),
13    Str(String),
14    Path(PathBuf),
15    List(Vec<Value>),
16    Struct(Vec<(String, Value)>),
17    Message(Message),
18    EditProposal(Box<EditProposal>),
19    Err(RuntimeError),
20}
21
22impl Value {
23    pub fn is_err(&self) -> bool {
24        matches!(self, Value::Err(_))
25    }
26
27    pub fn kind_name(&self) -> &'static str {
28        match self {
29            Value::Unit => "unit",
30            Value::Bool(_) => "bool",
31            Value::Int(_) => "int",
32            Value::Float(_) => "float",
33            Value::Str(_) => "string",
34            Value::Path(_) => "path",
35            Value::List(_) => "list",
36            Value::Struct(_) => "struct",
37            Value::Message(_) => "message",
38            Value::EditProposal(_) => "edit_proposal",
39            Value::Err(_) => "err",
40        }
41    }
42
43    pub fn field(&self, name: &str) -> Option<&Value> {
44        if let Value::Struct(fields) = self {
45            fields.iter().find(|(k, _)| k == name).map(|(_, v)| v)
46        } else {
47            None
48        }
49    }
50
51    pub fn to_json(&self) -> serde_json::Value {
52        match self {
53            Value::Unit => serde_json::Value::Null,
54            Value::Bool(b) => serde_json::Value::Bool(*b),
55            Value::Int(i) => serde_json::Value::Number((*i).into()),
56            Value::Float(f) => serde_json::Number::from_f64(*f)
57                .map(serde_json::Value::Number)
58                .unwrap_or(serde_json::Value::Null),
59            Value::Str(s) => serde_json::Value::String(s.clone()),
60            Value::Path(p) => serde_json::Value::String(p.display().to_string()),
61            Value::List(items) => {
62                serde_json::Value::Array(items.iter().map(|v| v.to_json()).collect())
63            }
64            Value::Struct(fields) => {
65                let mut m = serde_json::Map::with_capacity(fields.len());
66                for (k, v) in fields {
67                    m.insert(k.clone(), v.to_json());
68                }
69                serde_json::Value::Object(m)
70            }
71            Value::Message(msg) => serde_json::to_value(msg).unwrap_or(serde_json::Value::Null),
72            Value::EditProposal(p) => serde_json::to_value(p).unwrap_or(serde_json::Value::Null),
73            Value::Err(e) => serde_json::json!({ "error": e.to_string() }),
74        }
75    }
76
77    pub fn from_json(v: serde_json::Value) -> Self {
78        match v {
79            serde_json::Value::Null => Value::Unit,
80            serde_json::Value::Bool(b) => Value::Bool(b),
81            serde_json::Value::Number(n) => {
82                if let Some(i) = n.as_i64() {
83                    Value::Int(i)
84                } else if let Some(f) = n.as_f64() {
85                    Value::Float(f)
86                } else {
87                    Value::Str(n.to_string())
88                }
89            }
90            serde_json::Value::String(s) => Value::Str(s),
91            serde_json::Value::Array(items) => {
92                Value::List(items.into_iter().map(Value::from_json).collect())
93            }
94            serde_json::Value::Object(map) => Value::Struct(
95                map.into_iter()
96                    .map(|(k, v)| (k, Value::from_json(v)))
97                    .collect(),
98            ),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn kind_names_are_stable() {
109        assert_eq!(Value::Unit.kind_name(), "unit");
110        assert_eq!(Value::Bool(true).kind_name(), "bool");
111        assert_eq!(Value::Int(1).kind_name(), "int");
112        assert_eq!(Value::Float(1.0).kind_name(), "float");
113        assert_eq!(Value::Str("x".into()).kind_name(), "string");
114        assert_eq!(Value::Path(PathBuf::from("/tmp")).kind_name(), "path");
115        assert_eq!(Value::List(vec![]).kind_name(), "list");
116        assert_eq!(Value::Struct(vec![]).kind_name(), "struct");
117        assert_eq!(
118            Value::Err(RuntimeError::UndefinedVar("x".into())).kind_name(),
119            "err",
120        );
121    }
122
123    #[test]
124    fn is_err_only_true_for_err_variant() {
125        assert!(!Value::Unit.is_err());
126        assert!(!Value::Bool(false).is_err());
127        assert!(Value::Err(RuntimeError::Cancelled("stop".into())).is_err());
128    }
129
130    #[test]
131    fn struct_field_lookup_returns_by_first_match() {
132        let v = Value::Struct(vec![
133            ("severity".into(), Value::Str("critical".into())),
134            ("count".into(), Value::Int(3)),
135        ]);
136        assert!(matches!(v.field("severity"), Some(Value::Str(s)) if s == "critical"));
137        assert!(matches!(v.field("count"), Some(Value::Int(3))));
138        assert!(v.field("missing").is_none());
139    }
140
141    #[test]
142    fn struct_field_preserves_declaration_order() {
143        let v = Value::Struct(vec![
144            ("a".into(), Value::Int(1)),
145            ("b".into(), Value::Int(2)),
146        ]);
147        if let Value::Struct(fields) = &v {
148            assert_eq!(fields[0].0, "a");
149            assert_eq!(fields[1].0, "b");
150        } else {
151            panic!("expected struct");
152        }
153    }
154
155    #[test]
156    fn runtime_error_display_is_stable() {
157        let msg = RuntimeError::TypeMismatch {
158            expected: "int".into(),
159            actual: "string".into(),
160        }
161        .to_string();
162        assert_eq!(msg, "type mismatch: expected int, got string");
163    }
164}