Skip to main content

agent_first_data/
output.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5/// Output format for CLI and pipe/MCP modes.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum OutputFormat {
9    Json,
10    /// Structure-preserving YAML (same semantics as [`OutputFormat::Json`]).
11    Yaml,
12    Plain,
13}
14
15impl OutputFormat {
16    /// Return the canonical CLI/config spelling.
17    pub const fn as_str(self) -> &'static str {
18        match self {
19            Self::Json => "json",
20            Self::Yaml => "yaml",
21            Self::Plain => "plain",
22        }
23    }
24}
25
26impl fmt::Display for OutputFormat {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        formatter.write_str(self.as_str())
29    }
30}
31
32impl FromStr for OutputFormat {
33    type Err = String;
34
35    fn from_str(value: &str) -> Result<Self, Self::Err> {
36        match value {
37            "json" => Ok(Self::Json),
38            "yaml" => Ok(Self::Yaml),
39            "plain" => Ok(Self::Plain),
40            _ => Err("invalid output format: expected json, yaml, or plain".to_string()),
41        }
42    }
43}
44
45/// Where a CLI emitter sends its events.
46#[cfg(feature = "cli")]
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum OutputTo {
50    /// Finite one-shot: `result` → stdout, `error`/`progress`/`log` → stderr.
51    Split,
52    /// Event stream: every event onto stdout.
53    Stdout,
54    /// Event stream: every event onto stderr.
55    Stderr,
56}
57
58#[cfg(feature = "cli")]
59impl OutputTo {
60    /// Return the canonical CLI/config spelling.
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Split => "split",
64            Self::Stdout => "stdout",
65            Self::Stderr => "stderr",
66        }
67    }
68
69    /// Parse an `--output-to` value.
70    pub fn parse(value: &str) -> Result<Self, String> {
71        value.parse()
72    }
73}
74
75#[cfg(feature = "cli")]
76impl fmt::Display for OutputTo {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        formatter.write_str(self.as_str())
79    }
80}
81
82#[cfg(feature = "cli")]
83impl FromStr for OutputTo {
84    type Err = String;
85
86    fn from_str(value: &str) -> Result<Self, Self::Err> {
87        match value {
88            "split" => Ok(Self::Split),
89            "stdout" => Ok(Self::Stdout),
90            "stderr" => Ok(Self::Stderr),
91            _ => Err("unsupported --output-to: expected split, stdout, or stderr".to_string()),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::OutputFormat;
99    #[cfg(feature = "cli")]
100    use super::OutputTo;
101
102    #[test]
103    fn output_types_round_trip_text_and_serde() {
104        for format in [OutputFormat::Json, OutputFormat::Yaml, OutputFormat::Plain] {
105            assert_eq!(format.to_string().parse(), Ok(format));
106            assert_eq!(
107                serde_json::from_str::<OutputFormat>(
108                    &serde_json::to_string(&format).unwrap_or_default()
109                )
110                .ok(),
111                Some(format)
112            );
113        }
114        #[cfg(feature = "cli")]
115        {
116            for destination in [OutputTo::Split, OutputTo::Stdout, OutputTo::Stderr] {
117                assert_eq!(destination.to_string().parse(), Ok(destination));
118                assert_eq!(
119                    serde_json::from_str::<OutputTo>(
120                        &serde_json::to_string(&destination).unwrap_or_default()
121                    )
122                    .ok(),
123                    Some(destination)
124                );
125            }
126        }
127
128        let format_canary = "canary-output-format-secret";
129        let format_error = format_canary.parse::<OutputFormat>().unwrap_err();
130        assert!(!format_error.contains(format_canary));
131        assert!(format_error.contains("json"));
132
133        #[cfg(feature = "cli")]
134        {
135            let destination_canary = "canary-output-to-secret";
136            let destination_error = destination_canary.parse::<OutputTo>().unwrap_err();
137            assert!(!destination_error.contains(destination_canary));
138            assert!(destination_error.contains("split"));
139        }
140    }
141}