cloud_sdk/transport/
delivery.rs1use core::fmt;
4
5use super::StatusCode;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum DeliveryPhase {
10 NotSent,
12 PossiblySent,
14 ResponseStarted,
16}
17
18pub trait DeliveryClassified {
23 fn delivery_phase(&self) -> DeliveryPhase;
25
26 fn observed_status(&self) -> Option<StatusCode> {
28 None
29 }
30}
31
32#[derive(Clone, Copy)]
38pub struct TransportFailure<E> {
39 phase: DeliveryPhase,
40 observed_status: Option<StatusCode>,
41 error: E,
42}
43
44impl<E> TransportFailure<E> {
45 #[must_use]
47 pub const fn not_sent(error: E) -> Self {
48 Self {
49 phase: DeliveryPhase::NotSent,
50 observed_status: None,
51 error,
52 }
53 }
54
55 #[must_use]
57 pub const fn possibly_sent(error: E) -> Self {
58 Self {
59 phase: DeliveryPhase::PossiblySent,
60 observed_status: None,
61 error,
62 }
63 }
64
65 #[must_use]
67 pub const fn response_started(error: E) -> Self {
68 Self {
69 phase: DeliveryPhase::ResponseStarted,
70 observed_status: None,
71 error,
72 }
73 }
74
75 #[must_use]
77 pub const fn response_started_with_status(status: StatusCode, error: E) -> Self {
78 Self {
79 phase: DeliveryPhase::ResponseStarted,
80 observed_status: Some(status),
81 error,
82 }
83 }
84
85 #[must_use]
87 pub const fn unknown(error: E) -> Self {
88 Self::possibly_sent(error)
89 }
90
91 #[must_use]
93 pub const fn phase(&self) -> DeliveryPhase {
94 self.phase
95 }
96
97 #[must_use]
99 pub const fn observed_status(&self) -> Option<StatusCode> {
100 self.observed_status
101 }
102
103 #[must_use]
105 pub const fn with_observed_status(mut self, status: StatusCode) -> Self {
106 self.phase = DeliveryPhase::ResponseStarted;
107 self.observed_status = Some(status);
108 self
109 }
110
111 #[must_use]
113 pub const fn error(&self) -> &E {
114 &self.error
115 }
116
117 #[must_use]
119 pub fn into_error(self) -> E {
120 self.error
121 }
122
123 #[must_use]
125 pub fn map<F>(self, transform: impl FnOnce(E) -> F) -> TransportFailure<F> {
126 TransportFailure {
127 phase: self.phase,
128 observed_status: self.observed_status,
129 error: transform(self.error),
130 }
131 }
132}
133
134impl<E> DeliveryClassified for TransportFailure<E> {
135 fn delivery_phase(&self) -> DeliveryPhase {
136 self.phase
137 }
138
139 fn observed_status(&self) -> Option<StatusCode> {
140 self.observed_status
141 }
142}
143
144impl<E: PartialEq> PartialEq for TransportFailure<E> {
145 fn eq(&self, other: &Self) -> bool {
146 self.phase == other.phase
147 && self.observed_status == other.observed_status
148 && self.error == other.error
149 }
150}
151
152impl<E: Eq> Eq for TransportFailure<E> {}
153
154impl<E> fmt::Debug for TransportFailure<E> {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 formatter
157 .debug_struct("TransportFailure")
158 .field("phase", &self.phase)
159 .field("observed_status", &self.observed_status)
160 .field("error", &"[redacted]")
161 .finish()
162 }
163}
164
165impl<E> fmt::Display for TransportFailure<E> {
166 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 match self.phase {
168 DeliveryPhase::NotSent => formatter.write_str("transport failed before delivery"),
169 DeliveryPhase::PossiblySent => {
170 formatter.write_str("transport failed with uncertain delivery")
171 }
172 DeliveryPhase::ResponseStarted => {
173 formatter.write_str("transport failed after response start")
174 }
175 }
176 }
177}
178
179impl<E> core::error::Error for TransportFailure<E> {}
180
181#[cfg(test)]
182mod tests {
183 use core::fmt::{self, Write};
184
185 use super::{DeliveryClassified, DeliveryPhase, StatusCode, TransportFailure};
186
187 struct FixedText {
188 bytes: [u8; 96],
189 len: usize,
190 }
191
192 impl FixedText {
193 const fn new() -> Self {
194 Self {
195 bytes: [0_u8; 96],
196 len: 0,
197 }
198 }
199
200 fn as_str(&self) -> &str {
201 core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
202 }
203 }
204
205 impl Write for FixedText {
206 fn write_str(&mut self, value: &str) -> fmt::Result {
207 let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
208 let output = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
209 output.copy_from_slice(value.as_bytes());
210 self.len = end;
211 Ok(())
212 }
213 }
214
215 #[test]
216 fn unknown_delivery_fails_closed_and_debug_redacts_the_error() {
217 let failure = TransportFailure::unknown("secret payload");
218 assert_eq!(failure.phase(), DeliveryPhase::PossiblySent);
219 let mut debug = FixedText::new();
220 assert!(write!(&mut debug, "{failure:?}").is_ok());
221 assert!(debug.as_str().contains("PossiblySent"));
222 assert!(!debug.as_str().contains("secret payload"));
223 }
224
225 #[test]
226 fn observed_status_survives_mapping_without_exposing_error_payload() {
227 let failure = TransportFailure::response_started_with_status(
228 StatusCode::new(401).unwrap_or(StatusCode::OK),
229 "secret payload",
230 )
231 .map(|_| 7_u8);
232 assert_eq!(failure.phase(), DeliveryPhase::ResponseStarted);
233 assert_eq!(failure.observed_status().map(StatusCode::get), Some(401));
234 assert_eq!(
235 DeliveryClassified::observed_status(&failure),
236 failure.observed_status()
237 );
238 assert_eq!(*failure.error(), 7);
239 }
240}