dstar_gateway_core/validator/
sink.rs

1//! `DiagnosticSink` trait and three concrete implementations.
2
3use super::diagnostic::Diagnostic;
4
5/// Sink for [`Diagnostic`] events emitted by lenient parsers.
6///
7/// Three concrete impls ship in this crate ([`NullSink`], [`VecSink`],
8/// [`TracingSink`]); consumers can write their own to drive metrics,
9/// alerting, strict-mode rejection, etc.
10pub trait DiagnosticSink {
11    /// Record a diagnostic.
12    fn record(&mut self, diagnostic: Diagnostic);
13}
14
15/// Discards every diagnostic. Default for tests and pure-codec
16/// callers that don't want to track parser observations.
17#[derive(Debug, Default, Clone, Copy)]
18pub struct NullSink;
19
20impl DiagnosticSink for NullSink {
21    fn record(&mut self, _: Diagnostic) {}
22}
23
24/// Captures diagnostics into an in-memory `Vec`. Used by tests and
25/// by `Session::diagnostics()` (the user-facing accessor).
26#[derive(Debug, Default, Clone)]
27pub struct VecSink {
28    diagnostics: Vec<Diagnostic>,
29}
30
31impl VecSink {
32    /// Number of recorded diagnostics.
33    #[must_use]
34    pub const fn len(&self) -> usize {
35        self.diagnostics.len()
36    }
37
38    /// True if no diagnostics have been recorded.
39    #[must_use]
40    pub const fn is_empty(&self) -> bool {
41        self.diagnostics.is_empty()
42    }
43
44    /// Drain all recorded diagnostics, leaving the sink empty.
45    pub fn drain(&mut self) -> impl Iterator<Item = Diagnostic> + '_ {
46        self.diagnostics.drain(..)
47    }
48}
49
50impl DiagnosticSink for VecSink {
51    fn record(&mut self, d: Diagnostic) {
52        self.diagnostics.push(d);
53    }
54}
55
56/// Routes every diagnostic to a `tracing::warn!` event.
57///
58/// Default sink for the `dstar-gateway` shell crate.
59#[derive(Debug, Default, Clone, Copy)]
60pub struct TracingSink;
61
62impl DiagnosticSink for TracingSink {
63    fn record(&mut self, diagnostic: Diagnostic) {
64        tracing::warn!(?diagnostic, "dstar diagnostic recorded");
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
72
73    const PEER1: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 20001);
74    const PEER2: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 20002);
75
76    #[test]
77    fn null_sink_compiles_and_records_nothing() {
78        let mut sink = NullSink;
79        let d = Diagnostic::DuplicateLink1Ack { peer: PEER1 };
80        sink.record(d);
81    }
82
83    #[test]
84    fn vec_sink_collects_diagnostics() {
85        let mut sink = VecSink::default();
86        assert!(sink.is_empty());
87        sink.record(Diagnostic::DuplicateLink1Ack { peer: PEER1 });
88        sink.record(Diagnostic::DuplicateLink1Ack { peer: PEER2 });
89        assert_eq!(sink.len(), 2);
90        assert_eq!(sink.drain().count(), 2);
91        assert!(sink.is_empty());
92    }
93
94    #[test]
95    fn tracing_sink_can_be_invoked() {
96        let mut sink = TracingSink;
97        sink.record(Diagnostic::DuplicateLink1Ack { peer: PEER1 });
98    }
99}