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,
100 pub data: Option<Value>,
103 output: Option<Box<OutputData>>,
113 pub did_spill: bool,
121 #[serde(skip_serializing_if = "Option::is_none")]
124 pub original_code: Option<i64>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub content_type: Option<String>,
129 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
134 pub baggage: BTreeMap<String, String>,
135}
136
137impl ExecResult {
138 pub fn success(out: impl Into<String>) -> Self {
140 Self {
141 code: 0,
142 out: OutputPayload::Text(out.into()),
143 err: String::new(),
144 data: None,
145 output: None,
146 did_spill: false,
147 original_code: None,
148 content_type: None,
149 baggage: BTreeMap::new(),
150 }
151 }
152
153 pub fn with_output(output: OutputData) -> Self {
158 match output.into_text() {
161 Ok(text) => Self::success(text),
162 Err(output) => Self {
163 code: 0,
164 out: OutputPayload::Text(String::new()),
165 err: String::new(),
166 data: None,
167 output: Some(Box::new(output)),
168 did_spill: false,
169 original_code: None,
170 content_type: None,
171 baggage: BTreeMap::new(),
172 },
173 }
174 }
175
176 pub fn success_bytes(bytes: Vec<u8>) -> Self {
178 let mut r = Self::success("");
179 r.out = OutputPayload::Bytes(bytes);
180 r
181 }
182
183 pub fn success_text_or_bytes(bytes: Vec<u8>) -> Self {
189 match String::from_utf8(bytes) {
190 Ok(text) => Self::success(text),
191 Err(e) => Self::success_bytes(e.into_bytes()),
192 }
193 }
194
195 pub fn success_data(data: Value) -> Self {
197 let out = value_to_json(&data).to_string();
198 Self {
199 code: 0,
200 out: OutputPayload::Text(out),
201 err: String::new(),
202 data: Some(data),
203 output: None,
204 did_spill: false,
205 original_code: None,
206 content_type: None,
207 baggage: BTreeMap::new(),
208 }
209 }
210
211 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
220 Self {
221 code: 0,
222 out: OutputPayload::Text(out.into()),
223 err: String::new(),
224 data: Some(data),
225 output: None,
226 did_spill: false,
227 original_code: None,
228 content_type: None,
229 baggage: BTreeMap::new(),
230 }
231 }
232
233 pub fn failure(code: i64, err: impl Into<String>) -> Self {
235 Self {
236 code,
237 out: OutputPayload::Text(String::new()),
238 err: err.into(),
239 data: None,
240 output: None,
241 did_spill: false,
242 original_code: None,
243 content_type: None,
244 baggage: BTreeMap::new(),
245 }
246 }
247
248 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
254 Self {
255 code,
256 out: OutputPayload::Text(stdout.into()),
257 err: stderr.into(),
258 data: None,
259 output: None,
260 did_spill: false,
261 original_code: None,
262 content_type: None,
263 baggage: BTreeMap::new(),
264 }
265 }
266
267 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
272 Self {
273 code: 0,
274 out: OutputPayload::Text(text.into()),
275 err: String::new(),
276 data: None,
277 output: Some(Box::new(output)),
278 did_spill: false,
279 original_code: None,
280 content_type: None,
281 baggage: BTreeMap::new(),
282 }
283 }
284
285 pub fn from_parts(
287 code: i64,
288 out: String,
289 err: String,
290 data: Option<Value>,
291 ) -> Self {
292 Self {
293 code,
294 out: OutputPayload::Text(out),
295 err,
296 data,
297 output: None,
298 did_spill: false,
299 original_code: None,
300 content_type: None,
301 baggage: BTreeMap::new(),
302 }
303 }
304
305 pub fn with_code(mut self, code: i64) -> Self {
307 self.code = code;
308 self
309 }
310
311 pub fn text_out(&self) -> Cow<'_, str> {
326 match &self.out {
327 OutputPayload::Text(s) if !s.is_empty() => Cow::Borrowed(s),
328 OutputPayload::Bytes(b) => match std::str::from_utf8(b) {
329 Ok(s) => Cow::Borrowed(s),
330 Err(_) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
331 },
332 _ => match self.output {
334 Some(ref output) => Cow::Owned(output.to_canonical_string()),
335 None => Cow::Borrowed(""),
336 },
337 }
338 }
339
340 pub fn try_text_out(&self) -> Result<Cow<'_, str>, BinaryNotText> {
345 match &self.out {
346 OutputPayload::Bytes(b) => std::str::from_utf8(b)
347 .map(Cow::Borrowed)
348 .map_err(|_| BinaryNotText { len: b.len() }),
349 _ => Ok(self.text_out()),
350 }
351 }
352
353 pub fn out_bytes(&self) -> Option<&[u8]> {
355 match &self.out {
356 OutputPayload::Bytes(b) => Some(b),
357 OutputPayload::Text(_) => None,
358 }
359 }
360
361 pub fn is_bytes(&self) -> bool {
363 matches!(self.out, OutputPayload::Bytes(_))
364 }
365
366 pub fn output(&self) -> Option<&OutputData> {
368 self.output.as_deref()
369 }
370
371 pub fn has_output(&self) -> bool {
373 self.output.is_some()
374 }
375
376 pub fn set_out(&mut self, s: String) {
380 self.out = OutputPayload::Text(s);
381 }
382
383 pub fn set_out_bytes(&mut self, b: Vec<u8>) {
385 self.out = OutputPayload::Bytes(b);
386 }
387
388 pub fn push_out(&mut self, s: &str) {
390 match &mut self.out {
391 OutputPayload::Text(t) => t.push_str(s),
392 OutputPayload::Bytes(b) => b.extend_from_slice(s.as_bytes()),
393 }
394 }
395
396 pub fn clear_out(&mut self) {
398 self.out = OutputPayload::Text(String::new());
399 }
400
401 pub fn clear_stdout(&mut self) {
414 self.out = OutputPayload::Text(String::new());
415 self.output = None;
416 self.data = None;
417 }
418
419 pub fn set_output(&mut self, o: Option<OutputData>) {
421 self.output = o.map(Box::new);
422 }
423
424 pub fn take_output(&mut self) -> Option<OutputData> {
426 self.output.take().map(|o| *o)
427 }
428
429 pub fn materialize(&mut self) {
432 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
433 if let Some(ref output) = self.output {
434 self.out = OutputPayload::Text(output.to_canonical_string());
435 }
436 }
437 self.output = None;
438 }
439
440 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
443 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
444 self.output.take().map(|o| *o)
445 } else {
446 None
447 }
448 }
449
450 pub fn ok(&self) -> bool {
452 self.code == 0
453 }
454
455 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
457 self.content_type = Some(ct.into());
458 self
459 }
460
461}
462
463pub fn json_to_value(json: serde_json::Value) -> Value {
468 match json {
469 serde_json::Value::Null => Value::Null,
470 serde_json::Value::Bool(b) => Value::Bool(b),
471 serde_json::Value::Number(n) => {
472 if let Some(i) = n.as_i64() {
473 Value::Int(i)
474 } else if let Some(f) = n.as_f64() {
475 Value::Float(f)
476 } else {
477 Value::String(n.to_string())
478 }
479 }
480 serde_json::Value::String(s) => Value::String(s),
481 serde_json::Value::Object(_) => match crate::bytes::envelope_to_bytes(&json) {
484 Some(bytes) => Value::Bytes(bytes),
485 None => Value::Json(json),
486 },
487 serde_json::Value::Array(_) => Value::Json(json),
488 }
489}
490
491pub fn json_to_value_no_envelope(json: serde_json::Value) -> Value {
501 match json {
502 serde_json::Value::Null => Value::Null,
503 serde_json::Value::Bool(b) => Value::Bool(b),
504 serde_json::Value::Number(n) => {
505 if let Some(i) = n.as_i64() {
506 Value::Int(i)
507 } else if let Some(f) = n.as_f64() {
508 Value::Float(f)
509 } else {
510 Value::String(n.to_string())
511 }
512 }
513 serde_json::Value::String(s) => Value::String(s),
514 serde_json::Value::Object(_) | serde_json::Value::Array(_) => Value::Json(json),
517 }
518}
519
520pub fn value_to_json(value: &Value) -> serde_json::Value {
522 match value {
523 Value::Null => serde_json::Value::Null,
524 Value::Bool(b) => serde_json::Value::Bool(*b),
525 Value::Int(i) => serde_json::Value::Number((*i).into()),
526 Value::Float(f) => {
527 serde_json::Number::from_f64(*f)
531 .map(serde_json::Value::Number)
532 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
533 }
534 Value::String(s) => serde_json::Value::String(s.clone()),
535 Value::Json(json) => json.clone(),
536 Value::Bytes(data) => crate::bytes::bytes_to_envelope(data),
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn success_creates_ok_result() {
546 let result = ExecResult::success("hello world");
547 assert!(result.ok());
548 assert_eq!(result.code, 0);
549 assert_eq!(&*result.text_out(),"hello world");
550 assert!(result.err.is_empty());
551 }
552
553 #[test]
554 fn value_to_json_finite_float_is_number() {
555 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
556 }
557
558 #[test]
559 fn value_to_json_non_finite_float_serializes_to_string() {
560 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
562 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
563 assert_eq!(
564 value_to_json(&Value::Float(f64::NEG_INFINITY)),
565 serde_json::json!("-inf")
566 );
567 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
569 }
570
571 #[test]
572 fn failure_creates_non_ok_result() {
573 let result = ExecResult::failure(1, "command not found");
574 assert!(!result.ok());
575 assert_eq!(result.code, 1);
576 assert_eq!(result.err, "command not found");
577 }
578
579 #[test]
580 fn success_does_not_sniff_json_stdout() {
581 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
584 assert!(result.data.is_none());
585 assert_eq!(&*result.text_out(),r#"{"count": 42, "items": ["a", "b"]}"#);
586 }
587
588 #[test]
589 fn from_output_does_not_sniff_json_stdout() {
590 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
591 assert!(result.data.is_none());
592 assert_eq!(&*result.text_out(),"[1, 2, 3]");
593 }
594
595 #[test]
596 fn non_json_stdout_has_no_data() {
597 let result = ExecResult::success("just plain text");
598 assert!(result.data.is_none());
599 }
600
601 #[test]
602 fn success_data_creates_result_with_value() {
603 let value = Value::String("test data".into());
604 let result = ExecResult::success_data(value.clone());
605 assert!(result.ok());
606 assert_eq!(result.data, Some(value));
607 }
608
609 #[test]
610 fn did_spill_defaults_to_false() {
611 assert!(!ExecResult::success("hi").did_spill);
612 assert!(!ExecResult::failure(1, "err").did_spill);
613 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
614 }
615
616 #[test]
617 fn did_spill_is_serialized() {
618 let mut result = ExecResult::success("hi");
619 result.did_spill = true;
620 let json = serde_json::to_string(&result).unwrap();
621 assert!(json.contains("\"did_spill\":true"));
622 }
623
624 #[test]
625 fn original_code_omitted_when_none() {
626 let result = ExecResult::success("hi");
627 let json = serde_json::to_string(&result).unwrap();
628 assert!(!json.contains("original_code"));
629 }
630
631 #[test]
632 fn original_code_present_when_set() {
633 let mut result = ExecResult::success("hi");
634 result.original_code = Some(0);
635 let json = serde_json::to_string(&result).unwrap();
636 assert!(json.contains("\"original_code\":0"));
637 }
638
639 #[test]
640 fn default_is_empty_success() {
641 let result = ExecResult::default();
642 assert!(result.ok());
643 assert!(result.text_out().is_empty());
644 assert!(result.data.is_none());
645 assert!(result.content_type.is_none());
646 assert!(result.baggage.is_empty());
647 }
648
649 #[test]
650 fn from_parts_creates_result() {
651 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
652 assert_eq!(result.code, 42);
653 assert_eq!(&*result.text_out(),"out");
654 assert_eq!(result.err, "err");
655 assert!(result.data.is_none());
656 assert!(result.output.is_none());
657 }
658
659 #[test]
660 fn with_code_sets_code() {
661 let result = ExecResult::success("hi").with_code(42);
662 assert_eq!(result.code, 42);
663 assert_eq!(&*result.text_out(),"hi");
664 }
665
666 #[test]
667 fn output_getter() {
668 use crate::output::{OutputData, OutputNode};
669 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
671 let result = ExecResult::with_output(nodes);
672 assert!(result.output().is_some());
673 assert!(result.has_output());
674
675 let text_result = ExecResult::with_output(OutputData::text("test"));
677 assert!(!text_result.has_output());
678 assert_eq!(&*text_result.text_out(), "test");
679
680 let plain = ExecResult::success("text");
681 assert!(plain.output().is_none());
682 assert!(!plain.has_output());
683 }
684
685 #[test]
686 fn set_out_and_push_out_and_clear_out() {
687 let mut result = ExecResult::success("");
688 result.set_out("hello".into());
689 assert_eq!(&*result.text_out(),"hello");
690 result.push_out(" world");
691 assert_eq!(&*result.text_out(),"hello world");
692 result.clear_out();
693 assert!(result.text_out().is_empty());
694 }
695
696 #[test]
697 fn set_output_and_take_output() {
698 use crate::output::OutputData;
699 let mut result = ExecResult::success("");
700 assert!(result.take_output().is_none());
701
702 result.set_output(Some(OutputData::text("data")));
703 assert!(result.has_output());
704
705 let taken = result.take_output();
706 assert!(taken.is_some());
707 assert!(!result.has_output());
708 }
709
710 #[test]
711 fn materialize_populates_out_from_output() {
712 use crate::output::{OutputData, OutputNode};
713 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
715 let mut result = ExecResult::with_output(nodes);
716 assert!(matches!(&result.out, OutputPayload::Text(s) if s.is_empty()));
719 assert!(result.has_output());
720 result.materialize();
721 assert_eq!(&*result.text_out(),"a\nb");
722 assert!(result.output.is_none());
723 }
724
725 #[test]
726 fn value_bytes_round_trips_through_envelope() {
727 let v = Value::Bytes(vec![0u8, 1, 2, 255, 128]);
728 let json = value_to_json(&v);
729 assert_eq!(json["_type"], "bytes");
730 assert_eq!(json["len"], 5);
731 assert_eq!(json_to_value(json), v);
733 let obj = serde_json::json!({"name": "amy"});
735 assert!(matches!(json_to_value(obj), Value::Json(_)));
736 }
737
738 #[test]
739 fn no_envelope_never_decodes_bytes() {
740 let envelope = crate::bytes::bytes_to_envelope(&[1u8, 2, 3]);
744 assert!(matches!(json_to_value(envelope.clone()), Value::Bytes(_)));
746 assert!(matches!(
748 json_to_value_no_envelope(envelope),
749 Value::Json(serde_json::Value::Object(_))
750 ));
751 }
752
753 #[test]
754 fn no_envelope_shares_unwrap_law_for_scalars() {
755 assert_eq!(json_to_value_no_envelope(serde_json::json!(42)), Value::Int(42));
757 assert_eq!(json_to_value_no_envelope(serde_json::json!(1.5)), Value::Float(1.5));
758 assert_eq!(json_to_value_no_envelope(serde_json::json!(true)), Value::Bool(true));
759 assert_eq!(json_to_value_no_envelope(serde_json::json!("hi")), Value::String("hi".into()));
760 assert_eq!(json_to_value_no_envelope(serde_json::json!(null)), Value::Null);
761 assert!(matches!(
762 json_to_value_no_envelope(serde_json::json!([1, 2])),
763 Value::Json(serde_json::Value::Array(_))
764 ));
765 }
766
767 #[test]
768 fn output_payload_text_serializes_as_bare_string() {
769 let r = ExecResult::success("hello");
772 let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap();
773 assert_eq!(json["out"], "hello");
774 let back: ExecResult = serde_json::from_value(json).unwrap();
776 assert_eq!(&*back.text_out(), "hello");
777 assert!(!back.is_bytes());
778 }
779
780 #[test]
781 fn success_bytes_carries_binary_and_round_trips() {
782 let r = ExecResult::success_bytes(vec![0u8, 159, 146, 150]); assert!(r.is_bytes());
784 assert_eq!(r.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
785 assert!(r.try_text_out().is_err());
787 assert!(r.text_out().contains('\u{fffd}'));
789 let json: serde_json::Value = serde_json::to_value(&r).unwrap();
791 assert_eq!(json["out"]["_type"], "bytes");
792 let back: ExecResult = serde_json::from_value(json).unwrap();
793 assert_eq!(back.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
794 }
795
796 #[test]
797 fn valid_utf8_bytes_coerce_to_text() {
798 let r = ExecResult::success_bytes(b"plain text".to_vec());
799 assert!(r.is_bytes());
800 assert_eq!(r.try_text_out().unwrap(), "plain text");
801 assert_eq!(&*r.text_out(), "plain text");
802 }
803
804 #[test]
805 fn materialize_preserves_existing_out() {
806 use crate::output::OutputData;
807 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
808 result.materialize();
809 assert_eq!(&*result.text_out(),"custom");
810 }
811
812 #[test]
813 fn take_output_for_stream_when_out_empty() {
814 use crate::output::{OutputData, OutputNode};
815 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
817 let mut result = ExecResult::with_output(nodes);
818 let taken = result.take_output_for_stream();
819 assert!(taken.is_some());
820 assert!(!result.has_output());
821 }
822
823 #[test]
824 fn with_output_simple_text_populates_out_directly() {
825 use crate::output::OutputData;
826 let result = ExecResult::with_output(OutputData::text("hello"));
827 assert!(!result.has_output());
829 assert_eq!(&*result.text_out(), "hello");
830 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
832 assert!(json_result.data.is_none());
833 }
834
835
836 #[test]
837 fn clear_stdout_drops_data() {
838 let mut result = ExecResult::success_data(Value::Json(serde_json::json!([1, 2, 3])));
840 result.clear_stdout();
841 assert!(result.data.is_none(), "data-plane .data must clear");
842 }
843
844 #[test]
845 fn take_output_for_stream_when_out_populated() {
846 use crate::output::OutputData;
847 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
848 let taken = result.take_output_for_stream();
849 assert!(taken.is_none());
850 assert!(result.has_output()); }
852}