relay_delivery.rs (20373B)
1 #![forbid(unsafe_code)] 2 3 use serde::{Deserialize, Serialize}; 4 use serde_json::Value; 5 6 use crate::{ 7 LocalEventsError, canonical_relay_set_fingerprint, relay_url::RelayUrlValidationError, 8 relay_url::normalize_relay_urls, 9 }; 10 11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 12 #[serde(rename_all = "snake_case")] 13 pub enum RelayDeliveryState { 14 Pending, 15 Acknowledged, 16 Failed, 17 Observed, 18 } 19 20 impl RelayDeliveryState { 21 pub fn as_str(self) -> &'static str { 22 match self { 23 Self::Pending => "pending", 24 Self::Acknowledged => "acknowledged", 25 Self::Failed => "failed", 26 Self::Observed => "observed", 27 } 28 } 29 } 30 31 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 32 pub struct RelayDeliveryFailure { 33 pub relay_url: String, 34 pub error: String, 35 } 36 37 impl RelayDeliveryFailure { 38 pub fn new( 39 relay_url: impl AsRef<str>, 40 error: impl AsRef<str>, 41 ) -> Result<Self, LocalEventsError> { 42 let relay_url = normalize_relay_url_for_evidence("failed_relays.relay_url", relay_url)?; 43 let error = normalize_non_empty_text("failed_relays.error", error.as_ref())?; 44 Ok(Self { relay_url, error }) 45 } 46 } 47 48 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 49 pub struct RelayDeliveryEvidence { 50 pub state: RelayDeliveryState, 51 pub target_relays: Vec<String>, 52 pub connected_relays: Vec<String>, 53 pub acknowledged_relays: Vec<String>, 54 #[serde(default, skip_serializing_if = "Vec::is_empty")] 55 pub observed_relays: Vec<String>, 56 pub failed_relays: Vec<RelayDeliveryFailure>, 57 } 58 59 impl RelayDeliveryEvidence { 60 pub fn pending<I, S>(target_relays: I) -> Result<Self, LocalEventsError> 61 where 62 I: IntoIterator<Item = S>, 63 S: AsRef<str>, 64 { 65 Self::build( 66 RelayDeliveryState::Pending, 67 target_relays, 68 Vec::<String>::new(), 69 Vec::<String>::new(), 70 Vec::<String>::new(), 71 Vec::new(), 72 ) 73 } 74 75 pub fn acknowledged<I, S, J, T, K, U>( 76 target_relays: I, 77 connected_relays: J, 78 acknowledged_relays: K, 79 failed_relays: Vec<RelayDeliveryFailure>, 80 ) -> Result<Self, LocalEventsError> 81 where 82 I: IntoIterator<Item = S>, 83 S: AsRef<str>, 84 J: IntoIterator<Item = T>, 85 T: AsRef<str>, 86 K: IntoIterator<Item = U>, 87 U: AsRef<str>, 88 { 89 Self::build( 90 RelayDeliveryState::Acknowledged, 91 target_relays, 92 connected_relays, 93 acknowledged_relays, 94 Vec::<String>::new(), 95 failed_relays, 96 ) 97 } 98 99 pub fn observed<I, S, J, T, K, U>( 100 target_relays: I, 101 connected_relays: J, 102 observed_relays: K, 103 failed_relays: Vec<RelayDeliveryFailure>, 104 ) -> Result<Self, LocalEventsError> 105 where 106 I: IntoIterator<Item = S>, 107 S: AsRef<str>, 108 J: IntoIterator<Item = T>, 109 T: AsRef<str>, 110 K: IntoIterator<Item = U>, 111 U: AsRef<str>, 112 { 113 Self::build( 114 RelayDeliveryState::Observed, 115 target_relays, 116 connected_relays, 117 Vec::<String>::new(), 118 observed_relays, 119 failed_relays, 120 ) 121 } 122 123 pub fn failed<I, S, J, T>( 124 target_relays: I, 125 connected_relays: J, 126 failed_relays: Vec<RelayDeliveryFailure>, 127 ) -> Result<Self, LocalEventsError> 128 where 129 I: IntoIterator<Item = S>, 130 S: AsRef<str>, 131 J: IntoIterator<Item = T>, 132 T: AsRef<str>, 133 { 134 Self::build( 135 RelayDeliveryState::Failed, 136 target_relays, 137 connected_relays, 138 Vec::<String>::new(), 139 Vec::<String>::new(), 140 failed_relays, 141 ) 142 } 143 144 pub fn validate(&self) -> Result<(), LocalEventsError> { 145 validate_relay_set("target_relays", &self.target_relays, true)?; 146 validate_relay_set("connected_relays", &self.connected_relays, false)?; 147 validate_relay_set("acknowledged_relays", &self.acknowledged_relays, false)?; 148 validate_relay_set("observed_relays", &self.observed_relays, false)?; 149 for failure in &self.failed_relays { 150 let normalized = 151 normalize_relay_url_for_evidence("failed_relays.relay_url", &failure.relay_url)?; 152 if normalized != failure.relay_url { 153 return Err(invalid_evidence( 154 "failed_relays.relay_url must be normalized and deduplicated", 155 )); 156 } 157 let normalized_error = normalize_non_empty_text("failed_relays.error", &failure.error)?; 158 if normalized_error != failure.error { 159 return Err(invalid_evidence("failed_relays.error must be trimmed")); 160 } 161 } 162 match self.state { 163 RelayDeliveryState::Pending => { 164 if !self.acknowledged_relays.is_empty() 165 || !self.observed_relays.is_empty() 166 || !self.failed_relays.is_empty() 167 { 168 return Err(invalid_evidence( 169 "pending delivery evidence must not include acknowledged, observed, or failed relays", 170 )); 171 } 172 } 173 RelayDeliveryState::Acknowledged => { 174 if self.acknowledged_relays.is_empty() { 175 return Err(invalid_evidence( 176 "acknowledged delivery evidence requires acknowledged_relays", 177 )); 178 } 179 if !self.observed_relays.is_empty() { 180 return Err(invalid_evidence( 181 "acknowledged delivery evidence must not include observed_relays", 182 )); 183 } 184 } 185 RelayDeliveryState::Failed => { 186 if !self.acknowledged_relays.is_empty() 187 || !self.observed_relays.is_empty() 188 || self.failed_relays.is_empty() 189 { 190 return Err(invalid_evidence( 191 "failed delivery evidence requires failed_relays and no acknowledged or observed relays", 192 )); 193 } 194 } 195 RelayDeliveryState::Observed => { 196 if !self.acknowledged_relays.is_empty() { 197 return Err(invalid_evidence( 198 "observed delivery evidence must not include acknowledged_relays", 199 )); 200 } 201 if self.observed_relays.is_empty() && self.connected_relays.is_empty() { 202 return Err(invalid_evidence( 203 "observed delivery evidence requires connected_relays or observed_relays", 204 )); 205 } 206 } 207 } 208 Ok(()) 209 } 210 211 pub fn relay_set_fingerprint(&self) -> Option<String> { 212 canonical_relay_set_fingerprint(&self.target_relays) 213 } 214 215 pub fn to_json_value(&self) -> Result<Value, LocalEventsError> { 216 self.validate()?; 217 serde_json::to_value(self).map_err(LocalEventsError::from) 218 } 219 220 pub fn from_json_value(value: &Value) -> Result<Self, LocalEventsError> { 221 let evidence: Self = serde_json::from_value(value.clone())?; 222 evidence.validate()?; 223 Ok(evidence) 224 } 225 226 fn build<I, S, J, T, K, U, L, V>( 227 state: RelayDeliveryState, 228 target_relays: I, 229 connected_relays: J, 230 acknowledged_relays: K, 231 observed_relays: L, 232 failed_relays: Vec<RelayDeliveryFailure>, 233 ) -> Result<Self, LocalEventsError> 234 where 235 I: IntoIterator<Item = S>, 236 S: AsRef<str>, 237 J: IntoIterator<Item = T>, 238 T: AsRef<str>, 239 K: IntoIterator<Item = U>, 240 U: AsRef<str>, 241 L: IntoIterator<Item = V>, 242 V: AsRef<str>, 243 { 244 let evidence = Self { 245 state, 246 target_relays: normalize_required_relay_set("target_relays", target_relays)?, 247 connected_relays: normalize_relay_set("connected_relays", connected_relays)?, 248 acknowledged_relays: normalize_relay_set("acknowledged_relays", acknowledged_relays)?, 249 observed_relays: normalize_relay_set("observed_relays", observed_relays)?, 250 failed_relays, 251 }; 252 evidence.validate()?; 253 Ok(evidence) 254 } 255 } 256 257 fn normalize_relay_url_for_evidence( 258 field: &str, 259 value: impl AsRef<str>, 260 ) -> Result<String, LocalEventsError> { 261 crate::relay_url::normalize_relay_url(value.as_ref()).map_err(|error| relay_error(field, error)) 262 } 263 264 fn normalize_required_relay_set<I, S>( 265 field: &str, 266 values: I, 267 ) -> Result<Vec<String>, LocalEventsError> 268 where 269 I: IntoIterator<Item = S>, 270 S: AsRef<str>, 271 { 272 let relays = normalize_relay_set(field, values)?; 273 if relays.is_empty() { 274 return Err(invalid_evidence(format!("{field} must not be empty"))); 275 } 276 Ok(relays) 277 } 278 279 fn normalize_relay_set<I, S>(field: &str, values: I) -> Result<Vec<String>, LocalEventsError> 280 where 281 I: IntoIterator<Item = S>, 282 S: AsRef<str>, 283 { 284 normalize_relay_urls(values).map_err(|error| relay_error(field, error)) 285 } 286 287 fn validate_relay_set( 288 field: &str, 289 relays: &[String], 290 require_non_empty: bool, 291 ) -> Result<(), LocalEventsError> { 292 let normalized = normalize_relay_set(field, relays)?; 293 if require_non_empty && normalized.is_empty() { 294 return Err(invalid_evidence(format!("{field} must not be empty"))); 295 } 296 if normalized != relays { 297 return Err(invalid_evidence(format!( 298 "{field} must be normalized and deduplicated" 299 ))); 300 } 301 Ok(()) 302 } 303 304 fn normalize_non_empty_text(field: &str, value: &str) -> Result<String, LocalEventsError> { 305 let trimmed = value.trim(); 306 if trimmed.is_empty() { 307 return Err(invalid_evidence(format!("{field} must not be empty"))); 308 } 309 Ok(trimmed.to_owned()) 310 } 311 312 fn relay_error(field: &str, error: RelayUrlValidationError) -> LocalEventsError { 313 invalid_evidence(format!("{field}: {error}")) 314 } 315 316 fn invalid_evidence(message: impl Into<String>) -> LocalEventsError { 317 LocalEventsError::InvalidRecord(format!( 318 "invalid relay delivery evidence: {}", 319 message.into() 320 )) 321 } 322 323 #[cfg(test)] 324 mod tests { 325 use serde_json::json; 326 327 use super::*; 328 329 #[test] 330 fn state_labels_and_failure_constructor_cover_public_surface() { 331 for (state, value) in [ 332 (RelayDeliveryState::Pending, "pending"), 333 (RelayDeliveryState::Acknowledged, "acknowledged"), 334 (RelayDeliveryState::Failed, "failed"), 335 (RelayDeliveryState::Observed, "observed"), 336 ] { 337 assert_eq!(state.as_str(), value); 338 } 339 340 let failure = RelayDeliveryFailure::new(" ws://relay.test ", " connection refused ") 341 .expect("failure"); 342 assert_eq!(failure.relay_url, "ws://relay.test"); 343 assert_eq!(failure.error, "connection refused"); 344 assert_error_contains( 345 RelayDeliveryFailure::new("http://relay.test", "err"), 346 "failed_relays.relay_url", 347 ); 348 assert_error_contains(RelayDeliveryFailure::new("ws://relay.test", " "), "error"); 349 } 350 351 #[test] 352 fn constructors_validate_all_delivery_states_and_json_roundtrips() { 353 let pending = RelayDeliveryEvidence::pending(["ws://relay-a.test", "ws://relay-a.test"]) 354 .expect("pending evidence"); 355 assert_eq!(pending.state, RelayDeliveryState::Pending); 356 assert_eq!(pending.target_relays, vec!["ws://relay-a.test"]); 357 assert!(pending.relay_set_fingerprint().is_some()); 358 assert_eq!( 359 RelayDeliveryEvidence::from_json_value(&pending.to_json_value().expect("pending json")) 360 .expect("pending from json"), 361 pending 362 ); 363 364 let failure = RelayDeliveryFailure::new("ws://relay-b.test", "timeout").expect("failure"); 365 let acknowledged = RelayDeliveryEvidence::acknowledged( 366 ["ws://relay-a.test"], 367 ["ws://relay-a.test"], 368 ["ws://relay-a.test"], 369 vec![failure.clone()], 370 ) 371 .expect("acknowledged"); 372 assert_eq!(acknowledged.state, RelayDeliveryState::Acknowledged); 373 374 let observed = RelayDeliveryEvidence::observed( 375 ["ws://relay-a.test"], 376 Vec::<String>::new(), 377 ["ws://relay-b.test"], 378 vec![failure.clone()], 379 ) 380 .expect("observed"); 381 assert_eq!(observed.state, RelayDeliveryState::Observed); 382 383 let failed = RelayDeliveryEvidence::failed( 384 ["ws://relay-a.test"], 385 ["ws://relay-a.test"], 386 vec![failure], 387 ) 388 .expect("failed"); 389 assert_eq!(failed.state, RelayDeliveryState::Failed); 390 } 391 392 #[test] 393 fn validate_rejects_invalid_manual_evidence_shapes() { 394 assert_error_contains( 395 RelayDeliveryEvidence::pending(Vec::<String>::new()), 396 "target_relays", 397 ); 398 399 assert_error_contains( 400 RelayDeliveryEvidence { 401 state: RelayDeliveryState::Pending, 402 target_relays: vec!["ws://relay.test".to_owned()], 403 connected_relays: Vec::new(), 404 acknowledged_relays: vec!["ws://relay.test".to_owned()], 405 observed_relays: Vec::new(), 406 failed_relays: Vec::new(), 407 } 408 .validate(), 409 "pending delivery evidence", 410 ); 411 412 assert_error_contains( 413 RelayDeliveryEvidence { 414 state: RelayDeliveryState::Pending, 415 target_relays: vec!["ws://relay.test".to_owned()], 416 connected_relays: Vec::new(), 417 acknowledged_relays: Vec::new(), 418 observed_relays: Vec::new(), 419 failed_relays: vec![RelayDeliveryFailure { 420 relay_url: "ws://relay.test".to_owned(), 421 error: "timeout".to_owned(), 422 }], 423 } 424 .validate(), 425 "pending delivery evidence", 426 ); 427 428 assert_error_contains( 429 RelayDeliveryEvidence { 430 state: RelayDeliveryState::Acknowledged, 431 target_relays: vec!["ws://relay.test".to_owned()], 432 connected_relays: Vec::new(), 433 acknowledged_relays: Vec::new(), 434 observed_relays: Vec::new(), 435 failed_relays: Vec::new(), 436 } 437 .validate(), 438 "requires acknowledged_relays", 439 ); 440 441 assert_error_contains( 442 RelayDeliveryEvidence { 443 state: RelayDeliveryState::Acknowledged, 444 target_relays: vec!["ws://relay.test".to_owned()], 445 connected_relays: Vec::new(), 446 acknowledged_relays: vec!["ws://relay.test".to_owned()], 447 observed_relays: vec!["ws://relay.test".to_owned()], 448 failed_relays: Vec::new(), 449 } 450 .validate(), 451 "must not include observed_relays", 452 ); 453 454 assert_error_contains( 455 RelayDeliveryEvidence { 456 state: RelayDeliveryState::Failed, 457 target_relays: vec!["ws://relay.test".to_owned()], 458 connected_relays: Vec::new(), 459 acknowledged_relays: Vec::new(), 460 observed_relays: Vec::new(), 461 failed_relays: Vec::new(), 462 } 463 .validate(), 464 "failed delivery evidence", 465 ); 466 467 assert_error_contains( 468 RelayDeliveryEvidence { 469 state: RelayDeliveryState::Failed, 470 target_relays: vec!["ws://relay.test".to_owned()], 471 connected_relays: Vec::new(), 472 acknowledged_relays: Vec::new(), 473 observed_relays: vec!["ws://relay.test".to_owned()], 474 failed_relays: vec![RelayDeliveryFailure { 475 relay_url: "ws://relay.test".to_owned(), 476 error: "timeout".to_owned(), 477 }], 478 } 479 .validate(), 480 "failed delivery evidence", 481 ); 482 483 assert_error_contains( 484 RelayDeliveryEvidence { 485 state: RelayDeliveryState::Observed, 486 target_relays: vec!["ws://relay.test".to_owned()], 487 connected_relays: Vec::new(), 488 acknowledged_relays: vec!["ws://relay.test".to_owned()], 489 observed_relays: Vec::new(), 490 failed_relays: Vec::new(), 491 } 492 .validate(), 493 "must not include acknowledged_relays", 494 ); 495 496 assert_error_contains( 497 RelayDeliveryEvidence { 498 state: RelayDeliveryState::Observed, 499 target_relays: vec!["ws://relay.test".to_owned()], 500 connected_relays: Vec::new(), 501 acknowledged_relays: Vec::new(), 502 observed_relays: Vec::new(), 503 failed_relays: Vec::new(), 504 } 505 .validate(), 506 "requires connected_relays or observed_relays", 507 ); 508 } 509 510 #[test] 511 fn validate_rejects_non_normalized_relays_and_failure_text() { 512 assert_error_contains( 513 RelayDeliveryEvidence { 514 state: RelayDeliveryState::Pending, 515 target_relays: vec!["ws://relay.test".to_owned(), "ws://relay.test".to_owned()], 516 connected_relays: Vec::new(), 517 acknowledged_relays: Vec::new(), 518 observed_relays: Vec::new(), 519 failed_relays: Vec::new(), 520 } 521 .validate(), 522 "normalized and deduplicated", 523 ); 524 525 assert_error_contains( 526 RelayDeliveryEvidence { 527 state: RelayDeliveryState::Failed, 528 target_relays: vec!["ws://relay.test".to_owned()], 529 connected_relays: Vec::new(), 530 acknowledged_relays: Vec::new(), 531 observed_relays: Vec::new(), 532 failed_relays: vec![RelayDeliveryFailure { 533 relay_url: "http://relay.test".to_owned(), 534 error: "timeout".to_owned(), 535 }], 536 } 537 .validate(), 538 "failed_relays.relay_url", 539 ); 540 541 assert_error_contains( 542 RelayDeliveryEvidence { 543 state: RelayDeliveryState::Failed, 544 target_relays: vec!["ws://relay.test".to_owned()], 545 connected_relays: Vec::new(), 546 acknowledged_relays: Vec::new(), 547 observed_relays: Vec::new(), 548 failed_relays: vec![RelayDeliveryFailure { 549 relay_url: "ws://relay.test".to_owned(), 550 error: " timeout ".to_owned(), 551 }], 552 } 553 .validate(), 554 "must be trimmed", 555 ); 556 557 assert_error_contains( 558 RelayDeliveryEvidence::from_json_value(&json!({ 559 "state": "pending", 560 "target_relays": [], 561 "connected_relays": [], 562 "acknowledged_relays": [], 563 "failed_relays": [] 564 })), 565 "target_relays", 566 ); 567 568 let relay_vec = vec!["ws://relay-a.test".to_owned()]; 569 let relay_slice = relay_vec.as_slice(); 570 RelayDeliveryEvidence::acknowledged( 571 relay_vec.clone(), 572 relay_slice, 573 relay_vec.clone(), 574 Vec::new(), 575 ) 576 .expect("acknowledged from vecs and slices"); 577 assert_error_contains( 578 RelayDeliveryEvidence::observed( 579 ["http://relay.test"], 580 Vec::<String>::new(), 581 Vec::<String>::new(), 582 Vec::new(), 583 ), 584 "target_relays", 585 ); 586 } 587 588 fn assert_error_contains<T: std::fmt::Debug>( 589 result: Result<T, LocalEventsError>, 590 expected: &str, 591 ) { 592 let err = result.expect_err("expected relay delivery error"); 593 assert!( 594 err.to_string().contains(expected), 595 "expected error to contain {expected}, got {err}" 596 ); 597 } 598 }