Skip to main content

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}