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}