use crate::cases::{Case, Severity};
use crate::evidence::Evidence;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum Format {
#[default]
Human,
Json,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[must_use = "a Diagnosis is meaningless until rendered, inspected, or returned"]
pub struct Diagnosis {
pub rule_id: String,
pub likely_cause: String,
pub confidence: f32,
pub evidence: Vec<Evidence>,
pub next_steps: Vec<String>,
pub escalation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[must_use = "a Report should be rendered, inspected, or its exit_code propagated"]
pub struct Report {
pub case_name: String,
pub severity: Severity,
pub primary: Option<Diagnosis>,
#[serde(default)]
pub also_considered: Vec<Diagnosis>,
pub reproduction: String,
}
impl Report {
#[must_use = "the CLI must propagate this exit code via std::process::ExitCode"]
pub fn exit_code(&self) -> i32 {
match &self.primary {
Some(d) if d.confidence >= 0.6 => 0,
_ => 1,
}
}
pub fn render(&self, format: Format) -> String {
match format {
Format::Human => self.render_human(),
Format::Json => {
serde_json::to_string_pretty(self).unwrap_or_else(|_| String::from("{}"))
}
}
}
fn render_human(&self) -> String {
let mut out = String::new();
let _ = writeln!(out, "CASE: {}", self.case_name);
let _ = writeln!(out, "SEVERITY: {}", self.severity.as_str());
match &self.primary {
Some(d) => {
let _ = writeln!(out, "LIKELY CAUSE: {}", d.likely_cause);
let _ = writeln!(out, "CONFIDENCE: {:.2}", d.confidence);
let _ = writeln!(out, "RULE: {}", d.rule_id);
let _ = writeln!(out);
let _ = writeln!(out, "EVIDENCE:");
for e in &d.evidence {
let _ = writeln!(out, "- {}", e.message);
}
let _ = writeln!(out);
let _ = writeln!(out, "REPRODUCTION:");
let _ = writeln!(out, "{}", self.reproduction);
let _ = writeln!(out);
let _ = writeln!(out, "NEXT STEPS:");
for (i, step) in d.next_steps.iter().enumerate() {
let _ = writeln!(out, "{}. {}", i + 1, step);
}
let _ = writeln!(out);
let _ = writeln!(out, "ESCALATION NOTE:");
let _ = writeln!(out, "{}", d.escalation);
}
None => {
let _ = writeln!(out, "LIKELY CAUSE: unclassified");
let _ = writeln!(out, "CONFIDENCE: 0.00");
let _ = writeln!(out);
let _ = writeln!(
out,
"No rule matched with confidence \u{2265} 0.60. \
Inspect fixtures by hand and consider adding a new rule."
);
}
}
if !self.also_considered.is_empty() {
let _ = writeln!(out);
let _ = writeln!(out, "ALSO CONSIDERED:");
for d in &self.also_considered {
let _ = writeln!(
out,
"- {} (confidence {:.2}): {}",
d.rule_id, d.confidence, d.likely_cause
);
}
}
out
}
}
pub fn reproduction(case: &Case) -> String {
let mut out = String::new();
let _ = write!(out, "curl -X {} {}", case.request.method, case.request.url);
let mut keys: Vec<&String> = case.request.headers.keys().collect();
keys.sort();
for k in keys {
let v = &case.request.headers[k];
let _ = write!(out, " \\\n -H \"{k}: {v}\"");
}
if let Some(body) = case.request.body.as_deref() {
let escaped = body.replace('\'', "'\\''");
let _ = write!(out, " \\\n --data-raw '{escaped}'");
}
out
}