lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

outcome.rs (12783B)


      1 //! Normalized transport operation outcomes.
      2 
      3 use crate::target::TargetFingerprint;
      4 use alloc::string::String;
      5 
      6 /// Maximum encoded normalized outcome code length.
      7 pub const DELIVERY_OUTCOME_CODE_MAX_BYTES: usize = 64;
      8 /// Maximum encoded normalized outcome message length.
      9 pub const DELIVERY_OUTCOME_MESSAGE_MAX_BYTES: usize = 1_024;
     10 
     11 /// Target-local result of one bounded fetch attempt.
     12 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     13 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     14 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     15 pub enum FetchTargetState {
     16     /// The target reached its current end without error.
     17     Complete,
     18     /// The target produced some results but did not reach its current end.
     19     Partial,
     20     /// The target was not available for this operation.
     21     Unavailable,
     22     /// The attempt failed and a caller may choose to retry.
     23     FailedRetryable,
     24     /// The attempt failed and retrying the same request is not useful.
     25     FailedTerminal,
     26     /// Work for this target stopped because the operation was cancelled.
     27     Cancelled,
     28 }
     29 
     30 impl FetchTargetState {
     31     /// Whether a caller may choose to retry this target.
     32     pub const fn is_retryable(self) -> bool {
     33         matches!(
     34             self,
     35             Self::Partial | Self::Unavailable | Self::FailedRetryable
     36         )
     37     }
     38 
     39     /// Whether this target reached a terminal state for the current request.
     40     pub const fn is_terminal(self) -> bool {
     41         matches!(self, Self::Complete | Self::FailedTerminal)
     42     }
     43 }
     44 
     45 /// Explicit result for one requested source target.
     46 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     47 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     48 #[derive(Clone, Debug, Eq, PartialEq)]
     49 pub struct FetchTargetOutcome {
     50     target: TargetFingerprint,
     51     state: FetchTargetState,
     52     message: Option<String>,
     53 }
     54 
     55 impl FetchTargetOutcome {
     56     /// Creates a target-specific normalized outcome.
     57     pub const fn new(target: TargetFingerprint, state: FetchTargetState) -> Self {
     58         Self {
     59             target,
     60             state,
     61             message: None,
     62         }
     63     }
     64 
     65     /// Attaches caller-safe diagnostic detail.
     66     #[must_use]
     67     pub fn with_message(mut self, message: impl Into<String>) -> Self {
     68         self.message = Some(message.into());
     69         self
     70     }
     71 
     72     /// Returns the exact requested target fingerprint.
     73     pub const fn target(&self) -> &TargetFingerprint {
     74         &self.target
     75     }
     76 
     77     /// Returns normalized state.
     78     pub const fn state(&self) -> FetchTargetState {
     79         self.state
     80     }
     81 
     82     /// Returns bounded adapter-normalized diagnostic detail.
     83     pub fn message(&self) -> Option<&str> {
     84         self.message.as_deref()
     85     }
     86 }
     87 
     88 /// Normalized result class for one delivery target.
     89 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     90 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
     91 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     92 pub enum DeliveryOutcomeKind {
     93     /// The target accepted responsibility for the event.
     94     Accepted,
     95     /// The target confirmed final delivery.
     96     Delivered,
     97     /// The target rejected the event permanently.
     98     Rejected,
     99     /// The target was temporarily unavailable.
    100     Unavailable,
    101     /// The adapter reported another normalized failure.
    102     Failed,
    103 }
    104 
    105 /// Whether a failed outcome can be retried without changing the request.
    106 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    107 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
    108 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    109 pub enum Retryability {
    110     /// Outcome is successful and retry classification does not apply.
    111     NotApplicable,
    112     /// A caller may decide to retry the same target.
    113     Retryable,
    114     /// Retrying the same target and payload is not useful.
    115     Terminal,
    116 }
    117 
    118 /// Validated normalized outcome for one delivery target.
    119 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
    120 #[derive(Clone, Debug, Eq, PartialEq)]
    121 pub struct DeliveryOutcome {
    122     kind: DeliveryOutcomeKind,
    123     retryability: Retryability,
    124     code: Option<String>,
    125     message: Option<String>,
    126 }
    127 
    128 impl DeliveryOutcome {
    129     /// The target accepted responsibility for the event.
    130     pub const fn accepted() -> Self {
    131         Self::new_success(DeliveryOutcomeKind::Accepted)
    132     }
    133 
    134     /// The target confirmed final delivery.
    135     pub const fn delivered() -> Self {
    136         Self::new_success(DeliveryOutcomeKind::Delivered)
    137     }
    138 
    139     /// The target rejected the event permanently.
    140     pub const fn rejected() -> Self {
    141         Self::new_failure(DeliveryOutcomeKind::Rejected, Retryability::Terminal)
    142     }
    143 
    144     /// The target was temporarily unavailable.
    145     pub const fn unavailable() -> Self {
    146         Self::new_failure(DeliveryOutcomeKind::Unavailable, Retryability::Retryable)
    147     }
    148 
    149     /// Creates another normalized failure with explicit retry classification.
    150     pub const fn failed(retryability: Retryability) -> Result<Self, crate::Error> {
    151         if matches!(retryability, Retryability::NotApplicable) {
    152             return Err(crate::Error::InvalidDeliveryOutcome);
    153         }
    154         Ok(Self::new_failure(DeliveryOutcomeKind::Failed, retryability))
    155     }
    156 
    157     const fn new_success(kind: DeliveryOutcomeKind) -> Self {
    158         Self {
    159             kind,
    160             retryability: Retryability::NotApplicable,
    161             code: None,
    162             message: None,
    163         }
    164     }
    165 
    166     const fn new_failure(kind: DeliveryOutcomeKind, retryability: Retryability) -> Self {
    167         Self {
    168             kind,
    169             retryability,
    170             code: None,
    171             message: None,
    172         }
    173     }
    174 
    175     /// Attaches adapter-normalized diagnostic fields.
    176     pub fn with_detail(
    177         mut self,
    178         code: impl Into<String>,
    179         message: impl Into<String>,
    180     ) -> Result<Self, crate::Error> {
    181         let code = code.into();
    182         let message = message.into();
    183         validate_delivery_detail(code.as_str(), message.as_str())?;
    184         self.code = Some(code);
    185         self.message = Some(message);
    186         Ok(self)
    187     }
    188 
    189     /// Returns the normalized result kind.
    190     pub const fn kind(&self) -> DeliveryOutcomeKind {
    191         self.kind
    192     }
    193 
    194     /// Returns the explicit retry classification.
    195     pub const fn retryability(&self) -> Retryability {
    196         self.retryability
    197     }
    198 
    199     /// Whether this outcome satisfies the requested success class.
    200     pub const fn satisfies(&self, class: crate::policy::SatisfactionClass) -> bool {
    201         match class {
    202             crate::policy::SatisfactionClass::Accepted => matches!(
    203                 self.kind,
    204                 DeliveryOutcomeKind::Accepted | DeliveryOutcomeKind::Delivered
    205             ),
    206             crate::policy::SatisfactionClass::Delivered => {
    207                 matches!(self.kind, DeliveryOutcomeKind::Delivered)
    208             }
    209         }
    210     }
    211 
    212     /// Whether the same target and payload may be retried.
    213     pub const fn is_retryable(&self) -> bool {
    214         matches!(self.retryability, Retryability::Retryable)
    215     }
    216 
    217     /// Whether the failure is terminal for the same target and payload.
    218     pub const fn is_terminal(&self) -> bool {
    219         matches!(self.retryability, Retryability::Terminal)
    220     }
    221 
    222     /// Returns the adapter-normalized code.
    223     pub fn code(&self) -> Option<&str> {
    224         self.code.as_deref()
    225     }
    226 
    227     /// Returns caller-safe diagnostic detail.
    228     pub fn message(&self) -> Option<&str> {
    229         self.message.as_deref()
    230     }
    231 
    232     pub(crate) fn validate(&self) -> Result<(), crate::Error> {
    233         let valid = match self.kind {
    234             DeliveryOutcomeKind::Accepted | DeliveryOutcomeKind::Delivered => {
    235                 matches!(self.retryability, Retryability::NotApplicable)
    236             }
    237             DeliveryOutcomeKind::Rejected => matches!(self.retryability, Retryability::Terminal),
    238             DeliveryOutcomeKind::Unavailable => {
    239                 matches!(self.retryability, Retryability::Retryable)
    240             }
    241             DeliveryOutcomeKind::Failed => {
    242                 !matches!(self.retryability, Retryability::NotApplicable)
    243             }
    244         };
    245         if !valid {
    246             return Err(crate::Error::InvalidDeliveryOutcome);
    247         }
    248         match (&self.code, &self.message) {
    249             (None, None) => Ok(()),
    250             (Some(code), Some(message)) => validate_delivery_detail(code, message),
    251             (None, Some(_)) | (Some(_), None) => Err(crate::Error::InvalidDeliveryOutcome),
    252         }
    253     }
    254 }
    255 
    256 pub(crate) fn validate_delivery_code(code: &str) -> Result<(), crate::Error> {
    257     let valid = !code.is_empty()
    258         && code.len() <= DELIVERY_OUTCOME_CODE_MAX_BYTES
    259         && code.bytes().all(|byte| {
    260             byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'.')
    261         });
    262     if valid {
    263         Ok(())
    264     } else {
    265         Err(crate::Error::InvalidDeliveryOutcome)
    266     }
    267 }
    268 
    269 pub(crate) fn validate_delivery_message(message: &str) -> Result<(), crate::Error> {
    270     let valid_message = !message.is_empty()
    271         && message.len() <= DELIVERY_OUTCOME_MESSAGE_MAX_BYTES
    272         && message == message.trim()
    273         && !message.chars().any(char::is_control);
    274     if valid_message {
    275         Ok(())
    276     } else {
    277         Err(crate::Error::InvalidDeliveryOutcome)
    278     }
    279 }
    280 
    281 fn validate_delivery_detail(code: &str, message: &str) -> Result<(), crate::Error> {
    282     validate_delivery_code(code)?;
    283     validate_delivery_message(message)
    284 }
    285 
    286 #[cfg(feature = "serde")]
    287 impl<'de> serde::Deserialize<'de> for DeliveryOutcome {
    288     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    289     where
    290         D: serde::Deserializer<'de>,
    291     {
    292         #[derive(serde::Deserialize)]
    293         #[serde(deny_unknown_fields)]
    294         struct Wire {
    295             kind: DeliveryOutcomeKind,
    296             retryability: Retryability,
    297             code: Option<String>,
    298             message: Option<String>,
    299         }
    300 
    301         let wire = Wire::deserialize(deserializer)?;
    302         let outcome = Self {
    303             kind: wire.kind,
    304             retryability: wire.retryability,
    305             code: wire.code,
    306             message: wire.message,
    307         };
    308         outcome.validate().map_err(serde::de::Error::custom)?;
    309         Ok(outcome)
    310     }
    311 }
    312 
    313 #[cfg(test)]
    314 mod tests {
    315     use super::*;
    316     use crate::policy::SatisfactionClass;
    317 
    318     #[test]
    319     fn outcome_classes_cover_success_failure_retry_and_detail_branches() {
    320         for state in [
    321             FetchTargetState::Complete,
    322             FetchTargetState::Partial,
    323             FetchTargetState::Unavailable,
    324             FetchTargetState::FailedRetryable,
    325             FetchTargetState::FailedTerminal,
    326             FetchTargetState::Cancelled,
    327         ] {
    328             assert_eq!(
    329                 state.is_retryable(),
    330                 matches!(
    331                     state,
    332                     FetchTargetState::Partial
    333                         | FetchTargetState::Unavailable
    334                         | FetchTargetState::FailedRetryable
    335                 )
    336             );
    337             assert_eq!(
    338                 state.is_terminal(),
    339                 matches!(
    340                     state,
    341                     FetchTargetState::Complete | FetchTargetState::FailedTerminal
    342                 )
    343             );
    344         }
    345 
    346         let accepted = DeliveryOutcome::accepted();
    347         assert!(accepted.satisfies(SatisfactionClass::Accepted));
    348         assert!(!accepted.satisfies(SatisfactionClass::Delivered));
    349         assert_eq!(accepted.kind(), DeliveryOutcomeKind::Accepted);
    350         assert_eq!(accepted.retryability(), Retryability::NotApplicable);
    351         let delivered = DeliveryOutcome::delivered();
    352         assert!(delivered.satisfies(SatisfactionClass::Accepted));
    353         assert!(delivered.satisfies(SatisfactionClass::Delivered));
    354         assert!(DeliveryOutcome::unavailable().is_retryable());
    355         assert!(DeliveryOutcome::rejected().is_terminal());
    356         assert_eq!(
    357             DeliveryOutcome::failed(Retryability::NotApplicable),
    358             Err(crate::Error::InvalidDeliveryOutcome)
    359         );
    360         let detailed = DeliveryOutcome::failed(Retryability::Retryable)
    361             .unwrap()
    362             .with_detail("temporary_failure", "Try again")
    363             .unwrap();
    364         assert_eq!(detailed.code(), Some("temporary_failure"));
    365         assert_eq!(detailed.message(), Some("Try again"));
    366         for (code, message) in [
    367             ("", "message"),
    368             ("BAD", "message"),
    369             ("good", ""),
    370             ("good", " padded "),
    371             ("good", "line\nbreak"),
    372         ] {
    373             assert!(
    374                 DeliveryOutcome::rejected()
    375                     .with_detail(code, message)
    376                     .is_err()
    377             );
    378         }
    379     }
    380 }