1use crate::output::OutputData;
6use crate::value::Value;
7
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
17pub struct ExecResult {
18 pub code: i64,
20 pub out: String,
22 pub err: String,
24 pub data: Option<Value>,
26 pub output: Option<OutputData>,
28}
29
30impl ExecResult {
31 pub fn success(out: impl Into<String>) -> Self {
33 let out = out.into();
34 let data = Self::try_parse_json(&out);
35 Self {
36 code: 0,
37 out,
38 err: String::new(),
39 data,
40 output: None,
41 }
42 }
43
44 pub fn with_output(output: OutputData) -> Self {
49 let out = output.to_canonical_string();
50 let data = Self::try_parse_json(&out);
51 Self {
52 code: 0,
53 out,
54 err: String::new(),
55 data,
56 output: Some(output),
57 }
58 }
59
60 pub fn success_data(data: Value) -> Self {
62 let out = value_to_json(&data).to_string();
63 Self {
64 code: 0,
65 out,
66 err: String::new(),
67 data: Some(data),
68 output: None,
69 }
70 }
71
72 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
81 Self {
82 code: 0,
83 out: out.into(),
84 err: String::new(),
85 data: Some(data),
86 output: None,
87 }
88 }
89
90 pub fn failure(code: i64, err: impl Into<String>) -> Self {
92 Self {
93 code,
94 out: String::new(),
95 err: err.into(),
96 data: None,
97 output: None,
98 }
99 }
100
101 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
110 let out = stdout.into();
111 let data = if code == 0 {
112 Self::try_parse_json(&out)
113 } else {
114 None
115 };
116 Self {
117 code,
118 out,
119 err: stderr.into(),
120 data,
121 output: None,
122 }
123 }
124
125 pub fn ok(&self) -> bool {
127 self.code == 0
128 }
129
130 pub fn get_field(&self, name: &str) -> Option<Value> {
132 match name {
133 "code" => Some(Value::Int(self.code)),
134 "ok" => Some(Value::Bool(self.ok())),
135 "out" => Some(Value::String(self.out.clone())),
136 "err" => Some(Value::String(self.err.clone())),
137 "data" => self.data.clone(),
138 _ => None,
139 }
140 }
141
142 fn try_parse_json(s: &str) -> Option<Value> {
144 let trimmed = s.trim();
145 if trimmed.is_empty() {
146 return None;
147 }
148 serde_json::from_str::<serde_json::Value>(trimmed)
149 .ok()
150 .map(json_to_value)
151 }
152}
153
154impl Default for ExecResult {
155 fn default() -> Self {
156 Self::success("")
157 }
158}
159
160pub fn json_to_value(json: serde_json::Value) -> Value {
165 match json {
166 serde_json::Value::Null => Value::Null,
167 serde_json::Value::Bool(b) => Value::Bool(b),
168 serde_json::Value::Number(n) => {
169 if let Some(i) = n.as_i64() {
170 Value::Int(i)
171 } else if let Some(f) = n.as_f64() {
172 Value::Float(f)
173 } else {
174 Value::String(n.to_string())
175 }
176 }
177 serde_json::Value::String(s) => Value::String(s),
178 serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Json(json),
180 }
181}
182
183pub fn value_to_json(value: &Value) -> serde_json::Value {
185 match value {
186 Value::Null => serde_json::Value::Null,
187 Value::Bool(b) => serde_json::Value::Bool(*b),
188 Value::Int(i) => serde_json::Value::Number((*i).into()),
189 Value::Float(f) => {
190 serde_json::Number::from_f64(*f)
191 .map(serde_json::Value::Number)
192 .unwrap_or(serde_json::Value::Null)
193 }
194 Value::String(s) => serde_json::Value::String(s.clone()),
195 Value::Json(json) => json.clone(),
196 Value::Blob(blob) => {
197 let mut map = serde_json::Map::new();
198 map.insert("_type".to_string(), serde_json::Value::String("blob".to_string()));
199 map.insert("id".to_string(), serde_json::Value::String(blob.id.clone()));
200 map.insert("size".to_string(), serde_json::Value::Number(blob.size.into()));
201 map.insert("contentType".to_string(), serde_json::Value::String(blob.content_type.clone()));
202 if let Some(hash) = &blob.hash {
203 let hash_hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect();
204 map.insert("hash".to_string(), serde_json::Value::String(hash_hex));
205 }
206 serde_json::Value::Object(map)
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn success_creates_ok_result() {
217 let result = ExecResult::success("hello world");
218 assert!(result.ok());
219 assert_eq!(result.code, 0);
220 assert_eq!(result.out, "hello world");
221 assert!(result.err.is_empty());
222 }
223
224 #[test]
225 fn failure_creates_non_ok_result() {
226 let result = ExecResult::failure(1, "command not found");
227 assert!(!result.ok());
228 assert_eq!(result.code, 1);
229 assert_eq!(result.err, "command not found");
230 }
231
232 #[test]
233 fn json_stdout_is_parsed() {
234 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
235 assert!(result.data.is_some());
236 let data = result.data.unwrap();
237 assert!(matches!(data, Value::Json(_)));
238 if let Value::Json(json) = data {
239 assert_eq!(json.get("count"), Some(&serde_json::json!(42)));
240 assert_eq!(json.get("items"), Some(&serde_json::json!(["a", "b"])));
241 }
242 }
243
244 #[test]
245 fn non_json_stdout_has_no_data() {
246 let result = ExecResult::success("just plain text");
247 assert!(result.data.is_none());
248 }
249
250 #[test]
251 fn get_field_code() {
252 let result = ExecResult::failure(127, "not found");
253 assert_eq!(result.get_field("code"), Some(Value::Int(127)));
254 }
255
256 #[test]
257 fn get_field_ok() {
258 let success = ExecResult::success("hi");
259 let failure = ExecResult::failure(1, "err");
260 assert_eq!(success.get_field("ok"), Some(Value::Bool(true)));
261 assert_eq!(failure.get_field("ok"), Some(Value::Bool(false)));
262 }
263
264 #[test]
265 fn get_field_out_and_err() {
266 let result = ExecResult::from_output(1, "stdout text", "stderr text");
267 assert_eq!(result.get_field("out"), Some(Value::String("stdout text".into())));
268 assert_eq!(result.get_field("err"), Some(Value::String("stderr text".into())));
269 }
270
271 #[test]
272 fn get_field_data() {
273 let result = ExecResult::success(r#"{"key": "value"}"#);
274 let data = result.get_field("data");
275 assert!(data.is_some());
276 }
277
278 #[test]
279 fn get_field_unknown_returns_none() {
280 let result = ExecResult::success("");
281 assert_eq!(result.get_field("nonexistent"), None);
282 }
283
284 #[test]
285 fn success_data_creates_result_with_value() {
286 let value = Value::String("test data".into());
287 let result = ExecResult::success_data(value.clone());
288 assert!(result.ok());
289 assert_eq!(result.data, Some(value));
290 }
291}