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 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102 pub data_is_value: bool,
103 out: OutputPayload,
105 pub err: String,
114 pub data: Option<Value>,
117 output: Option<Box<OutputData>>,
127 pub did_spill: bool,
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub original_code: Option<i64>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub content_type: Option<String>,
143 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
148 pub baggage: BTreeMap<String, String>,
149}
150
151impl ExecResult {
152 pub fn terminate_diagnostic(message: impl Into<String>) -> String {
160 let mut message = message.into();
161 if message.is_empty() {
162 return message;
163 }
164 let terminated = message.trim_end_matches('\n').len();
165 message.truncate(terminated);
166 message.push('\n');
167 message
168 }
169
170 pub fn success(out: impl Into<String>) -> Self {
172 Self {
173 code: 0,
174 data_is_value: false,
175 out: OutputPayload::Text(out.into()),
176 err: String::new(),
177 data: None,
178 output: None,
179 did_spill: false,
180 original_code: None,
181 content_type: None,
182 baggage: BTreeMap::new(),
183 }
184 }
185
186 pub fn with_output(output: OutputData) -> Self {
191 match output.into_text() {
194 Ok(text) => Self::success(text),
195 Err(output) => Self {
196 code: 0,
197 data_is_value: false,
198 out: OutputPayload::Text(String::new()),
199 err: String::new(),
200 data: None,
201 output: Some(Box::new(output)),
202 did_spill: false,
203 original_code: None,
204 content_type: None,
205 baggage: BTreeMap::new(),
206 },
207 }
208 }
209
210 pub fn success_bytes(bytes: Vec<u8>) -> Self {
212 let mut r = Self::success("");
213 r.out = OutputPayload::Bytes(bytes);
214 r
215 }
216
217 pub fn success_text_or_bytes(bytes: Vec<u8>) -> Self {
223 match String::from_utf8(bytes) {
224 Ok(text) => Self::success(text),
225 Err(e) => Self::success_bytes(e.into_bytes()),
226 }
227 }
228
229 pub fn success_data(data: Value) -> Self {
231 let out = value_to_json(&data).to_string();
232 Self {
233 code: 0,
234 data_is_value: false,
235 out: OutputPayload::Text(out),
236 err: String::new(),
237 data: Some(data),
238 output: None,
239 did_spill: false,
240 original_code: None,
241 content_type: None,
242 baggage: BTreeMap::new(),
243 }
244 }
245
246 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
255 Self {
256 code: 0,
257 data_is_value: false,
258 out: OutputPayload::Text(out.into()),
259 err: String::new(),
260 data: Some(data),
261 output: None,
262 did_spill: false,
263 original_code: None,
264 content_type: None,
265 baggage: BTreeMap::new(),
266 }
267 }
268
269 pub fn failure(code: i64, err: impl Into<String>) -> Self {
274 Self {
275 code,
276 data_is_value: false,
277 out: OutputPayload::Text(String::new()),
278 err: Self::terminate_diagnostic(err),
279 data: None,
280 output: None,
281 did_spill: false,
282 original_code: None,
283 content_type: None,
284 baggage: BTreeMap::new(),
285 }
286 }
287
288 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
294 Self {
295 data_is_value: false,
296 code,
297 out: OutputPayload::Text(stdout.into()),
298 err: stderr.into(),
299 data: None,
300 output: None,
301 did_spill: false,
302 original_code: None,
303 content_type: None,
304 baggage: BTreeMap::new(),
305 }
306 }
307
308 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
313 Self {
314 code: 0,
315 data_is_value: false,
316 out: OutputPayload::Text(text.into()),
317 err: String::new(),
318 data: None,
319 output: Some(Box::new(output)),
320 did_spill: false,
321 original_code: None,
322 content_type: None,
323 baggage: BTreeMap::new(),
324 }
325 }
326
327 pub fn from_parts(
329 code: i64,
330 out: String,
331 err: String,
332 data: Option<Value>,
333 ) -> Self {
334 Self {
335 data_is_value: false,
336 code,
337 out: OutputPayload::Text(out),
338 err: Self::terminate_diagnostic(err),
339 data,
340 output: None,
341 did_spill: false,
342 original_code: None,
343 content_type: None,
344 baggage: BTreeMap::new(),
345 }
346 }
347
348 pub fn with_code(mut self, code: i64) -> Self {
350 self.code = code;
351 self
352 }
353
354 pub fn text_out(&self) -> Cow<'_, str> {
369 match &self.out {
370 OutputPayload::Text(s) if !s.is_empty() => Cow::Borrowed(s),
371 OutputPayload::Bytes(b) => match std::str::from_utf8(b) {
372 Ok(s) => Cow::Borrowed(s),
373 Err(_) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
374 },
375 _ => match self.output {
377 Some(ref output) => Cow::Owned(output.to_canonical_string()),
378 None => Cow::Borrowed(""),
379 },
380 }
381 }
382
383 pub fn try_text_out(&self) -> Result<Cow<'_, str>, BinaryNotText> {
388 match &self.out {
389 OutputPayload::Bytes(b) => std::str::from_utf8(b)
390 .map(Cow::Borrowed)
391 .map_err(|_| BinaryNotText { len: b.len() }),
392 _ => Ok(self.text_out()),
393 }
394 }
395
396 pub fn out_bytes(&self) -> Option<&[u8]> {
398 match &self.out {
399 OutputPayload::Bytes(b) => Some(b),
400 OutputPayload::Text(_) => None,
401 }
402 }
403
404 pub fn is_bytes(&self) -> bool {
406 matches!(self.out, OutputPayload::Bytes(_))
407 }
408
409 pub fn output(&self) -> Option<&OutputData> {
411 self.output.as_deref()
412 }
413
414 pub fn has_output(&self) -> bool {
416 self.output.is_some()
417 }
418
419 pub fn set_out(&mut self, s: String) {
423 self.out = OutputPayload::Text(s);
424 }
425
426 pub fn set_out_bytes(&mut self, b: Vec<u8>) {
428 self.out = OutputPayload::Bytes(b);
429 }
430
431 pub fn push_out(&mut self, s: &str) {
433 match &mut self.out {
434 OutputPayload::Text(t) => t.push_str(s),
435 OutputPayload::Bytes(b) => b.extend_from_slice(s.as_bytes()),
436 }
437 }
438
439 pub fn clear_out(&mut self) {
441 self.out = OutputPayload::Text(String::new());
442 }
443
444 pub fn clear_stdout(&mut self) {
457 self.out = OutputPayload::Text(String::new());
458 self.output = None;
459 self.data = None;
460 }
461
462 pub fn set_output(&mut self, o: Option<OutputData>) {
464 self.output = o.map(Box::new);
465 }
466
467 pub fn take_output(&mut self) -> Option<OutputData> {
469 self.output.take().map(|o| *o)
470 }
471
472 pub fn materialize(&mut self) {
475 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
476 if let Some(ref output) = self.output {
477 self.out = OutputPayload::Text(output.to_canonical_string());
478 }
479 }
480 self.output = None;
481 }
482
483 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
486 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
487 self.output.take().map(|o| *o)
488 } else {
489 None
490 }
491 }
492
493 pub fn ok(&self) -> bool {
495 self.code == 0
496 }
497
498 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
500 self.content_type = Some(ct.into());
501 self
502 }
503
504}
505
506pub fn json_to_value(json: serde_json::Value) -> Value {
511 match json {
512 serde_json::Value::Null => Value::Null,
513 serde_json::Value::Bool(b) => Value::Bool(b),
514 serde_json::Value::Number(n) => {
515 if let Some(i) = n.as_i64() {
516 Value::Int(i)
517 } else if let Some(f) = n.as_f64() {
518 Value::Float(f)
519 } else {
520 Value::String(n.to_string())
521 }
522 }
523 serde_json::Value::String(s) => Value::String(s),
524 serde_json::Value::Object(_) => match crate::bytes::envelope_to_bytes(&json) {
527 Some(bytes) => Value::Bytes(bytes),
528 None => Value::Json(json),
529 },
530 serde_json::Value::Array(_) => Value::Json(json),
531 }
532}
533
534pub fn json_to_value_no_envelope(json: serde_json::Value) -> Value {
544 match json {
545 serde_json::Value::Null => Value::Null,
546 serde_json::Value::Bool(b) => Value::Bool(b),
547 serde_json::Value::Number(n) => {
548 if let Some(i) = n.as_i64() {
549 Value::Int(i)
550 } else if let Some(f) = n.as_f64() {
551 Value::Float(f)
552 } else {
553 Value::String(n.to_string())
554 }
555 }
556 serde_json::Value::String(s) => Value::String(s),
557 serde_json::Value::Object(_) | serde_json::Value::Array(_) => Value::Json(json),
560 }
561}
562
563pub fn value_to_json(value: &Value) -> serde_json::Value {
565 match value {
566 Value::Null => serde_json::Value::Null,
567 Value::Bool(b) => serde_json::Value::Bool(*b),
568 Value::Int(i) => serde_json::Value::Number((*i).into()),
569 Value::Float(f) => {
570 serde_json::Number::from_f64(*f)
574 .map(serde_json::Value::Number)
575 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
576 }
577 Value::String(s) => serde_json::Value::String(s.clone()),
578 Value::Json(json) => json.clone(),
579 Value::Bytes(data) => crate::bytes::bytes_to_envelope(data),
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn success_creates_ok_result() {
589 let result = ExecResult::success("hello world");
590 assert!(result.ok());
591 assert_eq!(result.code, 0);
592 assert_eq!(&*result.text_out(),"hello world");
593 assert!(result.err.is_empty());
594 }
595
596 #[test]
597 fn value_to_json_finite_float_is_number() {
598 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
599 }
600
601 #[test]
602 fn value_to_json_non_finite_float_serializes_to_string() {
603 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
605 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
606 assert_eq!(
607 value_to_json(&Value::Float(f64::NEG_INFINITY)),
608 serde_json::json!("-inf")
609 );
610 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
612 }
613
614 #[test]
615 fn failure_creates_non_ok_result() {
616 let result = ExecResult::failure(1, "command not found");
617 assert!(!result.ok());
618 assert_eq!(result.code, 1);
619 assert_eq!(result.err, "command not found\n");
620 }
621
622 #[test]
623 fn failure_ends_exactly_one_newline() {
624 assert_eq!(ExecResult::failure(1, "msg").err, "msg\n");
625 assert_eq!(ExecResult::failure(1, "msg\n").err, "msg\n");
626 assert_eq!(ExecResult::failure(1, "msg\n\n\n").err, "msg\n");
627 }
628
629 #[test]
630 fn failure_empty_message_stays_empty() {
631 assert_eq!(ExecResult::failure(1, "").err, "");
634 }
635
636 #[test]
637 fn terminate_diagnostic_keeps_multiline_interior_newlines() {
638 let msg = "wc: a: not found\nwc: b: not found";
639 assert_eq!(
640 ExecResult::terminate_diagnostic(msg),
641 "wc: a: not found\nwc: b: not found\n"
642 );
643 }
644
645 #[test]
646 fn from_parts_ends_the_diagnostic_line() {
647 assert_eq!(ExecResult::from_parts(1, String::new(), "boom".into(), None).err, "boom\n");
648 assert_eq!(ExecResult::from_parts(1, String::new(), String::new(), None).err, "");
649 }
650
651 #[test]
652 fn from_output_keeps_external_stderr_byte_faithful() {
653 let result = ExecResult::from_output(1, "", "died mid-line");
656 assert_eq!(result.err, "died mid-line");
657 }
658
659 #[test]
660 fn success_does_not_sniff_json_stdout() {
661 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
664 assert!(result.data.is_none());
665 assert_eq!(&*result.text_out(),r#"{"count": 42, "items": ["a", "b"]}"#);
666 }
667
668 #[test]
669 fn from_output_does_not_sniff_json_stdout() {
670 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
671 assert!(result.data.is_none());
672 assert_eq!(&*result.text_out(),"[1, 2, 3]");
673 }
674
675 #[test]
676 fn non_json_stdout_has_no_data() {
677 let result = ExecResult::success("just plain text");
678 assert!(result.data.is_none());
679 }
680
681 #[test]
682 fn success_data_creates_result_with_value() {
683 let value = Value::String("test data".into());
684 let result = ExecResult::success_data(value.clone());
685 assert!(result.ok());
686 assert_eq!(result.data, Some(value));
687 }
688
689 #[test]
690 fn did_spill_defaults_to_false() {
691 assert!(!ExecResult::success("hi").did_spill);
692 assert!(!ExecResult::failure(1, "err").did_spill);
693 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
694 }
695
696 #[test]
697 fn did_spill_is_serialized() {
698 let mut result = ExecResult::success("hi");
699 result.did_spill = true;
700 let json = serde_json::to_string(&result).unwrap();
701 assert!(json.contains("\"did_spill\":true"));
702 }
703
704 #[test]
705 fn original_code_omitted_when_none() {
706 let result = ExecResult::success("hi");
707 let json = serde_json::to_string(&result).unwrap();
708 assert!(!json.contains("original_code"));
709 }
710
711 #[test]
712 fn original_code_present_when_set() {
713 let mut result = ExecResult::success("hi");
714 result.original_code = Some(0);
715 let json = serde_json::to_string(&result).unwrap();
716 assert!(json.contains("\"original_code\":0"));
717 }
718
719 #[test]
720 fn default_is_empty_success() {
721 let result = ExecResult::default();
722 assert!(result.ok());
723 assert!(result.text_out().is_empty());
724 assert!(result.data.is_none());
725 assert!(result.content_type.is_none());
726 assert!(result.baggage.is_empty());
727 }
728
729 #[test]
730 fn from_parts_creates_result() {
731 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
732 assert_eq!(result.code, 42);
733 assert_eq!(&*result.text_out(),"out");
734 assert_eq!(result.err, "err\n");
735 assert!(result.data.is_none());
736 assert!(result.output.is_none());
737 }
738
739 #[test]
740 fn with_code_sets_code() {
741 let result = ExecResult::success("hi").with_code(42);
742 assert_eq!(result.code, 42);
743 assert_eq!(&*result.text_out(),"hi");
744 }
745
746 #[test]
747 fn output_getter() {
748 use crate::output::{OutputData, OutputNode};
749 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
751 let result = ExecResult::with_output(nodes);
752 assert!(result.output().is_some());
753 assert!(result.has_output());
754
755 let text_result = ExecResult::with_output(OutputData::text("test"));
757 assert!(!text_result.has_output());
758 assert_eq!(&*text_result.text_out(), "test");
759
760 let plain = ExecResult::success("text");
761 assert!(plain.output().is_none());
762 assert!(!plain.has_output());
763 }
764
765 #[test]
766 fn set_out_and_push_out_and_clear_out() {
767 let mut result = ExecResult::success("");
768 result.set_out("hello".into());
769 assert_eq!(&*result.text_out(),"hello");
770 result.push_out(" world");
771 assert_eq!(&*result.text_out(),"hello world");
772 result.clear_out();
773 assert!(result.text_out().is_empty());
774 }
775
776 #[test]
777 fn set_output_and_take_output() {
778 use crate::output::OutputData;
779 let mut result = ExecResult::success("");
780 assert!(result.take_output().is_none());
781
782 result.set_output(Some(OutputData::text("data")));
783 assert!(result.has_output());
784
785 let taken = result.take_output();
786 assert!(taken.is_some());
787 assert!(!result.has_output());
788 }
789
790 #[test]
791 fn materialize_populates_out_from_output() {
792 use crate::output::{OutputData, OutputNode};
793 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
795 let mut result = ExecResult::with_output(nodes);
796 assert!(matches!(&result.out, OutputPayload::Text(s) if s.is_empty()));
799 assert!(result.has_output());
800 result.materialize();
801 assert_eq!(&*result.text_out(),"a\nb");
802 assert!(result.output.is_none());
803 }
804
805 #[test]
806 fn value_bytes_round_trips_through_envelope() {
807 let v = Value::Bytes(vec![0u8, 1, 2, 255, 128]);
808 let json = value_to_json(&v);
809 assert_eq!(json["_type"], "bytes");
810 assert_eq!(json["len"], 5);
811 assert_eq!(json_to_value(json), v);
813 let obj = serde_json::json!({"name": "amy"});
815 assert!(matches!(json_to_value(obj), Value::Json(_)));
816 }
817
818 #[test]
819 fn no_envelope_never_decodes_bytes() {
820 let envelope = crate::bytes::bytes_to_envelope(&[1u8, 2, 3]);
824 assert!(matches!(json_to_value(envelope.clone()), Value::Bytes(_)));
826 assert!(matches!(
828 json_to_value_no_envelope(envelope),
829 Value::Json(serde_json::Value::Object(_))
830 ));
831 }
832
833 #[test]
834 fn no_envelope_shares_unwrap_law_for_scalars() {
835 assert_eq!(json_to_value_no_envelope(serde_json::json!(42)), Value::Int(42));
837 assert_eq!(json_to_value_no_envelope(serde_json::json!(1.5)), Value::Float(1.5));
838 assert_eq!(json_to_value_no_envelope(serde_json::json!(true)), Value::Bool(true));
839 assert_eq!(json_to_value_no_envelope(serde_json::json!("hi")), Value::String("hi".into()));
840 assert_eq!(json_to_value_no_envelope(serde_json::json!(null)), Value::Null);
841 assert!(matches!(
842 json_to_value_no_envelope(serde_json::json!([1, 2])),
843 Value::Json(serde_json::Value::Array(_))
844 ));
845 }
846
847 #[test]
848 fn output_payload_text_serializes_as_bare_string() {
849 let r = ExecResult::success("hello");
852 let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap();
853 assert_eq!(json["out"], "hello");
854 let back: ExecResult = serde_json::from_value(json).unwrap();
856 assert_eq!(&*back.text_out(), "hello");
857 assert!(!back.is_bytes());
858 }
859
860 #[test]
861 fn success_bytes_carries_binary_and_round_trips() {
862 let r = ExecResult::success_bytes(vec![0u8, 159, 146, 150]); assert!(r.is_bytes());
864 assert_eq!(r.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
865 assert!(r.try_text_out().is_err());
867 assert!(r.text_out().contains('\u{fffd}'));
869 let json: serde_json::Value = serde_json::to_value(&r).unwrap();
871 assert_eq!(json["out"]["_type"], "bytes");
872 let back: ExecResult = serde_json::from_value(json).unwrap();
873 assert_eq!(back.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
874 }
875
876 #[test]
877 fn valid_utf8_bytes_coerce_to_text() {
878 let r = ExecResult::success_bytes(b"plain text".to_vec());
879 assert!(r.is_bytes());
880 assert_eq!(r.try_text_out().unwrap(), "plain text");
881 assert_eq!(&*r.text_out(), "plain text");
882 }
883
884 #[test]
885 fn materialize_preserves_existing_out() {
886 use crate::output::OutputData;
887 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
888 result.materialize();
889 assert_eq!(&*result.text_out(),"custom");
890 }
891
892 #[test]
893 fn take_output_for_stream_when_out_empty() {
894 use crate::output::{OutputData, OutputNode};
895 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
897 let mut result = ExecResult::with_output(nodes);
898 let taken = result.take_output_for_stream();
899 assert!(taken.is_some());
900 assert!(!result.has_output());
901 }
902
903 #[test]
904 fn with_output_simple_text_populates_out_directly() {
905 use crate::output::OutputData;
906 let result = ExecResult::with_output(OutputData::text("hello"));
907 assert!(!result.has_output());
909 assert_eq!(&*result.text_out(), "hello");
910 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
912 assert!(json_result.data.is_none());
913 }
914
915
916 #[test]
917 fn clear_stdout_drops_data() {
918 let mut result = ExecResult::success_data(Value::Json(serde_json::json!([1, 2, 3])));
920 result.clear_stdout();
921 assert!(result.data.is_none(), "data-plane .data must clear");
922 }
923
924 #[test]
925 fn take_output_for_stream_when_out_populated() {
926 use crate::output::OutputData;
927 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
928 let taken = result.take_output_for_stream();
929 assert!(taken.is_none());
930 assert!(result.has_output()); }
932}