use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown {label} `{value}` (expected one of: {})", allowed.join(", "))]
pub struct UnknownStatus {
pub label: &'static str,
pub value: String,
pub allowed: &'static [&'static str],
}
macro_rules! statuses {
($(
$(#[$doc:meta])*
$name:ident ($label:literal) {
$( $(#[$vdoc:meta])* $variant:ident => $wire:literal ),+ $(,)?
}
)*) => { $(
$(#[$doc])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum $name {
$( $(#[$vdoc])* $variant, )+
}
impl $name {
pub const ALL: &'static [Self] = &[ $( Self::$variant, )+ ];
#[must_use]
pub fn as_str(self) -> &'static str {
match self { $( Self::$variant => $wire, )+ }
}
const SPELLINGS: &'static [&'static str] = &[ $( $wire, )+ ];
}
impl FromStr for $name {
type Err = UnknownStatus;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
$( $wire => Ok(Self::$variant), )+
other => Err(UnknownStatus {
label: $label,
value: other.to_string(),
allowed: Self::SPELLINGS,
}),
}
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
)* };
}
statuses! {
OrderStatus("order status") {
Pending => "pending",
Ready => "ready",
Processing => "processing",
Valid => "valid",
Invalid => "invalid",
}
AuthzStatus("authorization status") {
Pending => "pending",
Valid => "valid",
Invalid => "invalid",
Deactivated => "deactivated",
Expired => "expired",
Revoked => "revoked",
}
ChallengeStatus("challenge status") {
Pending => "pending",
Processing => "processing",
Valid => "valid",
Invalid => "invalid",
}
JobStatus("job status") {
Ready => "ready",
Running => "running",
Done => "done",
Failed => "failed",
Cancelled => "cancelled",
}
UpstreamOrderStatus("upstream order status") {
Processing => "processing",
Valid => "valid",
Invalid => "invalid",
}
}
pub(crate) fn from_column<T>(value: &str) -> Result<T, sqlx::Error>
where
T: FromStr<Err = UnknownStatus>,
{
value
.parse()
.map_err(|error: UnknownStatus| sqlx::Error::Decode(Box::new(error)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_status_round_trips_through_its_stored_string() {
for status in OrderStatus::ALL {
assert_eq!(status.as_str().parse::<OrderStatus>().unwrap(), *status);
}
for status in AuthzStatus::ALL {
assert_eq!(status.as_str().parse::<AuthzStatus>().unwrap(), *status);
}
for status in ChallengeStatus::ALL {
assert_eq!(status.as_str().parse::<ChallengeStatus>().unwrap(), *status);
}
for status in JobStatus::ALL {
assert_eq!(status.as_str().parse::<JobStatus>().unwrap(), *status);
}
for status in UpstreamOrderStatus::ALL {
assert_eq!(
status.as_str().parse::<UpstreamOrderStatus>().unwrap(),
*status
);
}
}
#[test]
fn the_stored_spellings_are_the_ones_the_check_constraints_permit() {
assert_eq!(
OrderStatus::SPELLINGS,
&["pending", "ready", "processing", "valid", "invalid"]
);
assert_eq!(
AuthzStatus::SPELLINGS,
&[
"pending",
"valid",
"invalid",
"deactivated",
"expired",
"revoked"
]
);
assert_eq!(
ChallengeStatus::SPELLINGS,
&["pending", "processing", "valid", "invalid"]
);
assert_eq!(
JobStatus::SPELLINGS,
&["ready", "running", "done", "failed", "cancelled"]
);
assert_eq!(
UpstreamOrderStatus::SPELLINGS,
&["processing", "valid", "invalid"]
);
}
#[test]
fn an_unknown_status_names_itself_and_the_alternatives() {
let error = "readyy".parse::<OrderStatus>().unwrap_err();
let rendered = error.to_string();
assert!(
rendered.contains("unknown order status `readyy`"),
"{rendered}"
);
assert!(
rendered.contains("pending, ready, processing, valid, invalid"),
"{rendered}"
);
}
#[test]
fn a_status_from_another_state_machine_is_refused() {
assert!("deactivated".parse::<OrderStatus>().is_err());
assert!("ready".parse::<AuthzStatus>().is_err());
assert!("deactivated".parse::<ChallengeStatus>().is_err());
assert!("deactivated".parse::<AuthzStatus>().is_ok());
assert!("pending".parse::<JobStatus>().is_err());
assert!("processing".parse::<JobStatus>().is_err());
assert!("running".parse::<UpstreamOrderStatus>().is_err());
assert!("ready".parse::<UpstreamOrderStatus>().is_err());
assert!("cancelled".parse::<JobStatus>().is_ok());
assert!("processing".parse::<UpstreamOrderStatus>().is_ok());
}
#[test]
fn a_bad_column_value_is_a_decode_error_not_a_panic() {
let error = from_column::<OrderStatus>("nonsense").unwrap_err();
assert!(matches!(error, sqlx::Error::Decode(_)), "{error:?}");
}
}