1use std::borrow::Cow;
6use std::collections::BTreeMap;
7
8use crate::output::OutputData;
9use crate::value::Value;
10
11#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
26pub struct ExecResult {
27 pub code: i64,
29 out: String,
31 pub err: String,
33 pub data: Option<Value>,
36 output: Option<OutputData>,
38 pub did_spill: bool,
40 #[serde(skip_serializing_if = "Option::is_none")]
43 pub original_code: Option<i64>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub content_type: Option<String>,
48 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
53 pub baggage: BTreeMap<String, String>,
54}
55
56impl ExecResult {
57 pub fn success(out: impl Into<String>) -> Self {
59 Self {
60 code: 0,
61 out: out.into(),
62 err: String::new(),
63 data: None,
64 output: None,
65 did_spill: false,
66 original_code: None,
67 content_type: None,
68 baggage: BTreeMap::new(),
69 }
70 }
71
72 pub fn with_output(output: OutputData) -> Self {
77 match output.into_text() {
80 Ok(text) => Self::success(text),
81 Err(output) => Self {
82 code: 0,
83 out: String::new(),
84 err: String::new(),
85 data: None,
86 output: Some(output),
87 did_spill: false,
88 original_code: None,
89 content_type: None,
90 baggage: BTreeMap::new(),
91 },
92 }
93 }
94
95 pub fn success_data(data: Value) -> Self {
97 let out = value_to_json(&data).to_string();
98 Self {
99 code: 0,
100 out,
101 err: String::new(),
102 data: Some(data),
103 output: None,
104 did_spill: false,
105 original_code: None,
106 content_type: None,
107 baggage: BTreeMap::new(),
108 }
109 }
110
111 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
120 Self {
121 code: 0,
122 out: out.into(),
123 err: String::new(),
124 data: Some(data),
125 output: None,
126 did_spill: false,
127 original_code: None,
128 content_type: None,
129 baggage: BTreeMap::new(),
130 }
131 }
132
133 pub fn failure(code: i64, err: impl Into<String>) -> Self {
135 Self {
136 code,
137 out: String::new(),
138 err: err.into(),
139 data: None,
140 output: None,
141 did_spill: false,
142 original_code: None,
143 content_type: None,
144 baggage: BTreeMap::new(),
145 }
146 }
147
148 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
154 Self {
155 code,
156 out: stdout.into(),
157 err: stderr.into(),
158 data: None,
159 output: None,
160 did_spill: false,
161 original_code: None,
162 content_type: None,
163 baggage: BTreeMap::new(),
164 }
165 }
166
167 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
172 Self {
173 code: 0,
174 out: text.into(),
175 err: String::new(),
176 data: None,
177 output: Some(output),
178 did_spill: false,
179 original_code: None,
180 content_type: None,
181 baggage: BTreeMap::new(),
182 }
183 }
184
185 pub fn from_parts(
187 code: i64,
188 out: String,
189 err: String,
190 data: Option<Value>,
191 ) -> Self {
192 Self {
193 code,
194 out,
195 err,
196 data,
197 output: None,
198 did_spill: false,
199 original_code: None,
200 content_type: None,
201 baggage: BTreeMap::new(),
202 }
203 }
204
205 pub fn with_code(mut self, code: i64) -> Self {
207 self.code = code;
208 self
209 }
210
211 pub fn text_out(&self) -> Cow<'_, str> {
219 if !self.out.is_empty() {
220 Cow::Borrowed(&self.out)
221 } else if let Some(ref output) = self.output {
222 Cow::Owned(output.to_canonical_string())
223 } else {
224 Cow::Borrowed("")
225 }
226 }
227
228 pub fn output(&self) -> Option<&OutputData> {
230 self.output.as_ref()
231 }
232
233 pub fn has_output(&self) -> bool {
235 self.output.is_some()
236 }
237
238 pub fn set_out(&mut self, s: String) {
242 self.out = s;
243 }
244
245 pub fn push_out(&mut self, s: &str) {
247 self.out.push_str(s);
248 }
249
250 pub fn clear_out(&mut self) {
252 self.out.clear();
253 }
254
255 pub fn set_output(&mut self, o: Option<OutputData>) {
257 self.output = o;
258 }
259
260 pub fn take_output(&mut self) -> Option<OutputData> {
262 self.output.take()
263 }
264
265 pub fn materialize(&mut self) {
268 if self.out.is_empty() {
269 if let Some(ref output) = self.output {
270 self.out = output.to_canonical_string();
271 }
272 }
273 self.output = None;
274 }
275
276 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
279 if self.out.is_empty() {
280 self.output.take()
281 } else {
282 None
283 }
284 }
285
286 pub fn ok(&self) -> bool {
288 self.code == 0
289 }
290
291 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
293 self.content_type = Some(ct.into());
294 self
295 }
296
297}
298
299pub fn json_to_value(json: serde_json::Value) -> Value {
304 match json {
305 serde_json::Value::Null => Value::Null,
306 serde_json::Value::Bool(b) => Value::Bool(b),
307 serde_json::Value::Number(n) => {
308 if let Some(i) = n.as_i64() {
309 Value::Int(i)
310 } else if let Some(f) = n.as_f64() {
311 Value::Float(f)
312 } else {
313 Value::String(n.to_string())
314 }
315 }
316 serde_json::Value::String(s) => Value::String(s),
317 serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Json(json),
319 }
320}
321
322pub fn value_to_json(value: &Value) -> serde_json::Value {
324 match value {
325 Value::Null => serde_json::Value::Null,
326 Value::Bool(b) => serde_json::Value::Bool(*b),
327 Value::Int(i) => serde_json::Value::Number((*i).into()),
328 Value::Float(f) => {
329 serde_json::Number::from_f64(*f)
330 .map(serde_json::Value::Number)
331 .unwrap_or(serde_json::Value::Null)
332 }
333 Value::String(s) => serde_json::Value::String(s.clone()),
334 Value::Json(json) => json.clone(),
335 Value::Blob(blob) => {
336 let mut map = serde_json::Map::new();
337 map.insert("_type".to_string(), serde_json::Value::String("blob".to_string()));
338 map.insert("id".to_string(), serde_json::Value::String(blob.id.clone()));
339 map.insert("size".to_string(), serde_json::Value::Number(blob.size.into()));
340 map.insert("contentType".to_string(), serde_json::Value::String(blob.content_type.clone()));
341 if let Some(hash) = &blob.hash {
342 let hash_hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect();
343 map.insert("hash".to_string(), serde_json::Value::String(hash_hex));
344 }
345 serde_json::Value::Object(map)
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn success_creates_ok_result() {
356 let result = ExecResult::success("hello world");
357 assert!(result.ok());
358 assert_eq!(result.code, 0);
359 assert_eq!(result.out, "hello world");
360 assert!(result.err.is_empty());
361 }
362
363 #[test]
364 fn failure_creates_non_ok_result() {
365 let result = ExecResult::failure(1, "command not found");
366 assert!(!result.ok());
367 assert_eq!(result.code, 1);
368 assert_eq!(result.err, "command not found");
369 }
370
371 #[test]
372 fn success_does_not_sniff_json_stdout() {
373 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
376 assert!(result.data.is_none());
377 assert_eq!(result.out, r#"{"count": 42, "items": ["a", "b"]}"#);
378 }
379
380 #[test]
381 fn from_output_does_not_sniff_json_stdout() {
382 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
383 assert!(result.data.is_none());
384 assert_eq!(result.out, "[1, 2, 3]");
385 }
386
387 #[test]
388 fn non_json_stdout_has_no_data() {
389 let result = ExecResult::success("just plain text");
390 assert!(result.data.is_none());
391 }
392
393 #[test]
394 fn success_data_creates_result_with_value() {
395 let value = Value::String("test data".into());
396 let result = ExecResult::success_data(value.clone());
397 assert!(result.ok());
398 assert_eq!(result.data, Some(value));
399 }
400
401 #[test]
402 fn did_spill_defaults_to_false() {
403 assert!(!ExecResult::success("hi").did_spill);
404 assert!(!ExecResult::failure(1, "err").did_spill);
405 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
406 }
407
408 #[test]
409 fn did_spill_is_serialized() {
410 let mut result = ExecResult::success("hi");
411 result.did_spill = true;
412 let json = serde_json::to_string(&result).unwrap();
413 assert!(json.contains("\"did_spill\":true"));
414 }
415
416 #[test]
417 fn original_code_omitted_when_none() {
418 let result = ExecResult::success("hi");
419 let json = serde_json::to_string(&result).unwrap();
420 assert!(!json.contains("original_code"));
421 }
422
423 #[test]
424 fn original_code_present_when_set() {
425 let mut result = ExecResult::success("hi");
426 result.original_code = Some(0);
427 let json = serde_json::to_string(&result).unwrap();
428 assert!(json.contains("\"original_code\":0"));
429 }
430
431 #[test]
432 fn default_is_empty_success() {
433 let result = ExecResult::default();
434 assert!(result.ok());
435 assert!(result.out.is_empty());
436 assert!(result.data.is_none());
437 assert!(result.content_type.is_none());
438 assert!(result.baggage.is_empty());
439 }
440
441 #[test]
442 fn from_parts_creates_result() {
443 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
444 assert_eq!(result.code, 42);
445 assert_eq!(result.out, "out");
446 assert_eq!(result.err, "err");
447 assert!(result.data.is_none());
448 assert!(result.output.is_none());
449 }
450
451 #[test]
452 fn with_code_sets_code() {
453 let result = ExecResult::success("hi").with_code(42);
454 assert_eq!(result.code, 42);
455 assert_eq!(result.out, "hi");
456 }
457
458 #[test]
459 fn output_getter() {
460 use crate::output::{OutputData, OutputNode};
461 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
463 let result = ExecResult::with_output(nodes);
464 assert!(result.output().is_some());
465 assert!(result.has_output());
466
467 let text_result = ExecResult::with_output(OutputData::text("test"));
469 assert!(!text_result.has_output());
470 assert_eq!(&*text_result.text_out(), "test");
471
472 let plain = ExecResult::success("text");
473 assert!(plain.output().is_none());
474 assert!(!plain.has_output());
475 }
476
477 #[test]
478 fn set_out_and_push_out_and_clear_out() {
479 let mut result = ExecResult::success("");
480 result.set_out("hello".into());
481 assert_eq!(result.out, "hello");
482 result.push_out(" world");
483 assert_eq!(result.out, "hello world");
484 result.clear_out();
485 assert!(result.out.is_empty());
486 }
487
488 #[test]
489 fn set_output_and_take_output() {
490 use crate::output::OutputData;
491 let mut result = ExecResult::success("");
492 assert!(result.take_output().is_none());
493
494 result.set_output(Some(OutputData::text("data")));
495 assert!(result.has_output());
496
497 let taken = result.take_output();
498 assert!(taken.is_some());
499 assert!(!result.has_output());
500 }
501
502 #[test]
503 fn materialize_populates_out_from_output() {
504 use crate::output::{OutputData, OutputNode};
505 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
507 let mut result = ExecResult::with_output(nodes);
508 assert!(result.out.is_empty());
509 assert!(result.has_output());
510 result.materialize();
511 assert_eq!(result.out, "a\nb");
512 assert!(result.output.is_none());
513 }
514
515 #[test]
516 fn materialize_preserves_existing_out() {
517 use crate::output::OutputData;
518 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
519 result.materialize();
520 assert_eq!(result.out, "custom");
521 }
522
523 #[test]
524 fn take_output_for_stream_when_out_empty() {
525 use crate::output::{OutputData, OutputNode};
526 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
528 let mut result = ExecResult::with_output(nodes);
529 let taken = result.take_output_for_stream();
530 assert!(taken.is_some());
531 assert!(!result.has_output());
532 }
533
534 #[test]
535 fn with_output_simple_text_populates_out_directly() {
536 use crate::output::OutputData;
537 let result = ExecResult::with_output(OutputData::text("hello"));
538 assert!(!result.has_output());
540 assert_eq!(&*result.text_out(), "hello");
541 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
543 assert!(json_result.data.is_none());
544 }
545
546 #[test]
547 fn take_output_for_stream_when_out_populated() {
548 use crate::output::OutputData;
549 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
550 let taken = result.take_output_for_stream();
551 assert!(taken.is_none());
552 assert!(result.has_output()); }
554}