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<OutputData>,
105 pub did_spill: bool,
110 #[serde(skip_serializing_if = "Option::is_none")]
113 pub original_code: Option<i64>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub content_type: Option<String>,
118 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
123 pub baggage: BTreeMap<String, String>,
124}
125
126impl ExecResult {
127 pub fn success(out: impl Into<String>) -> Self {
129 Self {
130 code: 0,
131 out: OutputPayload::Text(out.into()),
132 err: String::new(),
133 data: None,
134 output: None,
135 did_spill: false,
136 original_code: None,
137 content_type: None,
138 baggage: BTreeMap::new(),
139 }
140 }
141
142 pub fn with_output(output: OutputData) -> Self {
147 match output.into_text() {
150 Ok(text) => Self::success(text),
151 Err(output) => Self {
152 code: 0,
153 out: OutputPayload::Text(String::new()),
154 err: String::new(),
155 data: None,
156 output: Some(output),
157 did_spill: false,
158 original_code: None,
159 content_type: None,
160 baggage: BTreeMap::new(),
161 },
162 }
163 }
164
165 pub fn success_bytes(bytes: Vec<u8>) -> Self {
167 let mut r = Self::success("");
168 r.out = OutputPayload::Bytes(bytes);
169 r
170 }
171
172 pub fn success_text_or_bytes(bytes: Vec<u8>) -> Self {
178 match String::from_utf8(bytes) {
179 Ok(text) => Self::success(text),
180 Err(e) => Self::success_bytes(e.into_bytes()),
181 }
182 }
183
184 pub fn success_data(data: Value) -> Self {
186 let out = value_to_json(&data).to_string();
187 Self {
188 code: 0,
189 out: OutputPayload::Text(out),
190 err: String::new(),
191 data: Some(data),
192 output: None,
193 did_spill: false,
194 original_code: None,
195 content_type: None,
196 baggage: BTreeMap::new(),
197 }
198 }
199
200 pub fn success_with_data(out: impl Into<String>, data: Value) -> Self {
209 Self {
210 code: 0,
211 out: OutputPayload::Text(out.into()),
212 err: String::new(),
213 data: Some(data),
214 output: None,
215 did_spill: false,
216 original_code: None,
217 content_type: None,
218 baggage: BTreeMap::new(),
219 }
220 }
221
222 pub fn failure(code: i64, err: impl Into<String>) -> Self {
224 Self {
225 code,
226 out: OutputPayload::Text(String::new()),
227 err: err.into(),
228 data: None,
229 output: None,
230 did_spill: false,
231 original_code: None,
232 content_type: None,
233 baggage: BTreeMap::new(),
234 }
235 }
236
237 pub fn from_output(code: i64, stdout: impl Into<String>, stderr: impl Into<String>) -> Self {
243 Self {
244 code,
245 out: OutputPayload::Text(stdout.into()),
246 err: stderr.into(),
247 data: None,
248 output: None,
249 did_spill: false,
250 original_code: None,
251 content_type: None,
252 baggage: BTreeMap::new(),
253 }
254 }
255
256 pub fn with_output_and_text(output: OutputData, text: impl Into<String>) -> Self {
261 Self {
262 code: 0,
263 out: OutputPayload::Text(text.into()),
264 err: String::new(),
265 data: None,
266 output: Some(output),
267 did_spill: false,
268 original_code: None,
269 content_type: None,
270 baggage: BTreeMap::new(),
271 }
272 }
273
274 pub fn from_parts(
276 code: i64,
277 out: String,
278 err: String,
279 data: Option<Value>,
280 ) -> Self {
281 Self {
282 code,
283 out: OutputPayload::Text(out),
284 err,
285 data,
286 output: None,
287 did_spill: false,
288 original_code: None,
289 content_type: None,
290 baggage: BTreeMap::new(),
291 }
292 }
293
294 pub fn with_code(mut self, code: i64) -> Self {
296 self.code = code;
297 self
298 }
299
300 pub fn text_out(&self) -> Cow<'_, str> {
314 match &self.out {
315 OutputPayload::Text(s) if !s.is_empty() => Cow::Borrowed(s),
316 OutputPayload::Bytes(b) => match std::str::from_utf8(b) {
317 Ok(s) => Cow::Borrowed(s),
318 Err(_) => Cow::Owned(String::from_utf8_lossy(b).into_owned()),
319 },
320 _ => match self.output {
322 Some(ref output) => Cow::Owned(output.to_canonical_string()),
323 None => Cow::Borrowed(""),
324 },
325 }
326 }
327
328 pub fn try_text_out(&self) -> Result<Cow<'_, str>, BinaryNotText> {
333 match &self.out {
334 OutputPayload::Bytes(b) => std::str::from_utf8(b)
335 .map(Cow::Borrowed)
336 .map_err(|_| BinaryNotText { len: b.len() }),
337 _ => Ok(self.text_out()),
338 }
339 }
340
341 pub fn out_bytes(&self) -> Option<&[u8]> {
343 match &self.out {
344 OutputPayload::Bytes(b) => Some(b),
345 OutputPayload::Text(_) => None,
346 }
347 }
348
349 pub fn is_bytes(&self) -> bool {
351 matches!(self.out, OutputPayload::Bytes(_))
352 }
353
354 pub fn output(&self) -> Option<&OutputData> {
356 self.output.as_ref()
357 }
358
359 pub fn has_output(&self) -> bool {
361 self.output.is_some()
362 }
363
364 pub fn set_out(&mut self, s: String) {
368 self.out = OutputPayload::Text(s);
369 }
370
371 pub fn set_out_bytes(&mut self, b: Vec<u8>) {
373 self.out = OutputPayload::Bytes(b);
374 }
375
376 pub fn push_out(&mut self, s: &str) {
378 match &mut self.out {
379 OutputPayload::Text(t) => t.push_str(s),
380 OutputPayload::Bytes(b) => b.extend_from_slice(s.as_bytes()),
381 }
382 }
383
384 pub fn clear_out(&mut self) {
386 self.out = OutputPayload::Text(String::new());
387 }
388
389 pub fn set_output(&mut self, o: Option<OutputData>) {
391 self.output = o;
392 }
393
394 pub fn take_output(&mut self) -> Option<OutputData> {
396 self.output.take()
397 }
398
399 pub fn materialize(&mut self) {
402 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
403 if let Some(ref output) = self.output {
404 self.out = OutputPayload::Text(output.to_canonical_string());
405 }
406 }
407 self.output = None;
408 }
409
410 pub fn take_output_for_stream(&mut self) -> Option<OutputData> {
413 if matches!(&self.out, OutputPayload::Text(s) if s.is_empty()) {
414 self.output.take()
415 } else {
416 None
417 }
418 }
419
420 pub fn ok(&self) -> bool {
422 self.code == 0
423 }
424
425 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
427 self.content_type = Some(ct.into());
428 self
429 }
430
431}
432
433pub fn json_to_value(json: serde_json::Value) -> Value {
438 match json {
439 serde_json::Value::Null => Value::Null,
440 serde_json::Value::Bool(b) => Value::Bool(b),
441 serde_json::Value::Number(n) => {
442 if let Some(i) = n.as_i64() {
443 Value::Int(i)
444 } else if let Some(f) = n.as_f64() {
445 Value::Float(f)
446 } else {
447 Value::String(n.to_string())
448 }
449 }
450 serde_json::Value::String(s) => Value::String(s),
451 serde_json::Value::Object(_) => match crate::bytes::envelope_to_bytes(&json) {
454 Some(bytes) => Value::Bytes(bytes),
455 None => Value::Json(json),
456 },
457 serde_json::Value::Array(_) => Value::Json(json),
458 }
459}
460
461pub fn value_to_json(value: &Value) -> serde_json::Value {
463 match value {
464 Value::Null => serde_json::Value::Null,
465 Value::Bool(b) => serde_json::Value::Bool(*b),
466 Value::Int(i) => serde_json::Value::Number((*i).into()),
467 Value::Float(f) => {
468 serde_json::Number::from_f64(*f)
472 .map(serde_json::Value::Number)
473 .unwrap_or_else(|| serde_json::Value::String(f.to_string()))
474 }
475 Value::String(s) => serde_json::Value::String(s.clone()),
476 Value::Json(json) => json.clone(),
477 Value::Bytes(data) => crate::bytes::bytes_to_envelope(data),
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn success_creates_ok_result() {
487 let result = ExecResult::success("hello world");
488 assert!(result.ok());
489 assert_eq!(result.code, 0);
490 assert_eq!(&*result.text_out(),"hello world");
491 assert!(result.err.is_empty());
492 }
493
494 #[test]
495 fn value_to_json_finite_float_is_number() {
496 assert_eq!(value_to_json(&Value::Float(3.5)), serde_json::json!(3.5));
497 }
498
499 #[test]
500 fn value_to_json_non_finite_float_serializes_to_string() {
501 assert_eq!(value_to_json(&Value::Float(f64::NAN)), serde_json::json!("NaN"));
503 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), serde_json::json!("inf"));
504 assert_eq!(
505 value_to_json(&Value::Float(f64::NEG_INFINITY)),
506 serde_json::json!("-inf")
507 );
508 assert_ne!(value_to_json(&Value::Float(f64::NAN)), serde_json::Value::Null);
510 }
511
512 #[test]
513 fn failure_creates_non_ok_result() {
514 let result = ExecResult::failure(1, "command not found");
515 assert!(!result.ok());
516 assert_eq!(result.code, 1);
517 assert_eq!(result.err, "command not found");
518 }
519
520 #[test]
521 fn success_does_not_sniff_json_stdout() {
522 let result = ExecResult::success(r#"{"count": 42, "items": ["a", "b"]}"#);
525 assert!(result.data.is_none());
526 assert_eq!(&*result.text_out(),r#"{"count": 42, "items": ["a", "b"]}"#);
527 }
528
529 #[test]
530 fn from_output_does_not_sniff_json_stdout() {
531 let result = ExecResult::from_output(0, r#"[1, 2, 3]"#, "");
532 assert!(result.data.is_none());
533 assert_eq!(&*result.text_out(),"[1, 2, 3]");
534 }
535
536 #[test]
537 fn non_json_stdout_has_no_data() {
538 let result = ExecResult::success("just plain text");
539 assert!(result.data.is_none());
540 }
541
542 #[test]
543 fn success_data_creates_result_with_value() {
544 let value = Value::String("test data".into());
545 let result = ExecResult::success_data(value.clone());
546 assert!(result.ok());
547 assert_eq!(result.data, Some(value));
548 }
549
550 #[test]
551 fn did_spill_defaults_to_false() {
552 assert!(!ExecResult::success("hi").did_spill);
553 assert!(!ExecResult::failure(1, "err").did_spill);
554 assert!(!ExecResult::from_output(0, "out", "err").did_spill);
555 }
556
557 #[test]
558 fn did_spill_is_serialized() {
559 let mut result = ExecResult::success("hi");
560 result.did_spill = true;
561 let json = serde_json::to_string(&result).unwrap();
562 assert!(json.contains("\"did_spill\":true"));
563 }
564
565 #[test]
566 fn original_code_omitted_when_none() {
567 let result = ExecResult::success("hi");
568 let json = serde_json::to_string(&result).unwrap();
569 assert!(!json.contains("original_code"));
570 }
571
572 #[test]
573 fn original_code_present_when_set() {
574 let mut result = ExecResult::success("hi");
575 result.original_code = Some(0);
576 let json = serde_json::to_string(&result).unwrap();
577 assert!(json.contains("\"original_code\":0"));
578 }
579
580 #[test]
581 fn default_is_empty_success() {
582 let result = ExecResult::default();
583 assert!(result.ok());
584 assert!(result.text_out().is_empty());
585 assert!(result.data.is_none());
586 assert!(result.content_type.is_none());
587 assert!(result.baggage.is_empty());
588 }
589
590 #[test]
591 fn from_parts_creates_result() {
592 let result = ExecResult::from_parts(42, "out".into(), "err".into(), None);
593 assert_eq!(result.code, 42);
594 assert_eq!(&*result.text_out(),"out");
595 assert_eq!(result.err, "err");
596 assert!(result.data.is_none());
597 assert!(result.output.is_none());
598 }
599
600 #[test]
601 fn with_code_sets_code() {
602 let result = ExecResult::success("hi").with_code(42);
603 assert_eq!(result.code, 42);
604 assert_eq!(&*result.text_out(),"hi");
605 }
606
607 #[test]
608 fn output_getter() {
609 use crate::output::{OutputData, OutputNode};
610 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
612 let result = ExecResult::with_output(nodes);
613 assert!(result.output().is_some());
614 assert!(result.has_output());
615
616 let text_result = ExecResult::with_output(OutputData::text("test"));
618 assert!(!text_result.has_output());
619 assert_eq!(&*text_result.text_out(), "test");
620
621 let plain = ExecResult::success("text");
622 assert!(plain.output().is_none());
623 assert!(!plain.has_output());
624 }
625
626 #[test]
627 fn set_out_and_push_out_and_clear_out() {
628 let mut result = ExecResult::success("");
629 result.set_out("hello".into());
630 assert_eq!(&*result.text_out(),"hello");
631 result.push_out(" world");
632 assert_eq!(&*result.text_out(),"hello world");
633 result.clear_out();
634 assert!(result.text_out().is_empty());
635 }
636
637 #[test]
638 fn set_output_and_take_output() {
639 use crate::output::OutputData;
640 let mut result = ExecResult::success("");
641 assert!(result.take_output().is_none());
642
643 result.set_output(Some(OutputData::text("data")));
644 assert!(result.has_output());
645
646 let taken = result.take_output();
647 assert!(taken.is_some());
648 assert!(!result.has_output());
649 }
650
651 #[test]
652 fn materialize_populates_out_from_output() {
653 use crate::output::{OutputData, OutputNode};
654 let nodes = OutputData::nodes(vec![OutputNode::new("a"), OutputNode::new("b")]);
656 let mut result = ExecResult::with_output(nodes);
657 assert!(matches!(&result.out, OutputPayload::Text(s) if s.is_empty()));
660 assert!(result.has_output());
661 result.materialize();
662 assert_eq!(&*result.text_out(),"a\nb");
663 assert!(result.output.is_none());
664 }
665
666 #[test]
667 fn value_bytes_round_trips_through_envelope() {
668 let v = Value::Bytes(vec![0u8, 1, 2, 255, 128]);
669 let json = value_to_json(&v);
670 assert_eq!(json["_type"], "bytes");
671 assert_eq!(json["len"], 5);
672 assert_eq!(json_to_value(json), v);
674 let obj = serde_json::json!({"name": "amy"});
676 assert!(matches!(json_to_value(obj), Value::Json(_)));
677 }
678
679 #[test]
680 fn output_payload_text_serializes_as_bare_string() {
681 let r = ExecResult::success("hello");
684 let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&r).unwrap()).unwrap();
685 assert_eq!(json["out"], "hello");
686 let back: ExecResult = serde_json::from_value(json).unwrap();
688 assert_eq!(&*back.text_out(), "hello");
689 assert!(!back.is_bytes());
690 }
691
692 #[test]
693 fn success_bytes_carries_binary_and_round_trips() {
694 let r = ExecResult::success_bytes(vec![0u8, 159, 146, 150]); assert!(r.is_bytes());
696 assert_eq!(r.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
697 assert!(r.try_text_out().is_err());
699 assert!(r.text_out().contains('\u{fffd}'));
701 let json: serde_json::Value = serde_json::to_value(&r).unwrap();
703 assert_eq!(json["out"]["_type"], "bytes");
704 let back: ExecResult = serde_json::from_value(json).unwrap();
705 assert_eq!(back.out_bytes(), Some(&[0u8, 159, 146, 150][..]));
706 }
707
708 #[test]
709 fn valid_utf8_bytes_coerce_to_text() {
710 let r = ExecResult::success_bytes(b"plain text".to_vec());
711 assert!(r.is_bytes());
712 assert_eq!(r.try_text_out().unwrap(), "plain text");
713 assert_eq!(&*r.text_out(), "plain text");
714 }
715
716 #[test]
717 fn materialize_preserves_existing_out() {
718 use crate::output::OutputData;
719 let mut result = ExecResult::with_output_and_text(OutputData::text("ignored"), "custom");
720 result.materialize();
721 assert_eq!(&*result.text_out(),"custom");
722 }
723
724 #[test]
725 fn take_output_for_stream_when_out_empty() {
726 use crate::output::{OutputData, OutputNode};
727 let nodes = OutputData::nodes(vec![OutputNode::new("a")]);
729 let mut result = ExecResult::with_output(nodes);
730 let taken = result.take_output_for_stream();
731 assert!(taken.is_some());
732 assert!(!result.has_output());
733 }
734
735 #[test]
736 fn with_output_simple_text_populates_out_directly() {
737 use crate::output::OutputData;
738 let result = ExecResult::with_output(OutputData::text("hello"));
739 assert!(!result.has_output());
741 assert_eq!(&*result.text_out(), "hello");
742 let json_result = ExecResult::with_output(OutputData::text(r#"{"key": 1}"#));
744 assert!(json_result.data.is_none());
745 }
746
747 #[test]
748 fn take_output_for_stream_when_out_populated() {
749 use crate::output::OutputData;
750 let mut result = ExecResult::with_output_and_text(OutputData::text("x"), "custom");
751 let taken = result.take_output_for_stream();
752 assert!(taken.is_none());
753 assert!(result.has_output()); }
755}