Skip to main content

cloud_sdk_testkit/
recording.rs

1//! Bounded, payload-free request observations for dynamic scenarios.
2
3use core::sync::atomic::{AtomicUsize, Ordering};
4
5use cloud_sdk::Method;
6use cloud_sdk::transport::{StatusCode, TransportRequest};
7
8/// Maximum caller-owned record slots accepted by one dynamic mock.
9pub const MAX_DYNAMIC_RECORDS: usize = 1_024;
10
11/// Payload-free HTTP method classification.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum RecordedMethod {
14    /// GET.
15    Get,
16    /// POST.
17    Post,
18    /// PUT.
19    Put,
20    /// DELETE.
21    Delete,
22    /// PATCH.
23    Patch,
24    /// HEAD.
25    Head,
26    /// OPTIONS.
27    Options,
28    /// A validated provider extension method. Its token is not retained.
29    Extension,
30}
31
32impl RecordedMethod {
33    pub(crate) fn from_method(method: Method) -> Self {
34        match method {
35            Method::Get => Self::Get,
36            Method::Post => Self::Post,
37            Method::Put => Self::Put,
38            Method::Delete => Self::Delete,
39            Method::Patch => Self::Patch,
40            Method::Head => Self::Head,
41            Method::Options => Self::Options,
42            _ => Self::Extension,
43        }
44    }
45
46    const fn encode(self) -> usize {
47        match self {
48            Self::Get => 0,
49            Self::Post => 1,
50            Self::Put => 2,
51            Self::Delete => 3,
52            Self::Patch => 4,
53            Self::Head => 5,
54            Self::Options => 6,
55            Self::Extension => 7,
56        }
57    }
58
59    const fn decode(value: usize) -> Self {
60        match value {
61            0 => Self::Get,
62            1 => Self::Post,
63            2 => Self::Put,
64            3 => Self::Delete,
65            4 => Self::Patch,
66            5 => Self::Head,
67            6 => Self::Options,
68            _ => Self::Extension,
69        }
70    }
71}
72
73/// One committed request observation without request or response payloads.
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct RecordedRequest {
76    sequence: usize,
77    method: RecordedMethod,
78    target_len: usize,
79    body_len: usize,
80    header_count: usize,
81    status: StatusCode,
82}
83
84impl RecordedRequest {
85    /// Zero-based successful request sequence.
86    #[must_use]
87    pub const fn sequence(self) -> usize {
88        self.sequence
89    }
90
91    /// Payload-free method classification.
92    #[must_use]
93    pub const fn method(self) -> RecordedMethod {
94        self.method
95    }
96
97    /// Encoded target length; target bytes are never retained.
98    #[must_use]
99    pub const fn target_len(self) -> usize {
100        self.target_len
101    }
102
103    /// Request body length; body bytes are never retained.
104    #[must_use]
105    pub const fn body_len(self) -> usize {
106        self.body_len
107    }
108
109    /// Ordered request-header count; names and values are never retained.
110    #[must_use]
111    pub const fn header_count(self) -> usize {
112        self.header_count
113    }
114
115    /// Committed response status.
116    #[must_use]
117    pub const fn status(self) -> StatusCode {
118        self.status
119    }
120}
121
122/// Caller-owned atomic slot for one committed request observation.
123pub struct RequestRecordSlot {
124    ready: AtomicUsize,
125    sequence: AtomicUsize,
126    method: AtomicUsize,
127    target_len: AtomicUsize,
128    body_len: AtomicUsize,
129    header_count: AtomicUsize,
130    status: AtomicUsize,
131}
132
133impl RequestRecordSlot {
134    /// Creates an empty reusable slot.
135    #[must_use]
136    pub const fn new() -> Self {
137        Self {
138            ready: AtomicUsize::new(0),
139            sequence: AtomicUsize::new(0),
140            method: AtomicUsize::new(0),
141            target_len: AtomicUsize::new(0),
142            body_len: AtomicUsize::new(0),
143            header_count: AtomicUsize::new(0),
144            status: AtomicUsize::new(0),
145        }
146    }
147
148    /// Returns the committed observation, if any.
149    #[must_use]
150    pub fn get(&self) -> Option<RecordedRequest> {
151        if self.ready.load(Ordering::Acquire) == 0 {
152            return None;
153        }
154        let status = u16::try_from(self.status.load(Ordering::Relaxed)).ok()?;
155        Some(RecordedRequest {
156            sequence: self.sequence.load(Ordering::Relaxed),
157            method: RecordedMethod::decode(self.method.load(Ordering::Relaxed)),
158            target_len: self.target_len.load(Ordering::Relaxed),
159            body_len: self.body_len.load(Ordering::Relaxed),
160            header_count: self.header_count.load(Ordering::Relaxed),
161            status: StatusCode::new(status)?,
162        })
163    }
164
165    pub(crate) fn is_empty(&self) -> bool {
166        self.ready.load(Ordering::Acquire) == 0
167    }
168
169    pub(crate) fn commit(
170        &self,
171        sequence: usize,
172        request: TransportRequest<'_>,
173        status: StatusCode,
174    ) {
175        self.sequence.store(sequence, Ordering::Relaxed);
176        self.method.store(
177            RecordedMethod::from_method(request.method()).encode(),
178            Ordering::Relaxed,
179        );
180        self.target_len
181            .store(request.target().len(), Ordering::Relaxed);
182        self.body_len.store(request.body().len(), Ordering::Relaxed);
183        self.header_count
184            .store(request.headers().as_slice().len(), Ordering::Relaxed);
185        self.status
186            .store(usize::from(status.get()), Ordering::Relaxed);
187        self.ready.store(1, Ordering::Release);
188    }
189}
190
191impl Default for RequestRecordSlot {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197impl core::fmt::Debug for RequestRecordSlot {
198    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199        formatter
200            .debug_tuple("RequestRecordSlot")
201            .field(&self.get())
202            .finish()
203    }
204}