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 }