lib

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

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 }