Skip to main content

request_shadow/
divergence.rs

1//! Structured response diff.
2
3use crate::backend::ResponseRecord;
4use crate::config::{IgnoreField, ShadowConfig};
5
6/// Per-field divergence summary. None = no diff or field was ignored.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Divergence {
9    /// `Some((primary, shadow))` when the status codes differ.
10    pub status: Option<(u16, u16)>,
11    /// `Some((added, removed, changed))` header keys.
12    pub headers: Option<HeaderDiff>,
13    /// `Some((primary_len, shadow_len, prefix_equal_bytes))` when bodies differ.
14    pub body: Option<BodyDiff>,
15}
16
17/// Differences between two response header maps.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct HeaderDiff {
20    /// Headers present in the shadow but not the primary.
21    pub added: Vec<String>,
22    /// Headers present in the primary but not the shadow.
23    pub removed: Vec<String>,
24    /// Headers present in both but with different values.
25    pub changed: Vec<String>,
26}
27
28/// Differences between two response bodies.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct BodyDiff {
31    /// Byte length of the primary body.
32    pub primary_len: usize,
33    /// Byte length of the shadow body.
34    pub shadow_len: usize,
35    /// Number of leading bytes that match exactly.
36    pub prefix_equal_bytes: usize,
37}
38
39impl Divergence {
40    /// Compute a `Divergence` from two records given the config's ignore list.
41    /// Returns `None` when every diffed field matches.
42    pub fn compare(
43        primary: &ResponseRecord,
44        shadow: &ResponseRecord,
45        config: &ShadowConfig,
46    ) -> Option<Self> {
47        let status =
48            if config.ignore.contains(&IgnoreField::Status) || primary.status == shadow.status {
49                None
50            } else {
51                Some((primary.status, shadow.status))
52            };
53
54        let headers = if config.ignore.contains(&IgnoreField::Headers) {
55            None
56        } else {
57            Self::diff_headers(primary, shadow)
58        };
59
60        let body = if config.ignore.contains(&IgnoreField::Body) || primary.body == shadow.body {
61            None
62        } else {
63            Some(BodyDiff {
64                primary_len: primary.body.len(),
65                shadow_len: shadow.body.len(),
66                prefix_equal_bytes: shared_prefix(&primary.body, &shadow.body),
67            })
68        };
69
70        if status.is_none() && headers.is_none() && body.is_none() {
71            None
72        } else {
73            Some(Self {
74                status,
75                headers,
76                body,
77            })
78        }
79    }
80
81    fn diff_headers(primary: &ResponseRecord, shadow: &ResponseRecord) -> Option<HeaderDiff> {
82        let mut added = Vec::new();
83        let mut removed = Vec::new();
84        let mut changed = Vec::new();
85
86        for (k, v) in &primary.headers {
87            match shadow.headers.get(k) {
88                None => removed.push(k.clone()),
89                Some(sv) if sv != v => changed.push(k.clone()),
90                _ => {}
91            }
92        }
93        for k in shadow.headers.keys() {
94            if !primary.headers.contains_key(k) {
95                added.push(k.clone());
96            }
97        }
98        if added.is_empty() && removed.is_empty() && changed.is_empty() {
99            None
100        } else {
101            Some(HeaderDiff {
102                added,
103                removed,
104                changed,
105            })
106        }
107    }
108}
109
110fn shared_prefix(a: &[u8], b: &[u8]) -> usize {
111    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
112}