1use rustev_contract::remote::{MAX_TEXT_BYTES, RemoteCode, cut_to};
7use rustev_contract::run::Charge;
8use rustev_core::seams::{AdapterFailure, AttemptReport, CancelAck, RemoteEnd};
9
10pub const fn failure_class(code: RemoteCode) -> AdapterFailure {
12 match code {
13 RemoteCode::TransportUnsent
14 | RemoteCode::TransportInterrupted
15 | RemoteCode::RemoteDeadline
16 | RemoteCode::RemoteError => AdapterFailure::Transient,
17 RemoteCode::RateLimited | RemoteCode::Overloaded => AdapterFailure::Overloaded,
18 RemoteCode::Unauthorized
19 | RemoteCode::Capability
20 | RemoteCode::MalformedRequest
21 | RemoteCode::MalformedResponse
22 | RemoteCode::IdentityMismatch => AdapterFailure::Permanent,
23 RemoteCode::Cancelled => AdapterFailure::Cancelled,
24 }
25}
26
27pub const fn is_refusal(code: RemoteCode) -> bool {
29 matches!(
30 code,
31 RemoteCode::RateLimited
32 | RemoteCode::Overloaded
33 | RemoteCode::Unauthorized
34 | RemoteCode::Capability
35 | RemoteCode::MalformedRequest
36 )
37}
38
39pub const fn default_charge(code: RemoteCode, refusals_charged: bool) -> Charge {
47 match code {
48 RemoteCode::TransportUnsent => Charge::Observed { units: 0 },
49 c if is_refusal(c) && !refusals_charged => Charge::Observed { units: 0 },
50 _ => Charge::Unknown,
51 }
52}
53
54pub fn charge_for(code: RemoteCode, reported: Option<Charge>, refusals_charged: bool) -> Charge {
57 match (code, reported) {
58 (RemoteCode::TransportUnsent, _) => Charge::Observed { units: 0 },
60 (_, Some(c)) if c != Charge::Unknown => c,
61 _ => default_charge(code, refusals_charged),
62 }
63}
64
65pub fn failure_detail(code: RemoteCode, detail: &str) -> String {
68 let mut s = format!("remote:{}", code.as_str());
69 if !detail.is_empty() {
70 s.push_str(": ");
71 s.push_str(detail);
72 }
73 cut_to(&s, MAX_TEXT_BYTES)
74}
75
76pub fn code_of_detail(detail: &str) -> Option<RemoteCode> {
78 let rest = detail.strip_prefix("remote:")?;
79 let name = rest.split(':').next().unwrap_or(rest);
80 RemoteCode::ALL.into_iter().find(|c| c.as_str() == name)
81}
82
83pub const fn classify_status(status: u16) -> Option<RemoteCode> {
90 match status {
91 200..=299 => None,
92 401 | 403 => Some(RemoteCode::Unauthorized),
93 429 => Some(RemoteCode::RateLimited),
94 503 | 529 => Some(RemoteCode::Overloaded),
95 400..=499 => Some(RemoteCode::MalformedRequest),
96 _ => Some(RemoteCode::RemoteError),
97 }
98}
99
100pub const fn is_local_only(code: RemoteCode) -> bool {
104 matches!(
105 code,
106 RemoteCode::TransportUnsent | RemoteCode::TransportInterrupted
107 )
108}
109
110pub fn remote_end(code: RemoteCode, sent: bool, charge: Charge) -> RemoteEnd {
118 let no_complete_answer = matches!(
119 code,
120 RemoteCode::TransportInterrupted
121 | RemoteCode::MalformedResponse
122 | RemoteCode::RemoteDeadline
123 | RemoteCode::RemoteError
124 );
125 if sent && no_complete_answer && charge == Charge::Unknown {
126 RemoteEnd::PossiblyContinuing
127 } else {
128 RemoteEnd::Finished
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct RemoteFailure {
135 pub code: RemoteCode,
136 pub detail: String,
138 pub charge: Charge,
139 pub remote: RemoteEnd,
140}
141
142impl RemoteFailure {
143 pub fn new(
147 code: RemoteCode,
148 detail: impl Into<String>,
149 reported: Option<Charge>,
150 refusals_charged: bool,
151 sent: bool,
152 ) -> Self {
153 let charge = charge_for(code, reported, refusals_charged);
154 RemoteFailure {
155 code,
156 detail: detail.into(),
157 charge,
158 remote: remote_end(code, sent, charge),
159 }
160 }
161
162 pub fn report(&self) -> AttemptReport {
164 AttemptReport {
165 result: Err((
166 failure_class(self.code),
167 failure_detail(self.code, &self.detail),
168 )),
169 charge: self.charge,
170 cancel: CancelAck::NotRequested,
171 remote: self.remote,
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn every_code_has_one_class_and_the_table_default_charge() {
182 use AdapterFailure as F;
183 use RemoteCode as C;
184 let table = [
185 (
186 C::TransportUnsent,
187 F::Transient,
188 Charge::Observed { units: 0 },
189 ),
190 (C::TransportInterrupted, F::Transient, Charge::Unknown),
191 (C::RateLimited, F::Overloaded, Charge::Observed { units: 0 }),
192 (C::Overloaded, F::Overloaded, Charge::Observed { units: 0 }),
193 (C::Unauthorized, F::Permanent, Charge::Observed { units: 0 }),
194 (C::Capability, F::Permanent, Charge::Observed { units: 0 }),
195 (
196 C::MalformedRequest,
197 F::Permanent,
198 Charge::Observed { units: 0 },
199 ),
200 (C::MalformedResponse, F::Permanent, Charge::Unknown),
201 (C::IdentityMismatch, F::Permanent, Charge::Unknown),
202 (C::RemoteDeadline, F::Transient, Charge::Unknown),
203 (C::RemoteError, F::Transient, Charge::Unknown),
204 (C::Cancelled, F::Cancelled, Charge::Unknown),
205 ];
206 assert_eq!(table.len(), C::ALL.len());
207 for (code, class, charge) in table {
208 assert_eq!(failure_class(code), class, "{code:?}");
209 assert_eq!(default_charge(code, false), charge, "{code:?}");
210 let charged = default_charge(code, true);
212 if code == C::TransportUnsent {
213 assert_eq!(charged, Charge::Observed { units: 0 });
214 } else {
215 assert_eq!(charged, Charge::Unknown, "{code:?}");
216 }
217 }
218 }
219
220 #[test]
221 fn a_reported_charge_wins_except_for_an_unsent_request() {
222 let seven = Charge::Observed { units: 7 };
223 assert_eq!(
224 charge_for(RemoteCode::RemoteError, Some(seven), true),
225 seven
226 );
227 assert_eq!(
228 charge_for(RemoteCode::RateLimited, Some(Charge::Unknown), false),
229 Charge::Observed { units: 0 }
230 );
231 assert_eq!(
232 charge_for(RemoteCode::TransportUnsent, Some(seven), true),
233 Charge::Observed { units: 0 }
234 );
235 }
236
237 #[test]
238 fn statuses_map_to_codes() {
239 let cases = [
240 (200, None),
241 (204, None),
242 (400, Some(RemoteCode::MalformedRequest)),
243 (401, Some(RemoteCode::Unauthorized)),
244 (403, Some(RemoteCode::Unauthorized)),
245 (404, Some(RemoteCode::MalformedRequest)),
246 (413, Some(RemoteCode::MalformedRequest)),
247 (422, Some(RemoteCode::MalformedRequest)),
248 (429, Some(RemoteCode::RateLimited)),
249 (500, Some(RemoteCode::RemoteError)),
250 (502, Some(RemoteCode::RemoteError)),
251 (503, Some(RemoteCode::Overloaded)),
252 (529, Some(RemoteCode::Overloaded)),
253 (302, Some(RemoteCode::RemoteError)),
254 (100, Some(RemoteCode::RemoteError)),
255 (999, Some(RemoteCode::RemoteError)),
256 ];
257 for (s, c) in cases {
258 assert_eq!(classify_status(s), c, "{s}");
259 }
260 }
261
262 #[test]
263 fn details_carry_the_code_within_256_bytes() {
264 for c in RemoteCode::ALL {
265 let d = failure_detail(c, &"x".repeat(1000));
266 assert!(d.len() <= 256);
267 assert!(d.starts_with(&format!("remote:{}", c.as_str())));
268 assert_eq!(code_of_detail(&d), Some(c));
269 }
270 assert_eq!(
271 failure_detail(RemoteCode::Capability, ""),
272 "remote:capability"
273 );
274 assert_eq!(
275 code_of_detail("remote:capability"),
276 Some(RemoteCode::Capability)
277 );
278 assert_eq!(code_of_detail("no rule matched"), None);
279 }
280
281 #[test]
282 fn possibly_continuing_only_when_sent_and_unanswered() {
283 let u = Charge::Unknown;
284 let pc = RemoteEnd::PossiblyContinuing;
285 assert_eq!(remote_end(RemoteCode::TransportInterrupted, true, u), pc);
286 assert_eq!(remote_end(RemoteCode::MalformedResponse, true, u), pc);
287 assert_eq!(
288 remote_end(
289 RemoteCode::MalformedResponse,
290 true,
291 Charge::Observed { units: 1 }
292 ),
293 RemoteEnd::Finished
294 );
295 assert_eq!(
296 remote_end(RemoteCode::TransportInterrupted, false, u),
297 RemoteEnd::Finished
298 );
299 assert_eq!(remote_end(RemoteCode::RemoteError, true, u), pc);
301 assert_eq!(remote_end(RemoteCode::RemoteDeadline, true, u), pc);
302 assert_eq!(
303 remote_end(
304 RemoteCode::RemoteDeadline,
305 true,
306 Charge::Observed { units: 1 }
307 ),
308 RemoteEnd::Finished
309 );
310 assert_eq!(
311 remote_end(RemoteCode::Overloaded, true, u),
312 RemoteEnd::Finished
313 );
314 assert!(is_local_only(RemoteCode::TransportUnsent));
315 assert!(is_local_only(RemoteCode::TransportInterrupted));
316 assert!(!is_local_only(RemoteCode::RemoteError));
317 }
318}