1use clap::{Args, ValueEnum};
15use is_terminal::IsTerminal;
16use serde::{Deserialize, Serialize};
17
18#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum)]
22#[serde(rename_all = "lowercase")]
23pub enum OutputFormat {
24 #[default]
26 Text,
27 Json,
29 #[value(alias = "ndjson")]
31 Jsonl,
32 Raw,
34}
35
36impl OutputFormat {
37 #[must_use]
39 pub const fn is_machine_readable(self) -> bool {
40 matches!(self, Self::Json | Self::Jsonl)
41 }
42
43 #[must_use]
45 pub const fn is_human_readable(self) -> bool {
46 matches!(self, Self::Text)
47 }
48
49 #[must_use]
53 pub fn detect() -> Self {
54 if std::io::stdout().is_terminal() {
55 Self::Text
56 } else {
57 Self::Json
58 }
59 }
60}
61
62impl std::fmt::Display for OutputFormat {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::Text => write!(f, "text"),
66 Self::Json => write!(f, "json"),
67 Self::Jsonl => write!(f, "jsonl"),
68 Self::Raw => write!(f, "raw"),
69 }
70 }
71}
72
73#[derive(Args, Clone, Debug, Default, PartialEq, Eq)]
103pub struct OutputArgs {
104 #[arg(
108 short = 'f',
109 long = "format",
110 value_enum,
111 env = "BLZ_OUTPUT_FORMAT",
112 display_order = 44
113 )]
114 pub format: Option<OutputFormat>,
115
116 #[arg(long, conflicts_with = "format", display_order = 40)]
118 pub json: bool,
119
120 #[arg(long, conflicts_with_all = ["format", "json"], display_order = 41)]
122 pub jsonl: bool,
123
124 #[arg(long, conflicts_with_all = ["format", "json", "jsonl"], display_order = 42)]
126 pub text: bool,
127}
128
129impl OutputArgs {
130 #[must_use]
132 pub const fn with_format(format: OutputFormat) -> Self {
133 Self {
134 format: Some(format),
135 json: false,
136 jsonl: false,
137 text: false,
138 }
139 }
140
141 #[must_use]
148 pub fn resolve(&self) -> OutputFormat {
149 if self.json {
151 return OutputFormat::Json;
152 }
153 if self.jsonl {
154 return OutputFormat::Jsonl;
155 }
156 if self.text {
157 return OutputFormat::Text;
158 }
159
160 if let Some(format) = self.format {
162 return format;
163 }
164
165 OutputFormat::detect()
167 }
168
169 #[must_use]
171 pub const fn is_explicit(&self) -> bool {
172 self.format.is_some() || self.json || self.jsonl || self.text
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 mod output_format {
181 use super::*;
182
183 #[test]
184 fn test_default_is_text() {
185 assert_eq!(OutputFormat::default(), OutputFormat::Text);
186 }
187
188 #[test]
189 fn test_is_machine_readable() {
190 assert!(OutputFormat::Json.is_machine_readable());
191 assert!(OutputFormat::Jsonl.is_machine_readable());
192 assert!(!OutputFormat::Text.is_machine_readable());
193 assert!(!OutputFormat::Raw.is_machine_readable());
194 }
195
196 #[test]
197 fn test_is_human_readable() {
198 assert!(OutputFormat::Text.is_human_readable());
199 assert!(!OutputFormat::Json.is_human_readable());
200 assert!(!OutputFormat::Jsonl.is_human_readable());
201 assert!(!OutputFormat::Raw.is_human_readable());
202 }
203
204 #[test]
205 fn test_display() {
206 assert_eq!(OutputFormat::Text.to_string(), "text");
207 assert_eq!(OutputFormat::Json.to_string(), "json");
208 assert_eq!(OutputFormat::Jsonl.to_string(), "jsonl");
209 assert_eq!(OutputFormat::Raw.to_string(), "raw");
210 }
211 }
212
213 mod output_args {
214 use super::*;
215
216 #[test]
217 fn test_default() {
218 let args = OutputArgs::default();
219 assert_eq!(args.format, None);
220 assert!(!args.json);
221 assert!(!args.jsonl);
222 assert!(!args.text);
223 assert!(!args.is_explicit());
224 }
225
226 #[test]
227 fn test_with_format() {
228 let args = OutputArgs::with_format(OutputFormat::Json);
229 assert_eq!(args.format, Some(OutputFormat::Json));
230 assert!(args.is_explicit());
231 }
232
233 #[test]
234 fn test_resolve_json_flag() {
235 let args = OutputArgs {
236 format: None,
237 json: true,
238 jsonl: false,
239 text: false,
240 };
241 assert_eq!(args.resolve(), OutputFormat::Json);
242 }
243
244 #[test]
245 fn test_resolve_jsonl_flag() {
246 let args = OutputArgs {
247 format: None,
248 json: false,
249 jsonl: true,
250 text: false,
251 };
252 assert_eq!(args.resolve(), OutputFormat::Jsonl);
253 }
254
255 #[test]
256 fn test_resolve_text_flag() {
257 let args = OutputArgs {
258 format: None,
259 json: false,
260 jsonl: false,
261 text: true,
262 };
263 assert_eq!(args.resolve(), OutputFormat::Text);
264 }
265
266 #[test]
267 fn test_resolve_explicit_format() {
268 let args = OutputArgs {
269 format: Some(OutputFormat::Raw),
270 json: false,
271 jsonl: false,
272 text: false,
273 };
274 assert_eq!(args.resolve(), OutputFormat::Raw);
275 }
276
277 #[test]
278 fn test_json_flag_takes_precedence_over_format() {
279 let args = OutputArgs {
281 format: Some(OutputFormat::Text),
282 json: true,
283 jsonl: false,
284 text: false,
285 };
286 assert_eq!(args.resolve(), OutputFormat::Json);
288 }
289 }
290}