lib

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

retired_compatibility.rs (4741B)


      1 use std::{collections::BTreeMap, fs, path::Path};
      2 
      3 use serde::Deserialize;
      4 
      5 const CONTRACT_RELATIVE: &str = "contracts/architecture/retired_compatibility.v1.toml";
      6 const CONTRACT_ID: &str = "radroots.retired_compatibility.v1";
      7 const SCHEMA_VERSION: u16 = 1;
      8 
      9 #[derive(Debug, Deserialize)]
     10 #[serde(deny_unknown_fields)]
     11 struct RetiredCompatibility {
     12     schema_version: u16,
     13     contract_id: String,
     14     status: String,
     15     retired_bridge: Vec<RetiredBridge>,
     16 }
     17 
     18 #[derive(Debug, Deserialize)]
     19 #[serde(deny_unknown_fields)]
     20 struct RetiredBridge {
     21     id: String,
     22     final_owners: Vec<String>,
     23     removal_step: u16,
     24 }
     25 
     26 pub(super) fn validate(workspace_root: &Path) -> Result<(), String> {
     27     let path = workspace_root.join(CONTRACT_RELATIVE);
     28     let raw =
     29         fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?;
     30     validate_raw(&raw).map_err(|error| format!("{CONTRACT_RELATIVE}: {error}"))
     31 }
     32 
     33 fn validate_raw(raw: &str) -> Result<(), String> {
     34     let contract = toml::from_str::<RetiredCompatibility>(raw)
     35         .map_err(|error| format!("invalid TOML: {error}"))?;
     36     if contract.schema_version != SCHEMA_VERSION
     37         || contract.contract_id != CONTRACT_ID
     38         || contract.status != "enforced"
     39     {
     40         return Err("identity or lifecycle drifted".to_owned());
     41     }
     42 
     43     let mut actual = BTreeMap::new();
     44     for bridge in contract.retired_bridge {
     45         if bridge.id.trim().is_empty() || bridge.final_owners.is_empty() {
     46             return Err("bridge ids and final owners must not be empty".to_owned());
     47         }
     48         if bridge
     49             .final_owners
     50             .windows(2)
     51             .any(|pair| pair[0] >= pair[1])
     52         {
     53             return Err(format!(
     54                 "bridge {} final owners must be sorted and unique",
     55                 bridge.id
     56             ));
     57         }
     58         if actual
     59             .insert(
     60                 bridge.id.clone(),
     61                 (bridge.final_owners, bridge.removal_step),
     62             )
     63             .is_some()
     64         {
     65             return Err(format!("bridge {} is duplicated", bridge.id));
     66         }
     67     }
     68 
     69     let expected = BTreeMap::from([
     70         (
     71             "nostrdb_runtime_adapter".to_owned(),
     72             (vec!["radroots_storage_sqlite".to_owned()], 301),
     73         ),
     74         (
     75             "radroots_authority".to_owned(),
     76             (vec!["radroots_signing".to_owned()], 313),
     77         ),
     78         (
     79             "radroots_geocoder".to_owned(),
     80             (vec!["radroots_geonames".to_owned()], 313),
     81         ),
     82         (
     83             "radroots_net".to_owned(),
     84             (vec!["radroots_transport".to_owned()], 301),
     85         ),
     86         (
     87             "radroots_nostr_connect_hidden_prelude".to_owned(),
     88             (vec!["radroots_nostr_connect".to_owned()], 313),
     89         ),
     90         (
     91             "radroots_nostr_connect_prefixed_client_bridge".to_owned(),
     92             (vec!["radroots_nostr_connect".to_owned()], 313),
     93         ),
     94         (
     95             "radroots_nostr_runtime".to_owned(),
     96             (vec!["radroots_transport_nostr".to_owned()], 301),
     97         ),
     98         (
     99             "radroots_nostr_signer".to_owned(),
    100             (
    101                 vec![
    102                     "radroots_nostr_connect".to_owned(),
    103                     "radroots_signing".to_owned(),
    104                 ],
    105                 313,
    106             ),
    107         ),
    108     ]);
    109     if actual != expected {
    110         return Err("retired bridge inventory, final owners, or removal steps drifted".to_owned());
    111     }
    112     Ok(())
    113 }
    114 
    115 #[cfg(test)]
    116 mod tests {
    117     use super::validate_raw;
    118 
    119     const CONTRACT: &str =
    120         include_str!("../../../../contracts/architecture/retired_compatibility.v1.toml");
    121 
    122     #[test]
    123     fn current_retirement_contract_is_complete() {
    124         validate_raw(CONTRACT).expect("complete retirement contract");
    125     }
    126 
    127     #[test]
    128     fn malformed_or_incomplete_retirement_contract_fails_closed() {
    129         let malformed = validate_raw("not toml = [\n").expect_err("malformed contract must fail");
    130         assert!(malformed.contains("invalid TOML"));
    131 
    132         let geocoder = r#"[[retired_bridge]]
    133 id = "radroots_geocoder"
    134 final_owners = ["radroots_geonames"]
    135 removal_step = 313
    136 
    137 "#;
    138         let incomplete = validate_raw(&CONTRACT.replace(geocoder, ""))
    139             .expect_err("missing geocoder retirement must fail");
    140         assert!(incomplete.contains("inventory"));
    141 
    142         let no_prefixed_bridge = CONTRACT.replace(
    143             "radroots_nostr_connect_prefixed_client_bridge",
    144             "radroots_nostr_connect_unknown_bridge",
    145         );
    146         let prefixed = validate_raw(&no_prefixed_bridge)
    147             .expect_err("missing prefixed-client retirement must fail");
    148         assert!(prefixed.contains("inventory"));
    149     }
    150 }