1#![deny(clippy::all)]
8#![warn(clippy::pedantic)]
9#![deny(missing_docs)]
10
11use comfy_table::{Cell, Color, Table};
12use parlov_core::{OracleResult, OracleVerdict, Severity};
13
14#[must_use]
20pub fn render_table(result: &OracleResult) -> String {
21 let mut table = Table::new();
22 table.set_header(vec!["Oracle", "Verdict", "Severity", "Evidence"]);
23
24 let verdict_cell = verdict_cell(result.verdict);
25 let severity_cell = severity_cell(result.severity.as_ref());
26 let oracle_label = format!("{:?}", result.class);
27
28 add_evidence_rows(&mut table, &oracle_label, verdict_cell, severity_cell, &result.evidence);
29 add_metadata_rows(&mut table, result);
30
31 table.to_string()
32}
33
34fn add_evidence_rows(
35 table: &mut Table,
36 oracle_label: &str,
37 verdict_cell: Cell,
38 severity_cell: Cell,
39 evidence: &[String],
40) {
41 if evidence.is_empty() {
42 table.add_row(vec![
43 Cell::new(oracle_label),
44 verdict_cell,
45 severity_cell,
46 Cell::new("—"),
47 ]);
48 return;
49 }
50
51 for (i, ev) in evidence.iter().enumerate() {
52 if i == 0 {
53 table.add_row(vec![
54 Cell::new(oracle_label),
55 verdict_cell.clone(),
56 severity_cell.clone(),
57 Cell::new(ev.as_str()),
58 ]);
59 } else {
60 table.add_row(vec![
61 Cell::new(""),
62 Cell::new(""),
63 Cell::new(""),
64 Cell::new(ev.as_str()),
65 ]);
66 }
67 }
68}
69
70fn add_metadata_rows(table: &mut Table, result: &OracleResult) {
71 if let Some(label) = &result.label {
72 add_detail_row(table, "Label", label);
73 }
74 if let Some(leaks) = &result.leaks {
75 add_detail_row(table, "Leaks", leaks);
76 }
77 if let Some(rfc_basis) = &result.rfc_basis {
78 add_detail_row(table, "RFC Basis", rfc_basis);
79 }
80}
81
82fn add_detail_row(table: &mut Table, key: &str, value: &str) {
83 table.add_row(vec![
84 Cell::new(""),
85 Cell::new(""),
86 Cell::new(key),
87 Cell::new(value),
88 ]);
89}
90
91pub fn render_json(result: &OracleResult) -> Result<String, serde_json::Error> {
98 serde_json::to_string_pretty(result)
99}
100
101fn verdict_cell(verdict: OracleVerdict) -> Cell {
102 let (label, color) = match verdict {
103 OracleVerdict::Confirmed => ("Confirmed", Color::Red),
104 OracleVerdict::Likely => ("Likely", Color::Yellow),
105 OracleVerdict::Inconclusive => ("Inconclusive", Color::Blue),
106 OracleVerdict::NotPresent => ("NotPresent", Color::Green),
107 };
108 Cell::new(label).fg(color)
109}
110
111fn severity_cell(severity: Option<&Severity>) -> Cell {
112 match severity {
113 Some(Severity::High) => Cell::new("High").fg(Color::Red),
114 Some(Severity::Medium) => Cell::new("Medium").fg(Color::Yellow),
115 Some(Severity::Low) => Cell::new("Low").fg(Color::Cyan),
116 None => Cell::new("—"),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use parlov_core::{OracleClass, OracleVerdict};
124
125 fn confirmed_with_metadata() -> OracleResult {
126 OracleResult {
127 class: OracleClass::Existence,
128 verdict: OracleVerdict::Confirmed,
129 evidence: vec!["403 (baseline) vs 404 (probe)".into()],
130 severity: Some(Severity::High),
131 label: Some("Authorization-based differential".into()),
132 leaks: Some("Resource existence confirmed".into()),
133 rfc_basis: Some("RFC 9110 §15.5.4".into()),
134 }
135 }
136
137 fn not_present_no_metadata() -> OracleResult {
138 OracleResult {
139 class: OracleClass::Existence,
140 verdict: OracleVerdict::NotPresent,
141 evidence: vec!["404 (baseline) vs 404 (probe)".into()],
142 severity: None,
143 label: None,
144 leaks: None,
145 rfc_basis: None,
146 }
147 }
148
149 #[test]
150 fn table_includes_label_when_present() {
151 let table = render_table(&confirmed_with_metadata());
152 assert!(table.contains("Authorization-based differential"));
153 }
154
155 #[test]
156 fn table_includes_leaks_when_present() {
157 let table = render_table(&confirmed_with_metadata());
158 assert!(table.contains("Resource existence confirmed"));
159 }
160
161 #[test]
162 fn table_includes_rfc_basis_when_present() {
163 let table = render_table(&confirmed_with_metadata());
164 assert!(table.contains("RFC 9110 §15.5.4"));
165 }
166
167 #[test]
168 fn table_omits_label_row_when_none() {
169 let table = render_table(¬_present_no_metadata());
170 assert!(!table.contains("Label"));
171 }
172
173 #[test]
174 fn table_omits_leaks_row_when_none() {
175 let table = render_table(¬_present_no_metadata());
176 assert!(!table.contains("Leaks"));
177 }
178
179 #[test]
180 fn table_omits_rfc_basis_row_when_none() {
181 let table = render_table(¬_present_no_metadata());
182 assert!(!table.contains("RFC Basis"));
183 }
184
185 #[test]
186 fn json_omits_none_metadata_fields() {
187 let result = not_present_no_metadata();
188 let json = render_json(&result).expect("serialization failed");
189 assert!(!json.contains("label"));
190 assert!(!json.contains("leaks"));
191 assert!(!json.contains("rfc_basis"));
192 }
193
194 #[test]
195 fn json_includes_some_metadata_fields() {
196 let result = confirmed_with_metadata();
197 let json = render_json(&result).expect("serialization failed");
198 assert!(json.contains("\"label\""));
199 assert!(json.contains("\"leaks\""));
200 assert!(json.contains("\"rfc_basis\""));
201 }
202}