Skip to main content

covguard_output_features/
lib.rs

1//! Shared output feature-flag contracts for covguard rendering.
2//!
3//! This crate is intentionally tiny so it can be used as a stable interoperability
4//! boundary by callers that only need output budget configuration.
5
6use serde::{Deserialize, Serialize};
7
8use covguard_render::{DEFAULT_MAX_ANNOTATIONS, DEFAULT_MAX_LINES, DEFAULT_MAX_SARIF_RESULTS};
9use covguard_types::Truncation;
10
11/// Partial output configuration from external sources (config / CLI overrides).
12#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
13pub struct OutputFeatureConfig {
14    /// Optional maximum number of uncovered lines rendered in markdown.
15    #[serde(default)]
16    pub max_markdown_lines: Option<usize>,
17    /// Optional maximum number of GitHub annotations to emit.
18    #[serde(default)]
19    pub max_annotations: Option<usize>,
20    /// Optional maximum number of SARIF results to emit.
21    #[serde(default)]
22    pub max_sarif_results: Option<usize>,
23}
24
25impl OutputFeatureConfig {
26    /// Materialize this partial configuration over base flags.
27    pub fn materialize(self, base: OutputFeatureFlags) -> OutputFeatureFlags {
28        OutputFeatureFlags {
29            max_markdown_lines: self.max_markdown_lines.unwrap_or(base.max_markdown_lines),
30            max_annotations: self.max_annotations.unwrap_or(base.max_annotations),
31            max_sarif_results: self.max_sarif_results.unwrap_or(base.max_sarif_results),
32        }
33    }
34}
35
36/// Domain-level feature flags for rendering output.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct OutputFeatureFlags {
39    /// Maximum number of uncovered lines rendered in markdown.
40    pub max_markdown_lines: usize,
41    /// Maximum number of GitHub annotations to emit.
42    pub max_annotations: usize,
43    /// Maximum number of SARIF results to emit.
44    pub max_sarif_results: usize,
45}
46
47impl Default for OutputFeatureFlags {
48    fn default() -> Self {
49        Self {
50            max_markdown_lines: DEFAULT_MAX_LINES,
51            max_annotations: DEFAULT_MAX_ANNOTATIONS,
52            max_sarif_results: DEFAULT_MAX_SARIF_RESULTS,
53        }
54    }
55}
56
57/// Backward-compatible constant aliases for output budgets.
58pub const DEFAULT_ANNOTATION_LIMIT: usize = DEFAULT_MAX_ANNOTATIONS;
59pub const DEFAULT_MARKDOWN_LINES: usize = DEFAULT_MAX_LINES;
60pub const DEFAULT_SARIF_RESULTS: usize = DEFAULT_MAX_SARIF_RESULTS;
61
62/// Truncate findings with optional max cap and return truncation metadata.
63pub fn truncate_findings<T>(findings: Vec<T>, max: Option<usize>) -> (Vec<T>, Option<Truncation>) {
64    if let Some(max) = max {
65        let total = findings.len();
66        if total > max {
67            let truncated = findings.into_iter().take(max).collect();
68            let trunc = Truncation {
69                findings_truncated: true,
70                shown: max as u32,
71                total: total as u32,
72            };
73            (truncated, Some(trunc))
74        } else {
75            (findings, None)
76        }
77    } else {
78        (findings, None)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use covguard_types::Finding;
86
87    #[test]
88    fn test_default_feature_flags() {
89        let flags = OutputFeatureFlags::default();
90        assert_eq!(flags.max_markdown_lines, DEFAULT_MARKDOWN_LINES);
91        assert_eq!(flags.max_annotations, DEFAULT_ANNOTATION_LIMIT);
92        assert_eq!(flags.max_sarif_results, DEFAULT_SARIF_RESULTS);
93    }
94
95    #[test]
96    fn test_output_feature_config_materializes_defaults() {
97        let base = OutputFeatureFlags::default();
98        let config = OutputFeatureConfig {
99            max_markdown_lines: Some(3),
100            max_annotations: None,
101            max_sarif_results: Some(5),
102        };
103        let materialized = config.materialize(base);
104
105        assert_eq!(materialized.max_markdown_lines, 3);
106        assert_eq!(materialized.max_annotations, DEFAULT_ANNOTATION_LIMIT);
107        assert_eq!(materialized.max_sarif_results, 5);
108    }
109
110    #[test]
111    fn test_output_feature_config_full_passthrough() {
112        let base = OutputFeatureFlags::default();
113        let config = OutputFeatureConfig {
114            max_markdown_lines: Some(11),
115            max_annotations: Some(22),
116            max_sarif_results: Some(33),
117        };
118        let materialized = config.materialize(base);
119
120        assert_eq!(materialized.max_markdown_lines, 11);
121        assert_eq!(materialized.max_annotations, 22);
122        assert_eq!(materialized.max_sarif_results, 33);
123    }
124
125    #[test]
126    fn test_truncate_findings_caps_results() {
127        let findings = vec![
128            Finding::uncovered_line("src/lib.rs", 1, 0),
129            Finding::uncovered_line("src/lib.rs", 2, 0),
130        ];
131        let (truncated, trunc) = truncate_findings(findings.clone(), Some(1));
132
133        assert_eq!(truncated.len(), 1);
134        assert!(trunc.is_some());
135        let trunc = trunc.expect("truncation metadata");
136        assert!(trunc.findings_truncated);
137        assert_eq!(trunc.shown, 1);
138        assert_eq!(trunc.total, findings.len() as u32);
139    }
140
141    #[test]
142    fn test_truncate_findings_passthrough_when_under_limit() {
143        let findings = vec![
144            Finding::uncovered_line("src/lib.rs", 1, 0),
145            Finding::uncovered_line("src/lib.rs", 2, 0),
146        ];
147        let (truncated, trunc) = truncate_findings(findings.clone(), Some(5));
148
149        assert_eq!(truncated.len(), findings.len());
150        assert!(trunc.is_none());
151    }
152}