api_debug_lab/evidence.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Evidence types — the breadcrumbs a diagnosis emits to justify itself.
4//!
5//! Every [`crate::report::Diagnosis`] carries a `Vec<Evidence>`. Each
6//! [`Evidence`] is one short human-readable observation paired with an
7//! optional [`Pointer`] to the source the observation came from (a
8//! request field, a response header, a specific log line). The CLI's
9//! `explain` subcommand surfaces the pointers so a support engineer
10//! can audit a diagnosis line by line.
11//!
12//! Keeping evidence and pointers as plain owned strings (no borrowed
13//! slices, no `Cow`) makes [`crate::report::Report`] cheap to clone,
14//! `Send + Sync`, and trivially serialisable as JSON.
15
16use serde::{Deserialize, Serialize};
17
18/// One observation that supports a diagnosis.
19///
20/// `message` is the human-readable claim; `pointer`, when present,
21/// names the source the claim was derived from. Both fields are owned
22/// strings so an `Evidence` can survive being moved across rule
23/// boundaries without lifetime gymnastics.
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct Evidence {
26 /// Human-readable observation. Should be a single sentence;
27 /// rendered as a bullet point in the report's `EVIDENCE:` block.
28 pub message: String,
29
30 /// Where the observation came from. `None` is allowed for
31 /// statements derived from multiple sources at once (e.g.
32 /// "tolerance is 300 s; observed drift 4800 s") — these are
33 /// labelled `computed` in practice.
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub pointer: Option<Pointer>,
36}
37
38/// Reference to the source of an [`Evidence`].
39///
40/// `source` is a logical path into the case (`"request.headers.authorization"`,
41/// `"response.status"`, `"server.log"`, or the literal `"computed"` for
42/// values the rule produced rather than read). `line` is a 1-indexed
43/// line number into a log file when relevant.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct Pointer {
46 /// Logical source of the value, e.g. `"request.headers.authorization"`,
47 /// `"response.status"`, `"server.log"`, `"case.context.webhook.tolerance_seconds"`,
48 /// or `"computed"` for values the rule produced rather than read.
49 pub source: String,
50
51 /// Optional 1-indexed line number when `source` is a log file.
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub line: Option<u32>,
54}
55
56impl Evidence {
57 /// Construct an evidence item with no source pointer.
58 ///
59 /// Use this for derived statements that do not refer to a specific
60 /// case field — for example, summary lines computed by the rule
61 /// itself ("tolerance is 300 s; observed drift 4800 s").
62 ///
63 /// # Examples
64 ///
65 /// ```
66 /// use api_debug_lab::Evidence;
67 /// let e = Evidence::new("Response status 401 Unauthorized");
68 /// assert!(e.pointer.is_none());
69 /// ```
70 pub fn new(message: impl Into<String>) -> Self {
71 Self {
72 message: message.into(),
73 pointer: None,
74 }
75 }
76
77 /// Construct an evidence item that names a logical source but no
78 /// specific line.
79 ///
80 /// Most rule-emitted evidence uses this form. The `source` is a
81 /// logical path into the case (see [`Pointer::source`]).
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// use api_debug_lab::Evidence;
87 /// let e = Evidence::with("Authorization header absent", "request.headers.authorization");
88 /// assert_eq!(e.pointer.unwrap().source, "request.headers.authorization");
89 /// ```
90 pub fn with(message: impl Into<String>, source: impl Into<String>) -> Self {
91 Self {
92 message: message.into(),
93 pointer: Some(Pointer {
94 source: source.into(),
95 line: None,
96 }),
97 }
98 }
99
100 /// Construct an evidence item that points at a specific 1-indexed
101 /// line of a log file.
102 ///
103 /// This is the form used when a rule has identified a single log
104 /// line as the smoking gun. The CLI's `explain` subcommand renders
105 /// these as `[server.log:42] message`.
106 ///
107 /// # Examples
108 ///
109 /// ```
110 /// use api_debug_lab::Evidence;
111 /// let e = Evidence::at_line("timeout entry", "server.log", 3);
112 /// assert_eq!(e.pointer.unwrap().line, Some(3));
113 /// ```
114 pub fn at_line(message: impl Into<String>, source: impl Into<String>, line: u32) -> Self {
115 Self {
116 message: message.into(),
117 pointer: Some(Pointer {
118 source: source.into(),
119 line: Some(line),
120 }),
121 }
122 }
123}