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)]
26#[non_exhaustive]
27pub struct ExecResult {
28 pub code: i64,
30 out: String,
32 pub err: String,
34 pub data: Option<Value>,
37 output: Option<OutputData>,
39 pub did_spill: bool,
44 #[serde(skip_serializing_if = "Option::is_none")]
47 pub original_code: Option<i64>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub content_type: Option<String>,
52 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
57 pub baggage: BTreeMap<String, String>,
58}
59
60impl ExecResult {
61 pub fn success(out: impl Into<String>) -> Self {
63 Self {
64 code: 0,
65 out: out.into(),
66 err: String::new(),
67 data: None,
68 output: None,
69 did_spill: false,
70 original_code: None,
71 content_type: None,
72 baggage: BTreeMap::new(),
73 }
74 }
75
76 pub fn with_output(output: OutputData) -> Self {
81 match output.into_text() {
84 Ok(text) => Self::success(text),
85 Err(output) => Self {
86 code: 0,
87 out: String::new(),
88 err: String::new(),
89 data: None,
90 output: Some(output),
91 did_spill: false,
92 original_code: None,
93 content_type: None,
94 baggage: BTreeMap::new(),
95 },
96 }
97 }
98
99 pub fn success_data(data: Value) -> Self {
101 let out = value_to_json(&data).to_string();
102 Self {
103 code: 0,
104 out,
105 err: String::new(),
106 data: Some(data),
107 output: None,
108 did_spill: false,
109 original_code: None,
110 content_type: None,
111 baggage: BTreeMap::new(),
112 }
113 }
114
115 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
124 Self {
125 code: 0,
126 out: out.into(),
127 err: String::new(),
128 data: Some(data),
129 output: None,
130 did_spill: false,
131 original_code: None,
132 content_type: None,
133 baggage: BTreeMap::new(),
134 }
135 }
136
137 pub fn failure(code: i64, err: impl Into<String>) -> Self {
139 Self {
140 code,
141 out: String::new(),
142 err: err.into(),
143 data: None,
144 output: None,
145 did_spill: false,
146 original_code: None,
147 content_type: None,
148 baggage: BTreeMap::new(),
149 }
150 }
151
152 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
158 Self {
159 code,
160 out: stdout.into(),
161 err: stderr.into(),
162 data: None,
163 output: None,
164 did_spill: false,
165 original_code: None,
166 content_type: None,
167 baggage: BTreeMap::new(),
168 }
169 }
170
171 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
176 Self {
177 code: 0,
178 out: text.into(),
179 err: String::new(),
180 data: None,
181 output: Some(output),
182 did_spill: false,
183 original_code: None,
184 content_type: None,
185 baggage: BTreeMap::new(),
186 }
187 }
188
189 pub fn from_parts(
191 code: i64,
192 out: String,
193 err: String,
194 data: Option<Value>,
195 ) -> Self {
196 Self {
197 code,
198 out,
199 err,
200 data,
201 output: None,
202 did_spill: false,
203 original_code: None,
204 content_type: None,
205 baggage: BTreeMap::new(),
206 }
207 }
208
209 pub fn with_code(mut self, code: i64) -> Self {
211 self.code = code;
212 self
213 }
214
215 pub fn text_out(&self) -> Cow<'_, str> {
223 if !self.out.is_empty() {
224 Cow::Borrowed(&self.out)
225 } else if let Some(ref output) = self.output {
226 Cow::Owned(output.to_canonical_string())
227 } else {
228 Cow::Borrowed("")
229 }
230 }
231
232 pub fn output(&self) -> Option<&OutputData> {
234 self.output.as_ref()
235 }
236
237 pub fn has_output(&self) -> bool {
239 self.output.is_some()
240 }
241
242 pub fn set_out(&mut self, s: String) {
246 self.out = s;
247 }
248
249 pub fn push_out(&mut self, s: &str) {
251 self.out.push_str(s);
252 }
253
254 pub fn clear_out(&mut self) {
256 self.out.clear();
257 }
258
259 pub fn set_output(&mut self, o: Option<OutputData>) {
261 self.output = o;
262 }
263
264 pub fn take_output(&mut self) -> Option<OutputData> {
266 self.output.take()
267 }
268
269 pub fn materialize(&mut self) {
272 if self.out.is_empty() {
273 if let Some(ref output) = self.output {
274 self.out = output.to_canonical_string();
275 }
276 }
277 self.output = None;
278 }
279
280 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
283 if self.out.is_empty() {
284 self.output.take()
285 } else {
286 None
287 }
288 }
289
290 pub fn ok(&self) -> bool {
292 self.code == 0
293 }
294
295 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
297 self.content_type = Some(ct.into());
298 self
299 }
300
301}
302
303pub fn json_to_value(json: serde_json::Value) -> Value {
308 match json {
309 serde_json::Value::Null => Value::Null,
310 serde_json::Value::Bool(b) => Value::Bool(b),
311 serde_json::Value::Number(n) => {
312 if let Some(i) = n.as_i64() {
313 Value::Int(i)
314 } else if let Some(f) = n.as_f64() {
315 Value::Float(f)
316 } else {
317 Value::String(n.to_string())
318 }
319 }
320 serde_json::Value::String(s) => Value::String(s),
321 serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Json(json),
323 }
324}
325
326pub fn value_to_json(value: &Value) -> serde_json::Value {
328 match value {
329 Value::Null => serde_json::Value::Null,
330 Value::Bool(b) => serde_json::Value::Bool(*b),
331 Value::Int(i) => serde_json::Value::Number((*i).into()),
332 Value::Float(f) => {
333 serde_json::Number::from_f64(*f)
337 .map(serde_json::Value::Number)
338 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
339 }
340 Value::String(s) => serde_json::Value::String(s.clone()),
341 Value::Json(json) => json.clone(),
342 Value::Blob(blob) => {
343 let mut map = serde_json::Map::new();
344 map.insert("_type".to_string(), serde_json::Value::String("blob".to_string()));
345 map.insert("id".to_string(), serde_json::Value::String(blob.id.clone()));
346 map.insert("size".to_string(), serde_json::Value::Number(blob.size.into()));
347 map.insert("contentType".to_string(), serde_json::Value::String(blob.content_type.clone()));
348 if let Some(hash) = &blob.hash {
349 let hash_hex: String = hash.iter().map(|b| format!("{:02x}", b)).collect();
350 map.insert("hash".to_string(), serde_json::Value::String(hash_hex));
351 }
352 serde_json::Value::Object(map)
353 }
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn success_creates_ok_result() {
363 let result = ExecResult::success("hello world");
364 assert!(result.ok());
365 assert_eq!(result.code, 0);
366 assert_eq!(result.out, "hello world");
367 assert!(result.err.is_empty());
368 }
369
370 #[test]
371 fn value_to_json_finite_float_is_number() {
372 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
373 }
374
375 #[test]
376 fn value_to_json_non_finite_float_serializes_to_string() {
377 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
379 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
380 assert_eq!(
381 value_to_json(&Value::Float(f64::NEG_INFINITY)),
382 serde_json::json!("-inf")
383 );
384 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
386 }
387
388 #[test]
389 fn failure_creates_non_ok_result() {
390 let result = ExecResult::failure(1, "command not found");
391 assert!(!result.ok());
392 assert_eq!(result.code, 1);
393 assert_eq!(result.err, "command not found");
394 }
395
396 #[test]
397 fn success_does_not_sniff_json_stdout() {
398 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
401 assert!(result.data.is_none());
402 assert_eq!(result.out, r#"{"count": 42, "items": ["a", "b"]}"#);
403 }
404
405 #[test]
406 fn from_output_does_not_sniff_json_stdout() {
407 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
408 assert!(result.data.is_none());
409 assert_eq!(result.out, "[1, 2, 3]");
410 }
411
412 #[test]
413 fn non_json_stdout_has_no_data() {
414 let result = ExecResult::success("just plain text");
415 assert!(result.data.is_none());
416 }
417
418 #[test]
419 fn success_data_creates_result_with_value() {
420 let value = Value::String("test data".into());
421 let result = ExecResult::success_data(value.clone());
422 assert!(result.ok());
423 assert_eq!(result.data, Some(value));
424 }
425
426 #[test]
427 fn did_spill_defaults_to_false() {
428 assert!(!ExecResult::success("hi").did_spill);
429 assert!(!ExecResult::failure(1, "err").did_spill);
430 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
431 }
432
433 #[test]
434 fn did_spill_is_serialized() {
435 let mut result = ExecResult::success("hi");
436 result.did_spill = true;
437 let json = serde_json::to_string(&result).unwrap();
438 assert!(json.contains("\"did_spill\":true"));
439 }
440
441 #[test]
442 fn original_code_omitted_when_none() {
443 let result = ExecResult::success("hi");
444 let json = serde_json::to_string(&result).unwrap();
445 assert!(!json.contains("original_code"));
446 }
447
448 #[test]
449 fn original_code_present_when_set() {
450 let mut result = ExecResult::success("hi");
451 result.original_code = Some(0);
452 let json = serde_json::to_string(&result).unwrap();
453 assert!(json.contains("\"original_code\":0"));
454 }
455
456 #[test]
457 fn default_is_empty_success() {
458 let result = ExecResult::default();
459 assert!(result.ok());
460 assert!(result.out.is_empty());
461 assert!(result.data.is_none());
462 assert!(result.content_type.is_none());
463 assert!(result.baggage.is_empty());
464 }
465
466 #[test]
467 fn from_parts_creates_result() {
468 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
469 assert_eq!(result.code, 42);
470 assert_eq!(result.out, "out");
471 assert_eq!(result.err, "err");
472 assert!(result.data.is_none());
473 assert!(result.output.is_none());
474 }
475
476 #[test]
477 fn with_code_sets_code() {
478 let result = ExecResult::success("hi").with_code(42);
479 assert_eq!(result.code, 42);
480 assert_eq!(result.out, "hi");
481 }
482
483 #[test]
484 fn output_getter() {
485 use crate::output::{OutputData, OutputNode};
486 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
488 let result = ExecResult::with_output(nodes);
489 assert!(result.output().is_some());
490 assert!(result.has_output());
491
492 let text_result = ExecResult::with_output(OutputData::text("test"));
494 assert!(!text_result.has_output());
495 assert_eq!(&*text_result.text_out(), "test");
496
497 let plain = ExecResult::success("text");
498 assert!(plain.output().is_none());
499 assert!(!plain.has_output());
500 }
501
502 #[test]
503 fn set_out_and_push_out_and_clear_out() {
504 let mut result = ExecResult::success("");
505 result.set_out("hello".into());
506 assert_eq!(result.out, "hello");
507 result.push_out(" world");
508 assert_eq!(result.out, "hello world");
509 result.clear_out();
510 assert!(result.out.is_empty());
511 }
512
513 #[test]
514 fn set_output_and_take_output() {
515 use crate::output::OutputData;
516 let mut result = ExecResult::success("");
517 assert!(result.take_output().is_none());
518
519 result.set_output(Some(OutputData::text("data")));
520 assert!(result.has_output());
521
522 let taken = result.take_output();
523 assert!(taken.is_some());
524 assert!(!result.has_output());
525 }
526
527 #[test]
528 fn materialize_populates_out_from_output() {
529 use crate::output::{OutputData, OutputNode};
530 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
532 let mut result = ExecResult::with_output(nodes);
533 assert!(result.out.is_empty());
534 assert!(result.has_output());
535 result.materialize();
536 assert_eq!(result.out, "a\nb");
537 assert!(result.output.is_none());
538 }
539
540 #[test]
541 fn materialize_preserves_existing_out() {
542 use crate::output::OutputData;
543 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
544 result.materialize();
545 assert_eq!(result.out, "custom");
546 }
547
548 #[test]
549 fn take_output_for_stream_when_out_empty() {
550 use crate::output::{OutputData, OutputNode};
551 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
553 let mut result = ExecResult::with_output(nodes);
554 let taken = result.take_output_for_stream();
555 assert!(taken.is_some());
556 assert!(!result.has_output());
557 }
558
559 #[test]
560 fn with_output_simple_text_populates_out_directly() {
561 use crate::output::OutputData;
562 let result = ExecResult::with_output(OutputData::text("hello"));
563 assert!(!result.has_output());
565 assert_eq!(&*result.text_out(), "hello");
566 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
568 assert!(json_result.data.is_none());
569 }
570
571 #[test]
572 fn take_output_for_stream_when_out_populated() {
573 use crate::output::OutputData;
574 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
575 let taken = result.take_output_for_stream();
576 assert!(taken.is_none());
577 assert!(result.has_output()); }
579}