1use std::borrow::Cow;
6
7use crate::output::OutputData;
8use crate::value::Value;
9
10#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19pub struct ExecResult {
20 pub code: i64,
22 pub out: String,
24 pub err: String,
26 pub data: Option<Value>,
28 pub output: Option<OutputData>,
30 pub did_spill: bool,
32 #[serde(skip_serializing_if = "Option::is_none")]
35 pub original_code: Option<i64>,
36}
37
38impl ExecResult {
39 pub fn success(out: impl Into<String>) -> Self {
41 let out = out.into();
42 let data = Self::try_parse_json(&out);
43 Self {
44 code: 0,
45 out,
46 err: String::new(),
47 data,
48 output: None,
49 did_spill: false,
50 original_code: None,
51 }
52 }
53
54 pub fn with_output(output: OutputData) -> Self {
62 let data = if output.is_simple_text() {
63 output.as_text().and_then(Self::try_parse_json)
64 } else {
65 None
66 };
67 Self {
68 code: 0,
69 out: String::new(),
70 err: String::new(),
71 data,
72 output: Some(output),
73 did_spill: false,
74 original_code: None,
75 }
76 }
77
78 pub fn success_data(data: Value) -> Self {
80 let out = value_to_json(&data).to_string();
81 Self {
82 code: 0,
83 out,
84 err: String::new(),
85 data: Some(data),
86 output: None,
87 did_spill: false,
88 original_code: None,
89 }
90 }
91
92 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
101 Self {
102 code: 0,
103 out: out.into(),
104 err: String::new(),
105 data: Some(data),
106 output: None,
107 did_spill: false,
108 original_code: None,
109 }
110 }
111
112 pub fn failure(code: i64, err: impl Into<String>) -> Self {
114 Self {
115 code,
116 out: String::new(),
117 err: err.into(),
118 data: None,
119 output: None,
120 did_spill: false,
121 original_code: None,
122 }
123 }
124
125 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
134 let out = stdout.into();
135 let data = if code == 0 {
136 Self::try_parse_json(&out)
137 } else {
138 None
139 };
140 Self {
141 code,
142 out,
143 err: stderr.into(),
144 data,
145 output: None,
146 did_spill: false,
147 original_code: None,
148 }
149 }
150
151 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
156 let out = text.into();
157 let data = Self::try_parse_json(&out);
158 Self {
159 code: 0,
160 out,
161 err: String::new(),
162 data,
163 output: Some(output),
164 did_spill: false,
165 original_code: None,
166 }
167 }
168
169 pub fn text_out(&self) -> Cow<'_, str> {
175 if !self.out.is_empty() {
176 Cow::Borrowed(&self.out)
177 } else if let Some(ref output) = self.output {
178 Cow::Owned(output.to_canonical_string())
179 } else {
180 Cow::Borrowed("")
181 }
182 }
183
184 pub fn ok(&self) -> bool {
186 self.code == 0
187 }
188
189 pub fn get_field(&self, name: &str) -> Option<Value> {
191 match name {
192 "code" => Some(Value::Int(self.code)),
193 "ok" => Some(Value::Bool(self.ok())),
194 "out" => Some(Value::String(self.text_out().into_owned())),
195 "err" => Some(Value::String(self.err.clone())),
196 "data" => self.data.clone(),
197 _ => None,
198 }
199 }
200
201 pub(crate) fn try_parse_json(s: &str) -> Option<Value> {
203 let trimmed = s.trim();
204 if trimmed.is_empty() {
205 return None;
206 }
207 serde_json::from_str::<serde_json::Value>(trimmed)
208 .ok()
209 .map(json_to_value)
210 }
211}
212
213impl Default for ExecResult {
214 fn default() -> Self {
215 Self::success("")
216 }
217}
218
219pub fn json_to_value(json: serde_json::Value) -> Value {
224 match json {
225 serde_json::Value::Null => Value::Null,
226 serde_json::Value::Bool(b) => Value::Bool(b),
227 serde_json::Value::Number(n) => {
228 if let Some(i) = n.as_i64() {
229 Value::Int(i)
230 } else if let Some(f) = n.as_f64() {
231 Value::Float(f)
232 } else {
233 Value::String(n.to_string())
234 }
235 }
236 serde_json::Value::String(s) => Value::String(s),
237 serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Json(json),
239 }
240}
241
242pub fn value_to_json(value: &Value) -> serde_json::Value {
244 match value {
245 Value::Null => serde_json::Value::Null,
246 Value::Bool(b) => serde_json::Value::Bool(*b),
247 Value::Int(i) => serde_json::Value::Number((*i).into()),
248 Value::Float(f) => {
249 serde_json::Number::from_f64(*f)
250 .map(serde_json::Value::Number)
251 .unwrap_or(serde_json::Value::Null)
252 }
253 Value::String(s) => serde_json::Value::String(s.clone()),
254 Value::Json(json) => json.clone(),
255 Value::Blob(blob) => {
256 let mut map = serde_json::Map::new();
257 map.insert("_type".to_string(), serde_json::Value::String("blob".to_string()));
258 map.insert("id".to_string(), serde_json::Value::String(blob.id.clone()));
259 map.insert("size".to_string(), serde_json::Value::Number(blob.size.into()));
260 map.insert("contentType".to_string(), serde_json::Value::String(blob.content_type.clone()));
261 if let Some(hash) = &blob.hash {
262 let hash_hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect();
263 map.insert("hash".to_string(), serde_json::Value::String(hash_hex));
264 }
265 serde_json::Value::Object(map)
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn success_creates_ok_result() {
276 let result = ExecResult::success("hello world");
277 assert!(result.ok());
278 assert_eq!(result.code, 0);
279 assert_eq!(result.out, "hello world");
280 assert!(result.err.is_empty());
281 }
282
283 #[test]
284 fn failure_creates_non_ok_result() {
285 let result = ExecResult::failure(1, "command not found");
286 assert!(!result.ok());
287 assert_eq!(result.code, 1);
288 assert_eq!(result.err, "command not found");
289 }
290
291 #[test]
292 fn json_stdout_is_parsed() {
293 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
294 assert!(result.data.is_some());
295 let data = result.data.unwrap();
296 assert!(matches!(data, Value::Json(_)));
297 if let Value::Json(json) = data {
298 assert_eq!(json.get("count"), Some(&serde_json::json!(42)));
299 assert_eq!(json.get("items"), Some(&serde_json::json!(["a", "b"])));
300 }
301 }
302
303 #[test]
304 fn non_json_stdout_has_no_data() {
305 let result = ExecResult::success("just plain text");
306 assert!(result.data.is_none());
307 }
308
309 #[test]
310 fn get_field_code() {
311 let result = ExecResult::failure(127, "not found");
312 assert_eq!(result.get_field("code"), Some(Value::Int(127)));
313 }
314
315 #[test]
316 fn get_field_ok() {
317 let success = ExecResult::success("hi");
318 let failure = ExecResult::failure(1, "err");
319 assert_eq!(success.get_field("ok"), Some(Value::Bool(true)));
320 assert_eq!(failure.get_field("ok"), Some(Value::Bool(false)));
321 }
322
323 #[test]
324 fn get_field_out_and_err() {
325 let result = ExecResult::from_output(1, "stdout text", "stderr text");
326 assert_eq!(result.get_field("out"), Some(Value::String("stdout text".into())));
327 assert_eq!(result.get_field("err"), Some(Value::String("stderr text".into())));
328 }
329
330 #[test]
331 fn get_field_data() {
332 let result = ExecResult::success(r#"{"key": "value"}"#);
333 let data = result.get_field("data");
334 assert!(data.is_some());
335 }
336
337 #[test]
338 fn get_field_unknown_returns_none() {
339 let result = ExecResult::success("");
340 assert_eq!(result.get_field("nonexistent"), None);
341 }
342
343 #[test]
344 fn success_data_creates_result_with_value() {
345 let value = Value::String("test data".into());
346 let result = ExecResult::success_data(value.clone());
347 assert!(result.ok());
348 assert_eq!(result.data, Some(value));
349 }
350
351 #[test]
352 fn did_spill_defaults_to_false() {
353 assert!(!ExecResult::success("hi").did_spill);
354 assert!(!ExecResult::failure(1, "err").did_spill);
355 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
356 }
357
358 #[test]
359 fn did_spill_is_serialized() {
360 let mut result = ExecResult::success("hi");
361 result.did_spill = true;
362 let json = serde_json::to_string(&result).unwrap();
363 assert!(json.contains("\"did_spill\":true"));
364 }
365
366 #[test]
367 fn original_code_omitted_when_none() {
368 let result = ExecResult::success("hi");
369 let json = serde_json::to_string(&result).unwrap();
370 assert!(!json.contains("original_code"));
371 }
372
373 #[test]
374 fn original_code_present_when_set() {
375 let mut result = ExecResult::success("hi");
376 result.original_code = Some(0);
377 let json = serde_json::to_string(&result).unwrap();
378 assert!(json.contains("\"original_code\":0"));
379 }
380}