use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffEntry {
pub endpoint: String,
pub method: String,
pub change_type: String,
pub field: String,
pub baseline: Option<serde_json::Value>,
pub target: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffReport {
pub plan_name: String,
pub baseline_url: String,
pub target_url: String,
pub total_endpoints: usize,
pub identical: usize,
pub different: usize,
pub fields_added: usize,
pub fields_removed: usize,
pub fields_modified: usize,
pub duration_secs: f64,
pub diffs: Vec<DiffEntry>,
}
impl std::fmt::Display for DiffReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "── Diff Report: {} ──", self.plan_name)?;
writeln!(f, " Baseline: {}", self.baseline_url)?;
writeln!(f, " Target: {}", self.target_url)?;
writeln!(
f,
" Endpoints: {} identical, {} different",
self.identical, self.different
)?;
writeln!(
f,
" Fields: {} added, {} removed, {} modified",
self.fields_added, self.fields_removed, self.fields_modified
)?;
writeln!(f, " Duration: {:.1}s", self.duration_secs)?;
for d in &self.diffs {
writeln!(
f,
" [{}] {} {} — {}: {:?} → {:?}",
d.change_type, d.method, d.endpoint, d.field, d.baseline, d.target
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_report_display() {
let report = DiffReport {
plan_name: "migration".into(),
baseline_url: "https://api-v1.example.com".into(),
target_url: "https://api-v2.example.com".into(),
total_endpoints: 5,
identical: 3,
different: 2,
fields_added: 3,
fields_removed: 1,
fields_modified: 2,
duration_secs: 10.0,
diffs: vec![DiffEntry {
endpoint: "/users".into(),
method: "GET".into(),
change_type: "added".into(),
field: "$.data[0].email".into(),
baseline: None,
target: Some(serde_json::json!("test@example.com")),
}],
};
let output = report.to_string();
assert!(output.contains("3 added"));
assert!(output.contains("api-v1"));
}
}