use crate::color::Tone;
use crate::verdict::Verdict;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum DetailLevel {
#[default]
Compact,
Detailed,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderOptions {
width: Option<usize>,
}
impl RenderOptions {
pub const fn new() -> Self {
Self { width: None }
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = (width > 0).then_some(width);
self
}
pub const fn width(self) -> Option<usize> {
self.width
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Confidence {
High,
Medium,
Low,
}
impl Confidence {
pub fn label(self) -> &'static str {
match self {
Self::High => "high confidence",
Self::Medium => "medium confidence",
Self::Low => "low confidence",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Trend {
Positive,
Negative,
Neutral,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Badge {
pub label: String,
pub tone: Tone,
}
impl Badge {
pub fn new(label: impl Into<String>, tone: Tone) -> Self {
Self {
label: label.into(),
tone,
}
}
pub fn quick_win() -> Self {
Self::new("QUICK WIN", Tone::Success)
}
pub fn gained() -> Self {
Self::new("GAINED", Tone::Success)
}
pub fn lost() -> Self {
Self::new("LOST", Tone::Error)
}
pub fn render(&self, console: crate::color::Console) -> String {
console.paint(self.tone, format!("[{}]", self.label))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Metric {
pub key: String,
pub value: String,
pub tone: Option<Tone>,
pub trend: Option<Trend>,
pub delta: Option<String>,
pub verdict: Option<crate::Verdict>,
}
impl Metric {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
tone: None,
trend: None,
delta: None,
verdict: None,
}
}
pub fn with_tone(mut self, tone: Tone) -> Self {
self.tone = Some(tone);
self
}
pub fn with_verdict(mut self, verdict: crate::Verdict) -> Self {
self.verdict = Some(verdict);
self
}
pub fn with_trend(mut self, trend: Trend, delta: impl Into<String>) -> Self {
self.trend = Some(trend);
self.delta = Some(delta.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Finding {
pub message: String,
pub tone: Tone,
pub location: Option<crate::location::Location>,
pub rule_id: Option<String>,
pub remedy: Option<String>,
pub confidence: Option<Confidence>,
pub badge: Option<Badge>,
}
impl Finding {
pub fn new(tone: Tone, message: impl Into<String>) -> Self {
Self {
message: message.into(),
tone,
location: None,
rule_id: None,
remedy: None,
confidence: None,
badge: None,
}
}
pub fn with_location(mut self, location: crate::location::Location) -> Self {
self.location = Some(location);
self
}
pub fn with_rule_id(mut self, rule_id: impl Into<String>) -> Self {
self.rule_id = Some(rule_id.into());
self
}
pub fn with_remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = Some(remedy.into());
self
}
pub fn with_confidence(mut self, confidence: Confidence) -> Self {
self.confidence = Some(confidence);
self
}
pub fn with_badge(mut self, badge: Badge) -> Self {
self.badge = Some(badge);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FindingGroup {
pub title: String,
pub findings: Vec<Finding>,
pub total_count: usize,
pub advisory: bool,
}
impl FindingGroup {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
findings: Vec::new(),
total_count: 0,
advisory: false,
}
}
pub fn with_advisory(mut self, advisory: bool) -> Self {
self.advisory = advisory;
self
}
pub fn with_total_count(mut self, total_count: usize) -> Self {
self.total_count = total_count.max(self.findings.len());
self
}
pub fn add_finding(mut self, finding: Finding) -> Self {
self.findings.push(finding);
self.total_count = self.total_count.max(self.findings.len());
self
}
pub(super) fn rendered_count(&self) -> usize {
self.total_count.max(self.findings.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScopeNote {
pub heading: String,
pub items: Vec<String>,
}
impl ScopeNote {
pub fn new(heading: impl Into<String>, items: Vec<String>) -> Self {
Self {
heading: heading.into(),
items,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NextStep {
pub text: String,
pub command: Option<String>,
}
impl NextStep {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
command: None,
}
}
pub fn with_command(mut self, command: impl Into<String>) -> Self {
self.command = Some(command.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Vocabulary {
DryRun,
EmptyState(String),
Baseline(String),
Truncated(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Report {
pub title: String,
pub verdict: Verdict,
pub metrics: Vec<Metric>,
pub groups: Vec<FindingGroup>,
pub scope_notes: Vec<ScopeNote>,
pub next_steps: Vec<NextStep>,
pub vocabulary_notes: Vec<Vocabulary>,
pub detail_level: DetailLevel,
pub max_compact_samples: usize,
pub top_issues_threshold: Option<usize>,
}
impl Report {
pub fn new(title: impl Into<String>, verdict: Verdict) -> Self {
Self {
title: title.into(),
verdict,
metrics: Vec::new(),
groups: Vec::new(),
scope_notes: Vec::new(),
next_steps: Vec::new(),
vocabulary_notes: Vec::new(),
detail_level: DetailLevel::Compact,
max_compact_samples: 3,
top_issues_threshold: Some(20),
}
}
pub fn with_detail_level(mut self, level: DetailLevel) -> Self {
self.detail_level = level;
self
}
pub fn add_metric(mut self, metric: Metric) -> Self {
self.metrics.push(metric);
self
}
pub fn add_group(mut self, group: FindingGroup) -> Self {
self.groups.push(group);
self
}
pub fn add_scope_note(mut self, note: ScopeNote) -> Self {
self.scope_notes.push(note);
self
}
pub fn add_vocabulary(mut self, vocab: Vocabulary) -> Self {
self.vocabulary_notes.push(vocab);
self
}
pub fn add_next_step(mut self, step: NextStep) -> Self {
self.next_steps.push(step);
self
}
}