Skip to main content

fallow_types/
output_format.rs

1/// Output format for fallow results.
2///
3/// This is command-line and integration metadata, not stored in config files.
4/// Keeping it in `fallow-types` lets config, output, CLI, MCP, and API layers
5/// agree on the same contract without creating a config-to-output dependency.
6#[derive(Debug, Default, Clone, Copy)]
7pub enum OutputFormat {
8    /// Human-readable terminal output with source context.
9    #[default]
10    Human,
11    /// Machine-readable JSON.
12    Json,
13    /// SARIF format for GitHub Code Scanning.
14    Sarif,
15    /// One issue per line (grep-friendly).
16    Compact,
17    /// Markdown for PR comments.
18    Markdown,
19    /// `CodeClimate` JSON for GitLab Code Quality.
20    ///
21    /// CLI aliases: `codeclimate`, `gitlab-codequality`, `gitlab-code-quality`.
22    CodeClimate,
23    /// GitHub-flavored sticky PR comment markdown.
24    PrCommentGithub,
25    /// GitLab-flavored sticky MR comment markdown.
26    PrCommentGitlab,
27    /// GitHub PR review JSON envelope.
28    ReviewGithub,
29    /// GitLab MR review JSON envelope.
30    ReviewGitlab,
31    /// Shields.io-compatible SVG badge (health command only).
32    Badge,
33    /// GitHub Actions workflow-command annotations (`::error` / `::warning` /
34    /// `::notice` lines). Provider-prefixed name because workflow-command
35    /// annotations are a GitHub-only concept with no GitLab twin.
36    GithubAnnotations,
37    /// GitHub Actions job-summary markdown (for `>> $GITHUB_STEP_SUMMARY`).
38    /// Provider-prefixed for the same reason as `GithubAnnotations`.
39    GithubSummary,
40}
41
42impl OutputFormat {
43    /// The `--format` spelling of this variant.
44    ///
45    /// Notes that name the format a run degraded on have to name it the way the
46    /// user would pass it, so the sentence doubles as the fix. `codeclimate` is
47    /// the canonical spelling of the aliased GitLab Code Quality format.
48    #[must_use]
49    pub const fn flag_label(self) -> &'static str {
50        match self {
51            Self::Human => "human",
52            Self::Json => "json",
53            Self::Sarif => "sarif",
54            Self::Compact => "compact",
55            Self::Markdown => "markdown",
56            Self::CodeClimate => "codeclimate",
57            Self::PrCommentGithub => "pr-comment-github",
58            Self::PrCommentGitlab => "pr-comment-gitlab",
59            Self::ReviewGithub => "review-github",
60            Self::ReviewGitlab => "review-gitlab",
61            Self::Badge => "badge",
62            Self::GithubAnnotations => "github-annotations",
63            Self::GithubSummary => "github-summary",
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    const VARIANTS: [OutputFormat; 13] = [
73        OutputFormat::Human,
74        OutputFormat::Json,
75        OutputFormat::Sarif,
76        OutputFormat::Compact,
77        OutputFormat::Markdown,
78        OutputFormat::CodeClimate,
79        OutputFormat::PrCommentGithub,
80        OutputFormat::PrCommentGitlab,
81        OutputFormat::ReviewGithub,
82        OutputFormat::ReviewGitlab,
83        OutputFormat::Badge,
84        OutputFormat::GithubAnnotations,
85        OutputFormat::GithubSummary,
86    ];
87
88    /// The labels are what a note tells a user to pass, so they must be the
89    /// `--format` values clap accepts rather than prettier prose.
90    #[test]
91    fn flag_labels_are_the_format_values() {
92        let labels: Vec<&str> = VARIANTS
93            .iter()
94            .map(|variant| variant.flag_label())
95            .collect();
96        assert_eq!(
97            labels,
98            vec![
99                "human",
100                "json",
101                "sarif",
102                "compact",
103                "markdown",
104                "codeclimate",
105                "pr-comment-github",
106                "pr-comment-gitlab",
107                "review-github",
108                "review-gitlab",
109                "badge",
110                "github-annotations",
111                "github-summary",
112            ]
113        );
114    }
115
116    #[test]
117    fn default_is_human() {
118        assert!(matches!(OutputFormat::default(), OutputFormat::Human));
119    }
120
121    #[test]
122    fn debug_names_remain_stable() {
123        let names: Vec<String> = VARIANTS
124            .iter()
125            .map(|variant| format!("{variant:?}"))
126            .collect();
127        assert_eq!(
128            names,
129            vec![
130                "Human",
131                "Json",
132                "Sarif",
133                "Compact",
134                "Markdown",
135                "CodeClimate",
136                "PrCommentGithub",
137                "PrCommentGitlab",
138                "ReviewGithub",
139                "ReviewGitlab",
140                "Badge",
141                "GithubAnnotations",
142                "GithubSummary",
143            ]
144        );
145    }
146}