1use std::borrow::Cow;
6use std::collections::BTreeMap;
7
8use crate::output::OutputData;
9use crate::value::Value;
10
11#[derive(Debug, Clone, PartialEq)]
19pub enum OutputPayload {
20 Text(String),
22 Bytes(Vec<u8>),
25}
26
27impl Default for OutputPayload {
28 fn default() -> Self {
29 OutputPayload::Text(String::new())
30 }
31}
32
33impl serde::Serialize for OutputPayload {
34 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
35 match self {
36 OutputPayload::Text(t) => serializer.serialize_str(t),
38 OutputPayload::Bytes(b) => crate::bytes::bytes_to_envelope(b).serialize(serializer),
39 }
40 }
41}
42
43impl<'de> serde::Deserialize<'de> for OutputPayload {
44 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45 let v = serde_json::Value::deserialize(deserializer)?;
46 match v {
47 serde_json::Value::String(s) => Ok(OutputPayload::Text(s)),
48 other => match crate::bytes::envelope_to_bytes(&other) {
49 Some(b) => Ok(OutputPayload::Bytes(b)),
50 None => Err(serde::de::Error::custom(
51 "ExecResult.out: expected a string or a base64 bytes envelope",
52 )),
53 },
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct BinaryNotText {
61 pub len: usize,
63}
64
65impl std::fmt::Display for BinaryNotText {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "output is binary ({} bytes), not text — pipe through base64/xxd or redirect to a file",
70 self.len
71 )
72 }
73}
74
75impl std::error::Error for BinaryNotText {}
76
77#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
92#[non_exhaustive]
93pub struct ExecResult {
94 pub code: i64,
96 out: OutputPayload,
98 pub err: String,
107 pub data: Option<Value>,
110 output: Option<Box<OutputData>>,
120 pub did_spill: bool,
128 #[serde(skip_serializing_if = "Option::is_none")]
131 pub original_code: Option<i64>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub content_type: Option<String>,
136 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
141 pub baggage: BTreeMap<String, String>,
142}
143
144impl ExecResult {
145 pub fn terminate_diagnostic(message: impl Into<String>) -> String {
153 let mut message = message.into();
154 if message.is_empty() {
155 return message;
156 }
157 let terminated = message.trim_end_matches('\n').len();
158 message.truncate(terminated);
159 message.push('\n');
160 message
161 }
162
163 pub fn success(out: impl Into<String>) -> Self {
165 Self {
166 code: 0,
167 out: OutputPayload::Text(out.into()),
168 err: String::new(),
169 data: None,
170 output: None,
171 did_spill: false,
172 original_code: None,
173 content_type: None,
174 baggage: BTreeMap::new(),
175 }
176 }
177
178 pub fn with_output(output: OutputData) -> Self {
183 match output.into_text() {
186 Ok(text) => Self::success(text),
187 Err(output) => Self {
188 code: 0,
189 out: OutputPayload::Text(String::new()),
190 err: String::new(),
191 data: None,
192 output: Some(Box::new(output)),
193 did_spill: false,
194 original_code: None,
195 content_type: None,
196 baggage: BTreeMap::new(),
197 },
198 }
199 }
200
201 pub fn success_bytes(bytes: Vec<u8>) -> Self {
203 let mut r = Self::success("");
204 r.out = OutputPayload::Bytes(bytes);
205 r
206 }
207
208 pub fn success_text_or_bytes(bytes: Vec<u8>) -> Self {
214 match String::from_utf8(bytes) {
215 Ok(text) => Self::success(text),
216 Err(e) => Self::success_bytes(e.into_bytes()),
217 }
218 }
219
220 pub fn success_data(data: Value) -> Self {
222 let out = value_to_json(&data).to_string();
223 Self {
224 code: 0,
225 out: OutputPayload::Text(out),
226 err: String::new(),
227 data: Some(data),
228 output: None,
229 did_spill: false,
230 original_code: None,
231 content_type: None,
232 baggage: BTreeMap::new(),
233 }
234 }
235
236 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
245 Self {
246 code: 0,
247 out: OutputPayload::Text(out.into()),
248 err: String::new(),
249 data: Some(data),
250 output: None,
251 did_spill: false,
252 original_code: None,
253 content_type: None,
254 baggage: BTreeMap::new(),
255 }
256 }
257
258 pub fn failure(code: i64, err: impl Into<String>) -> Self {
263 Self {
264 code,
265 out: OutputPayload::Text(String::new()),
266 err: Self::terminate_diagnostic(err),
267 data: None,
268 output: None,
269 did_spill: false,
270 original_code: None,
271 content_type: None,
272 baggage: BTreeMap::new(),
273 }
274 }
275
276 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
282 Self {
283 code,
284 out: OutputPayload::Text(stdout.into()),
285 err: stderr.into(),
286 data: None,
287 output: None,
288 did_spill: false,
289 original_code: None,
290 content_type: None,
291 baggage: BTreeMap::new(),
292 }
293 }
294
295 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
300 Self {
301 code: 0,
302 out: OutputPayload::Text(text.into()),
303 err: String::new(),
304 data: None,
305 output: Some(Box::new(output)),
306 did_spill: false,
307 original_code: None,
308 content_type: None,
309 baggage: BTreeMap::new(),
310 }
311 }
312
313 pub fn from_parts(
315 code: i64,
316 out: String,
317 err: String,
318 data: Option<Value>,
319 ) -> Self {
320 Self {
321 code,
322 out: OutputPayload::Text(out),
323 err: Self::terminate_diagnostic(err),
324 data,
325 output: None,
326 did_spill: false,
327 original_code: None,
328 content_type: None,
329 baggage: BTreeMap::new(),
330 }
331 }
332
333 pub fn with_code(mut self, code: i64) -> Self {
335 self.code = code;
336 self
337 }
338
339 pub fn text_out(&self) -> Cow<'_, str> {
354 match &self.out {
355 OutputPayload::Text(s) if !s.is_empty() => Cow::Borrowed(s),
356 OutputPayload::Bytes(b) => match std::str::from_utf8(b) {
357 Ok(s) => Cow::Borrowed(s),
358 Err(_) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
359 },
360 _ => match self.output {
362 Some(ref output) => Cow::Owned(output.to_canonical_string()),
363 None => Cow::Borrowed(""),
364 },
365 }
366 }
367
368 pub fn try_text_out(&self) -> Result<Cow<'_, str>, BinaryNotText> {
373 match &self.out {
374 OutputPayload::Bytes(b) => std::str::from_utf8(b)
375 .map(Cow::Borrowed)
376 .map_err(|_| BinaryNotText { len: b.len() }),
377 _ => Ok(self.text_out()),
378 }
379 }
380
381 pub fn out_bytes(&self) -> Option<&[u8]> {
383 match &self.out {
384 OutputPayload::Bytes(b) => Some(b),
385 OutputPayload::Text(_) => None,
386 }
387 }
388
389 pub fn is_bytes(&self) -> bool {
391 matches!(self.out, OutputPayload::Bytes(_))
392 }
393
394 pub fn output(&self) -> Option<&OutputData> {
396 self.output.as_deref()
397 }
398
399 pub fn has_output(&self) -> bool {
401 self.output.is_some()
402 }
403
404 pub fn set_out(&mut self, s: String) {
408 self.out = OutputPayload::Text(s);
409 }
410
411 pub fn set_out_bytes(&mut self, b: Vec<u8>) {
413 self.out = OutputPayload::Bytes(b);
414 }
415
416 pub fn push_out(&mut self, s: &str) {
418 match &mut self.out {
419 OutputPayload::Text(t) => t.push_str(s),
420 OutputPayload::Bytes(b) => b.extend_from_slice(s.as_bytes()),
421 }
422 }
423
424 pub fn clear_out(&mut self) {
426 self.out = OutputPayload::Text(String::new());
427 }
428
429 pub fn clear_stdout(&mut self) {
442 self.out = OutputPayload::Text(String::new());
443 self.output = None;
444 self.data = None;
445 }
446
447 pub fn set_output(&mut self, o: Option<OutputData>) {
449 self.output = o.map(Box::new);
450 }
451
452 pub fn take_output(&mut self) -> Option<OutputData> {
454 self.output.take().map(|o| *o)
455 }
456
457 pub fn materialize(&mut self) {
460 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
461 if let Some(ref output) = self.output {
462 self.out = OutputPayload::Text(output.to_canonical_string());
463 }
464 }
465 self.output = None;
466 }
467
468 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
471 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
472 self.output.take().map(|o| *o)
473 } else {
474 None
475 }
476 }
477
478 pub fn ok(&self) -> bool {
480 self.code == 0
481 }
482
483 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
485 self.content_type = Some(ct.into());
486 self
487 }
488
489}
490
491pub fn json_to_value(json: serde_json::Value) -> Value {
496 match json {
497 serde_json::Value::Null => Value::Null,
498 serde_json::Value::Bool(b) => Value::Bool(b),
499 serde_json::Value::Number(n) => {
500 if let Some(i) = n.as_i64() {
501 Value::Int(i)
502 } else if let Some(f) = n.as_f64() {
503 Value::Float(f)
504 } else {
505 Value::String(n.to_string())
506 }
507 }
508 serde_json::Value::String(s) => Value::String(s),
509 serde_json::Value::Object(_) => match crate::bytes::envelope_to_bytes(&json) {
512 Some(bytes) => Value::Bytes(bytes),
513 None => Value::Json(json),
514 },
515 serde_json::Value::Array(_) => Value::Json(json),
516 }
517}
518
519pub fn json_to_value_no_envelope(json: serde_json::Value) -> Value {
529 match json {
530 serde_json::Value::Null => Value::Null,
531 serde_json::Value::Bool(b) => Value::Bool(b),
532 serde_json::Value::Number(n) => {
533 if let Some(i) = n.as_i64() {
534 Value::Int(i)
535 } else if let Some(f) = n.as_f64() {
536 Value::Float(f)
537 } else {
538 Value::String(n.to_string())
539 }
540 }
541 serde_json::Value::String(s) => Value::String(s),
542 serde_json::Value::Object(_) | serde_json::Value::Array(_) => Value::Json(json),
545 }
546}
547
548pub fn value_to_json(value: &Value) -> serde_json::Value {
550 match value {
551 Value::Null => serde_json::Value::Null,
552 Value::Bool(b) => serde_json::Value::Bool(*b),
553 Value::Int(i) => serde_json::Value::Number((*i).into()),
554 Value::Float(f) => {
555 serde_json::Number::from_f64(*f)
559 .map(serde_json::Value::Number)
560 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
561 }
562 Value::String(s) => serde_json::Value::String(s.clone()),
563 Value::Json(json) => json.clone(),
564 Value::Bytes(data) => crate::bytes::bytes_to_envelope(data),
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn success_creates_ok_result() {
574 let result = ExecResult::success("hello world");
575 assert!(result.ok());
576 assert_eq!(result.code, 0);
577 assert_eq!(&*result.text_out(),"hello world");
578 assert!(result.err.is_empty());
579 }
580
581 #[test]
582 fn value_to_json_finite_float_is_number() {
583 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
584 }
585
586 #[test]
587 fn value_to_json_non_finite_float_serializes_to_string() {
588 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
590 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
591 assert_eq!(
592 value_to_json(&Value::Float(f64::NEG_INFINITY)),
593 serde_json::json!("-inf")
594 );
595 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
597 }
598
599 #[test]
600 fn failure_creates_non_ok_result() {
601 let result = ExecResult::failure(1, "command not found");
602 assert!(!result.ok());
603 assert_eq!(result.code, 1);
604 assert_eq!(result.err, "command not found\n");
605 }
606
607 #[test]
608 fn failure_ends_exactly_one_newline() {
609 assert_eq!(ExecResult::failure(1, "msg").err, "msg\n");
610 assert_eq!(ExecResult::failure(1, "msg\n").err, "msg\n");
611 assert_eq!(ExecResult::failure(1, "msg\n\n\n").err, "msg\n");
612 }
613
614 #[test]
615 fn failure_empty_message_stays_empty() {
616 assert_eq!(ExecResult::failure(1, "").err, "");
619 }
620
621 #[test]
622 fn terminate_diagnostic_keeps_multiline_interior_newlines() {
623 let msg = "wc: a: not found\nwc: b: not found";
624 assert_eq!(
625 ExecResult::terminate_diagnostic(msg),
626 "wc: a: not found\nwc: b: not found\n"
627 );
628 }
629
630 #[test]
631 fn from_parts_ends_the_diagnostic_line() {
632 assert_eq!(ExecResult::from_parts(1, String::new(), "boom".into(), None).err, "boom\n");
633 assert_eq!(ExecResult::from_parts(1, String::new(), String::new(), None).err, "");
634 }
635
636 #[test]
637 fn from_output_keeps_external_stderr_byte_faithful() {
638 let result = ExecResult::from_output(1, "", "died mid-line");
641 assert_eq!(result.err, "died mid-line");
642 }
643
644 #[test]
645 fn success_does_not_sniff_json_stdout() {
646 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
649 assert!(result.data.is_none());
650 assert_eq!(&*result.text_out(),r#"{"count": 42, "items": ["a", "b"]}"#);
651 }
652
653 #[test]
654 fn from_output_does_not_sniff_json_stdout() {
655 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
656 assert!(result.data.is_none());
657 assert_eq!(&*result.text_out(),"[1, 2, 3]");
658 }
659
660 #[test]
661 fn non_json_stdout_has_no_data() {
662 let result = ExecResult::success("just plain text");
663 assert!(result.data.is_none());
664 }
665
666 #[test]
667 fn success_data_creates_result_with_value() {
668 let value = Value::String("test data".into());
669 let result = ExecResult::success_data(value.clone());
670 assert!(result.ok());
671 assert_eq!(result.data, Some(value));
672 }
673
674 #[test]
675 fn did_spill_defaults_to_false() {
676 assert!(!ExecResult::success("hi").did_spill);
677 assert!(!ExecResult::failure(1, "err").did_spill);
678 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
679 }
680
681 #[test]
682 fn did_spill_is_serialized() {
683 let mut result = ExecResult::success("hi");
684 result.did_spill = true;
685 let json = serde_json::to_string(&result).unwrap();
686 assert!(json.contains("\"did_spill\":true"));
687 }
688
689 #[test]
690 fn original_code_omitted_when_none() {
691 let result = ExecResult::success("hi");
692 let json = serde_json::to_string(&result).unwrap();
693 assert!(!json.contains("original_code"));
694 }
695
696 #[test]
697 fn original_code_present_when_set() {
698 let mut result = ExecResult::success("hi");
699 result.original_code = Some(0);
700 let json = serde_json::to_string(&result).unwrap();
701 assert!(json.contains("\"original_code\":0"));
702 }
703
704 #[test]
705 fn default_is_empty_success() {
706 let result = ExecResult::default();
707 assert!(result.ok());
708 assert!(result.text_out().is_empty());
709 assert!(result.data.is_none());
710 assert!(result.content_type.is_none());
711 assert!(result.baggage.is_empty());
712 }
713
714 #[test]
715 fn from_parts_creates_result() {
716 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
717 assert_eq!(result.code, 42);
718 assert_eq!(&*result.text_out(),"out");
719 assert_eq!(result.err, "err\n");
720 assert!(result.data.is_none());
721 assert!(result.output.is_none());
722 }
723
724 #[test]
725 fn with_code_sets_code() {
726 let result = ExecResult::success("hi").with_code(42);
727 assert_eq!(result.code, 42);
728 assert_eq!(&*result.text_out(),"hi");
729 }
730
731 #[test]
732 fn output_getter() {
733 use crate::output::{OutputData, OutputNode};
734 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
736 let result = ExecResult::with_output(nodes);
737 assert!(result.output().is_some());
738 assert!(result.has_output());
739
740 let text_result = ExecResult::with_output(OutputData::text("test"));
742 assert!(!text_result.has_output());
743 assert_eq!(&*text_result.text_out(), "test");
744
745 let plain = ExecResult::success("text");
746 assert!(plain.output().is_none());
747 assert!(!plain.has_output());
748 }
749
750 #[test]
751 fn set_out_and_push_out_and_clear_out() {
752 let mut result = ExecResult::success("");
753 result.set_out("hello".into());
754 assert_eq!(&*result.text_out(),"hello");
755 result.push_out(" world");
756 assert_eq!(&*result.text_out(),"hello world");
757 result.clear_out();
758 assert!(result.text_out().is_empty());
759 }
760
761 #[test]
762 fn set_output_and_take_output() {
763 use crate::output::OutputData;
764 let mut result = ExecResult::success("");
765 assert!(result.take_output().is_none());
766
767 result.set_output(Some(OutputData::text("data")));
768 assert!(result.has_output());
769
770 let taken = result.take_output();
771 assert!(taken.is_some());
772 assert!(!result.has_output());
773 }
774
775 #[test]
776 fn materialize_populates_out_from_output() {
777 use crate::output::{OutputData, OutputNode};
778 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
780 let mut result = ExecResult::with_output(nodes);
781 assert!(matches!(&result.out, OutputPayload::Text(s) if s.is_empty()));
784 assert!(result.has_output());
785 result.materialize();
786 assert_eq!(&*result.text_out(),"a\nb");
787 assert!(result.output.is_none());
788 }
789
790 #[test]
791 fn value_bytes_round_trips_through_envelope() {
792 let v = Value::Bytes(vec![0u8, 1, 2, 255, 128]);
793 let json = value_to_json(&v);
794 assert_eq!(json["_type"], "bytes");
795 assert_eq!(json["len"], 5);
796 assert_eq!(json_to_value(json), v);
798 let obj = serde_json::json!({"name": "amy"});
800 assert!(matches!(json_to_value(obj), Value::Json(_)));
801 }
802
803 #[test]
804 fn no_envelope_never_decodes_bytes() {
805 let envelope = crate::bytes::bytes_to_envelope(&[1u8, 2, 3]);
809 assert!(matches!(json_to_value(envelope.clone()), Value::Bytes(_)));
811 assert!(matches!(
813 json_to_value_no_envelope(envelope),
814 Value::Json(serde_json::Value::Object(_))
815 ));
816 }
817
818 #[test]
819 fn no_envelope_shares_unwrap_law_for_scalars() {
820 assert_eq!(json_to_value_no_envelope(serde_json::json!(42)), Value::Int(42));
822 assert_eq!(json_to_value_no_envelope(serde_json::json!(1.5)), Value::Float(1.5));
823 assert_eq!(json_to_value_no_envelope(serde_json::json!(true)), Value::Bool(true));
824 assert_eq!(json_to_value_no_envelope(serde_json::json!("hi")), Value::String("hi".into()));
825 assert_eq!(json_to_value_no_envelope(serde_json::json!(null)), Value::Null);
826 assert!(matches!(
827 json_to_value_no_envelope(serde_json::json!([1, 2])),
828 Value::Json(serde_json::Value::Array(_))
829 ));
830 }
831
832 #[test]
833 fn output_payload_text_serializes_as_bare_string() {
834 let r = ExecResult::success("hello");
837 let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap();
838 assert_eq!(json["out"], "hello");
839 let back: ExecResult = serde_json::from_value(json).unwrap();
841 assert_eq!(&*back.text_out(), "hello");
842 assert!(!back.is_bytes());
843 }
844
845 #[test]
846 fn success_bytes_carries_binary_and_round_trips() {
847 let r = ExecResult::success_bytes(vec![0u8, 159, 146, 150]); assert!(r.is_bytes());
849 assert_eq!(r.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
850 assert!(r.try_text_out().is_err());
852 assert!(r.text_out().contains('\u{fffd}'));
854 let json: serde_json::Value = serde_json::to_value(&r).unwrap();
856 assert_eq!(json["out"]["_type"], "bytes");
857 let back: ExecResult = serde_json::from_value(json).unwrap();
858 assert_eq!(back.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
859 }
860
861 #[test]
862 fn valid_utf8_bytes_coerce_to_text() {
863 let r = ExecResult::success_bytes(b"plain text".to_vec());
864 assert!(r.is_bytes());
865 assert_eq!(r.try_text_out().unwrap(), "plain text");
866 assert_eq!(&*r.text_out(), "plain text");
867 }
868
869 #[test]
870 fn materialize_preserves_existing_out() {
871 use crate::output::OutputData;
872 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
873 result.materialize();
874 assert_eq!(&*result.text_out(),"custom");
875 }
876
877 #[test]
878 fn take_output_for_stream_when_out_empty() {
879 use crate::output::{OutputData, OutputNode};
880 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
882 let mut result = ExecResult::with_output(nodes);
883 let taken = result.take_output_for_stream();
884 assert!(taken.is_some());
885 assert!(!result.has_output());
886 }
887
888 #[test]
889 fn with_output_simple_text_populates_out_directly() {
890 use crate::output::OutputData;
891 let result = ExecResult::with_output(OutputData::text("hello"));
892 assert!(!result.has_output());
894 assert_eq!(&*result.text_out(), "hello");
895 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
897 assert!(json_result.data.is_none());
898 }
899
900
901 #[test]
902 fn clear_stdout_drops_data() {
903 let mut result = ExecResult::success_data(Value::Json(serde_json::json!([1, 2, 3])));
905 result.clear_stdout();
906 assert!(result.data.is_none(), "data-plane .data must clear");
907 }
908
909 #[test]
910 fn take_output_for_stream_when_out_populated() {
911 use crate::output::OutputData;
912 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
913 let taken = result.take_output_for_stream();
914 assert!(taken.is_none());
915 assert!(result.has_output()); }
917}