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 }