Skip to main content

sbom_tools/pipeline/
output.rs

1//! Output handling for SBOM reports.
2//!
3//! Provides utilities for auto-detecting output format and writing reports.
4
5use crate::reports::ReportFormat;
6use anyhow::{Context, Result};
7use std::io::IsTerminal;
8use std::path::PathBuf;
9
10/// Target for output - either stdout or a file
11#[derive(Debug, Clone)]
12pub enum OutputTarget {
13    /// Write to stdout
14    Stdout,
15    /// Write to a file
16    File(PathBuf),
17}
18
19impl OutputTarget {
20    /// Create output target from optional path
21    pub fn from_option(path: Option<PathBuf>) -> Self {
22        path.map_or(Self::Stdout, Self::File)
23    }
24
25    /// Check if output is to a terminal
26    #[must_use]
27    pub fn is_terminal(&self) -> bool {
28        matches!(self, Self::Stdout) && std::io::stdout().is_terminal()
29    }
30}
31
32/// Auto-detect the output format based on TTY and output target
33///
34/// Returns TUI for interactive terminals (stdout to TTY),
35/// otherwise returns Summary for non-interactive contexts.
36#[must_use]
37pub fn auto_detect_format(format: ReportFormat, target: &OutputTarget) -> ReportFormat {
38    match format {
39        ReportFormat::Auto => {
40            if target.is_terminal() {
41                ReportFormat::Tui
42            } else {
43                ReportFormat::Summary
44            }
45        }
46        other => other,
47    }
48}
49
50/// Determine if color should be used based on flags and environment
51#[must_use]
52pub fn should_use_color(no_color_flag: bool) -> bool {
53    !no_color_flag && std::env::var("NO_COLOR").is_err()
54}
55
56/// Write output to the target (stdout or file)
57pub fn write_output(content: &str, target: &OutputTarget, quiet: bool) -> Result<()> {
58    match target {
59        OutputTarget::Stdout => {
60            println!("{content}");
61            Ok(())
62        }
63        OutputTarget::File(path) => {
64            std::fs::write(path, content)
65                .with_context(|| format!("Failed to write output to {}", path.display()))?;
66            if !quiet {
67                tracing::info!("Report written to {:?}", path);
68            }
69            Ok(())
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_output_target_from_option_none() {
80        let target = OutputTarget::from_option(None);
81        assert!(matches!(target, OutputTarget::Stdout));
82    }
83
84    #[test]
85    fn test_output_target_from_option_some() {
86        let path = PathBuf::from("/tmp/test.json");
87        let target = OutputTarget::from_option(Some(path.clone()));
88        match target {
89            OutputTarget::File(p) => assert_eq!(p, path),
90            _ => panic!("Expected File variant"),
91        }
92    }
93
94    #[test]
95    fn test_auto_detect_format_non_auto() {
96        let target = OutputTarget::Stdout;
97        assert_eq!(
98            auto_detect_format(ReportFormat::Json, &target),
99            ReportFormat::Json
100        );
101        assert_eq!(
102            auto_detect_format(ReportFormat::Sarif, &target),
103            ReportFormat::Sarif
104        );
105    }
106
107    #[test]
108    fn test_auto_detect_format_file_target() {
109        let target = OutputTarget::File(PathBuf::from("/tmp/test.json"));
110        // File targets are never terminals, so Auto -> Summary
111        assert_eq!(
112            auto_detect_format(ReportFormat::Auto, &target),
113            ReportFormat::Summary
114        );
115    }
116
117    #[test]
118    fn test_should_use_color_with_flag() {
119        assert!(!should_use_color(true));
120    }
121
122    #[test]
123    fn test_should_use_color_without_flag() {
124        // This depends on NO_COLOR env var
125        let expected = std::env::var("NO_COLOR").is_err();
126        assert_eq!(should_use_color(false), expected);
127    }
128}