use rustev_contract::remote::{MAX_TEXT_BYTES, RemoteCode, cut_to};
use rustev_contract::run::Charge;
use rustev_core::seams::{AdapterFailure, AttemptReport, CancelAck, RemoteEnd};
pub const fn failure_class(code: RemoteCode) -> AdapterFailure {
match code {
RemoteCode::TransportUnsent
| RemoteCode::TransportInterrupted
| RemoteCode::RemoteDeadline
| RemoteCode::RemoteError => AdapterFailure::Transient,
RemoteCode::RateLimited | RemoteCode::Overloaded => AdapterFailure::Overloaded,
RemoteCode::Unauthorized
| RemoteCode::Capability
| RemoteCode::MalformedRequest
| RemoteCode::MalformedResponse
| RemoteCode::IdentityMismatch => AdapterFailure::Permanent,
RemoteCode::Cancelled => AdapterFailure::Cancelled,
}
}
pub const fn is_refusal(code: RemoteCode) -> bool {
matches!(
code,
RemoteCode::RateLimited
| RemoteCode::Overloaded
| RemoteCode::Unauthorized
| RemoteCode::Capability
| RemoteCode::MalformedRequest
)
}
pub const fn default_charge(code: RemoteCode, refusals_charged: bool) -> Charge {
match code {
RemoteCode::TransportUnsent => Charge::Observed { units: 0 },
c if is_refusal(c) && !refusals_charged => Charge::Observed { units: 0 },
_ => Charge::Unknown,
}
}
pub fn charge_for(code: RemoteCode, reported: Option<Charge>, refusals_charged: bool) -> Charge {
match (code, reported) {
(RemoteCode::TransportUnsent, _) => Charge::Observed { units: 0 },
(_, Some(c)) if c != Charge::Unknown => c,
_ => default_charge(code, refusals_charged),
}
}
pub fn failure_detail(code: RemoteCode, detail: &str) -> String {
let mut s = format!("remote:{}", code.as_str());
if !detail.is_empty() {
s.push_str(": ");
s.push_str(detail);
}
cut_to(&s, MAX_TEXT_BYTES)
}
pub fn code_of_detail(detail: &str) -> Option<RemoteCode> {
let rest = detail.strip_prefix("remote:")?;
let name = rest.split(':').next().unwrap_or(rest);
RemoteCode::ALL.into_iter().find(|c| c.as_str() == name)
}
pub const fn classify_status(status: u16) -> Option<RemoteCode> {
match status {
200..=299 => None,
401 | 403 => Some(RemoteCode::Unauthorized),
429 => Some(RemoteCode::RateLimited),
503 | 529 => Some(RemoteCode::Overloaded),
400..=499 => Some(RemoteCode::MalformedRequest),
_ => Some(RemoteCode::RemoteError),
}
}
pub const fn is_local_only(code: RemoteCode) -> bool {
matches!(
code,
RemoteCode::TransportUnsent | RemoteCode::TransportInterrupted
)
}
pub fn remote_end(code: RemoteCode, sent: bool, charge: Charge) -> RemoteEnd {
let no_complete_answer = matches!(
code,
RemoteCode::TransportInterrupted
| RemoteCode::MalformedResponse
| RemoteCode::RemoteDeadline
| RemoteCode::RemoteError
);
if sent && no_complete_answer && charge == Charge::Unknown {
RemoteEnd::PossiblyContinuing
} else {
RemoteEnd::Finished
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteFailure {
pub code: RemoteCode,
pub detail: String,
pub charge: Charge,
pub remote: RemoteEnd,
}
impl RemoteFailure {
pub fn new(
code: RemoteCode,
detail: impl Into<String>,
reported: Option<Charge>,
refusals_charged: bool,
sent: bool,
) -> Self {
let charge = charge_for(code, reported, refusals_charged);
RemoteFailure {
code,
detail: detail.into(),
charge,
remote: remote_end(code, sent, charge),
}
}
pub fn report(&self) -> AttemptReport {
AttemptReport {
result: Err((
failure_class(self.code),
failure_detail(self.code, &self.detail),
)),
charge: self.charge,
cancel: CancelAck::NotRequested,
remote: self.remote,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_code_has_one_class_and_the_table_default_charge() {
use AdapterFailure as F;
use RemoteCode as C;
let table = [
(
C::TransportUnsent,
F::Transient,
Charge::Observed { units: 0 },
),
(C::TransportInterrupted, F::Transient, Charge::Unknown),
(C::RateLimited, F::Overloaded, Charge::Observed { units: 0 }),
(C::Overloaded, F::Overloaded, Charge::Observed { units: 0 }),
(C::Unauthorized, F::Permanent, Charge::Observed { units: 0 }),
(C::Capability, F::Permanent, Charge::Observed { units: 0 }),
(
C::MalformedRequest,
F::Permanent,
Charge::Observed { units: 0 },
),
(C::MalformedResponse, F::Permanent, Charge::Unknown),
(C::IdentityMismatch, F::Permanent, Charge::Unknown),
(C::RemoteDeadline, F::Transient, Charge::Unknown),
(C::RemoteError, F::Transient, Charge::Unknown),
(C::Cancelled, F::Cancelled, Charge::Unknown),
];
assert_eq!(table.len(), C::ALL.len());
for (code, class, charge) in table {
assert_eq!(failure_class(code), class, "{code:?}");
assert_eq!(default_charge(code, false), charge, "{code:?}");
let charged = default_charge(code, true);
if code == C::TransportUnsent {
assert_eq!(charged, Charge::Observed { units: 0 });
} else {
assert_eq!(charged, Charge::Unknown, "{code:?}");
}
}
}
#[test]
fn a_reported_charge_wins_except_for_an_unsent_request() {
let seven = Charge::Observed { units: 7 };
assert_eq!(
charge_for(RemoteCode::RemoteError, Some(seven), true),
seven
);
assert_eq!(
charge_for(RemoteCode::RateLimited, Some(Charge::Unknown), false),
Charge::Observed { units: 0 }
);
assert_eq!(
charge_for(RemoteCode::TransportUnsent, Some(seven), true),
Charge::Observed { units: 0 }
);
}
#[test]
fn statuses_map_to_codes() {
let cases = [
(200, None),
(204, None),
(400, Some(RemoteCode::MalformedRequest)),
(401, Some(RemoteCode::Unauthorized)),
(403, Some(RemoteCode::Unauthorized)),
(404, Some(RemoteCode::MalformedRequest)),
(413, Some(RemoteCode::MalformedRequest)),
(422, Some(RemoteCode::MalformedRequest)),
(429, Some(RemoteCode::RateLimited)),
(500, Some(RemoteCode::RemoteError)),
(502, Some(RemoteCode::RemoteError)),
(503, Some(RemoteCode::Overloaded)),
(529, Some(RemoteCode::Overloaded)),
(302, Some(RemoteCode::RemoteError)),
(100, Some(RemoteCode::RemoteError)),
(999, Some(RemoteCode::RemoteError)),
];
for (s, c) in cases {
assert_eq!(classify_status(s), c, "{s}");
}
}
#[test]
fn details_carry_the_code_within_256_bytes() {
for c in RemoteCode::ALL {
let d = failure_detail(c, &"x".repeat(1000));
assert!(d.len() <= 256);
assert!(d.starts_with(&format!("remote:{}", c.as_str())));
assert_eq!(code_of_detail(&d), Some(c));
}
assert_eq!(
failure_detail(RemoteCode::Capability, ""),
"remote:capability"
);
assert_eq!(
code_of_detail("remote:capability"),
Some(RemoteCode::Capability)
);
assert_eq!(code_of_detail("no rule matched"), None);
}
#[test]
fn possibly_continuing_only_when_sent_and_unanswered() {
let u = Charge::Unknown;
let pc = RemoteEnd::PossiblyContinuing;
assert_eq!(remote_end(RemoteCode::TransportInterrupted, true, u), pc);
assert_eq!(remote_end(RemoteCode::MalformedResponse, true, u), pc);
assert_eq!(
remote_end(
RemoteCode::MalformedResponse,
true,
Charge::Observed { units: 1 }
),
RemoteEnd::Finished
);
assert_eq!(
remote_end(RemoteCode::TransportInterrupted, false, u),
RemoteEnd::Finished
);
assert_eq!(remote_end(RemoteCode::RemoteError, true, u), pc);
assert_eq!(remote_end(RemoteCode::RemoteDeadline, true, u), pc);
assert_eq!(
remote_end(
RemoteCode::RemoteDeadline,
true,
Charge::Observed { units: 1 }
),
RemoteEnd::Finished
);
assert_eq!(
remote_end(RemoteCode::Overloaded, true, u),
RemoteEnd::Finished
);
assert!(is_local_only(RemoteCode::TransportUnsent));
assert!(is_local_only(RemoteCode::TransportInterrupted));
assert!(!is_local_only(RemoteCode::RemoteError));
}
}