package_boundary.rs (11378B)
1 use std::{collections::BTreeSet, fs, path::Path}; 2 3 #[allow(unused_imports)] 4 use radroots_transport::{ 5 DeliveryReceipt as _, DeliveryRequest as _, Error as _, EventSink as _, EventSource as _, 6 EventSubscriber as _, EventSubscription as _, FetchPage as _, FetchRequest as _, 7 SubscriptionEnd as _, SubscriptionEvent as _, SubscriptionRequest as _, Target as _, 8 TargetNetworkPolicy as _, TargetSet as _, TransportId as _, capability as _, endpoint as _, 9 error as _, outcome as _, policy as _, sink as _, source as _, target as _, 10 }; 11 12 const MANIFEST: &str = include_str!("../Cargo.toml"); 13 const EXAMPLE: &str = include_str!("../examples/host_transport.rs"); 14 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_transport.txt"); 15 const README: &str = include_str!("../README.md"); 16 const ROOT: &str = include_str!("../src/lib.rs"); 17 const SOURCE: &str = include_str!("../src/source.rs"); 18 const SINK: &str = include_str!("../src/sink.rs"); 19 const ID: &str = include_str!("../src/id.rs"); 20 const TARGET: &str = include_str!("../src/target.rs"); 21 const DEVIATIONS: &str = include_str!("../../../contracts/architecture/deviations.toml"); 22 23 #[test] 24 fn manifest_has_final_identity_features_and_required_radroots_dependencies() { 25 for required in [ 26 "name = \"radroots_transport\"", 27 "version = \"0.1.0-alpha\"", 28 "publish = [\"crates-io\"]", 29 "[lib]\nname = \"radroots_transport\"", 30 "default = [\"std\", \"serde\"]", 31 "radroots_event = { workspace = true, default-features = false }", 32 "radroots_identity = { workspace = true, default-features = false }", 33 "radroots_protocol = { workspace = true, default-features = false }", 34 ] { 35 assert!( 36 MANIFEST.contains(required), 37 "manifest is missing {required}" 38 ); 39 } 40 41 assert_eq!( 42 table_keys(MANIFEST, "[features]"), 43 BTreeSet::from(["default", "serde", "std"]) 44 ); 45 assert_eq!( 46 table_keys(MANIFEST, "[dependencies]") 47 .into_iter() 48 .filter(|dependency| dependency.starts_with("radroots_")) 49 .collect::<BTreeSet<_>>(), 50 BTreeSet::from(["radroots_event", "radroots_identity", "radroots_protocol"]) 51 ); 52 } 53 54 #[test] 55 fn crate_root_declares_the_approved_public_module_skeleton() { 56 assert!(ROOT.contains("#![doc = include_str!(\"../README.md\")]")); 57 assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); 58 assert_eq!( 59 root_declarations("pub mod "), 60 BTreeSet::from([ 61 "capability", 62 "endpoint", 63 "error", 64 "outcome", 65 "policy", 66 "sink", 67 "source", 68 "target", 69 ]) 70 ); 71 } 72 73 #[test] 74 fn package_documentation_and_reviewed_api_baseline_are_complete() { 75 for required in [ 76 "## Typical flow", 77 "## Host SPI contract", 78 "## Targets and extensible identity", 79 "## Bounds, deadlines, cancellation, and commit points", 80 "## Outcomes, partial success, and retry", 81 "## Serialization and provenance", 82 "## Security and side effects", 83 "## Features", 84 "## Intended consumers", 85 "radroots_crates_release_v1.toml", 86 "examples/host_transport.rs", 87 "indexed single-letter tag values", 88 "TargetNetworkPolicy::PrivateDevice", 89 "literal RFC1918 IPv4 or ULA IPv6 endpoints", 90 "not connection authority", 91 ] { 92 assert!(README.contains(required), "README is missing {required}"); 93 } 94 for required in [ 95 "impl EventSource for HostTransport", 96 "impl EventSubscriber for HostTransport", 97 "impl EventSink for HostTransport", 98 "fn fetch(&self, _request: FetchRequest) -> BoxFuture", 99 "_request: SubscriptionRequest", 100 "_request: DeliveryRequest", 101 "let source: &dyn EventSource", 102 "let subscriber: &dyn EventSubscriber", 103 "let sink: &dyn EventSink", 104 "drop(future)", 105 ] { 106 assert!(EXAMPLE.contains(required), "example is missing {required}"); 107 } 108 for required in [ 109 "pub mod radroots_transport::capability", 110 "pub mod radroots_transport::endpoint", 111 "pub mod radroots_transport::error", 112 "pub mod radroots_transport::outcome", 113 "pub mod radroots_transport::policy", 114 "pub mod radroots_transport::sink", 115 "pub mod radroots_transport::source", 116 "pub mod radroots_transport::target", 117 "pub trait radroots_transport::EventSource", 118 "pub trait radroots_transport::EventSubscriber", 119 "pub trait radroots_transport::EventSubscription", 120 "pub trait radroots_transport::EventSink", 121 "pub struct radroots_transport::source::SubscriptionBounds", 122 "pub struct radroots_transport::source::SubscriptionCheckpoint", 123 "pub struct radroots_transport::SubscriptionRequest", 124 "pub enum radroots_transport::SubscriptionEndReason", 125 "pub const radroots_transport::source::SUBSCRIPTION_MAX_EVENTS: u16", 126 "pub const radroots_transport::source::FETCH_SELECTOR_MAX_TAG_VALUES: usize", 127 "pub const radroots_transport::source::FETCH_SELECTOR_MAX_TAG_KEYS: usize", 128 "pub const radroots_transport::source::FETCH_SELECTOR_TAG_VALUE_MAX_BYTES: usize", 129 "pub fn radroots_transport::source::FetchSelector::with_exact_tag_value", 130 "pub fn radroots_transport::source::FetchSelector::exact_tag_filters", 131 "pub fn radroots_transport::target::Target::new(radroots_transport::TransportId", 132 "pub fn radroots_transport::target::Target::kind(&self) -> &radroots_transport::TransportId", 133 "pub enum radroots_transport::target::TargetNetworkPolicy", 134 "pub fn radroots_transport::target::Target::nostr_relay_with_policy", 135 ] { 136 assert!( 137 PUBLIC_API.contains(required), 138 "public API baseline is missing {required}" 139 ); 140 } 141 for forbidden in [ 142 "pub trait radroots_transport::RadrootsTransport", 143 "RadrootsTransportFuture", 144 "RadrootsTransportKind", 145 "RadrootsTransportTargetSet", 146 "Target::reticulum", 147 "TargetScope::local_reticulum", 148 ] { 149 assert!( 150 !PUBLIC_API.contains(forbidden), 151 "reviewed public API baseline still exposes `{forbidden}`" 152 ); 153 } 154 } 155 156 #[test] 157 fn every_public_module_has_crate_level_documentation() { 158 let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); 159 for module in [ 160 "capability", 161 "endpoint", 162 "error", 163 "outcome", 164 "policy", 165 "sink", 166 "source", 167 "target", 168 ] { 169 let path = source_root.join(format!("{module}.rs")); 170 let source = fs::read_to_string(&path).expect("read module source"); 171 assert!( 172 source.starts_with("//! "), 173 "public module {module} must start with module documentation" 174 ); 175 } 176 } 177 178 #[test] 179 fn source_subscription_and_sink_are_independent_dyn_compatible_host_spis() { 180 for required in [ 181 "pub trait EventSource: Send + Sync", 182 "fn status(&self)", 183 "fn fetch(", 184 "Dropping a returned future requests cancellation.", 185 "explicit request deadline", 186 ] { 187 assert!( 188 SOURCE.contains(required), 189 "source SPI is missing {required}" 190 ); 191 } 192 assert!(!SOURCE.contains("fn deliver(")); 193 194 for required in [ 195 "pub trait EventSubscriber: Send + Sync", 196 "pub trait EventSubscription: Send", 197 "fn subscribe(", 198 "fn next(&mut self)", 199 "fn cancel(&mut self)", 200 "Once [`SubscriptionNext::End`] has been", 201 "exact same", 202 ] { 203 assert!( 204 SOURCE.contains(required), 205 "subscription SPI is missing {required}" 206 ); 207 } 208 209 for required in [ 210 "pub trait EventSink: Send + Sync", 211 "fn status(&self)", 212 "fn deliver(", 213 "Dropping a returned future requests cancellation.", 214 "explicit request deadline", 215 ] { 216 assert!(SINK.contains(required), "sink SPI is missing {required}"); 217 } 218 assert!(!SINK.contains("fn fetch(")); 219 220 for retired in [ 221 "Publish-frozen external-consumer", 222 "pub fn reticulum(", 223 "pub fn reticulum_with_metadata(", 224 "pub fn local_reticulum(", 225 "RadrootsTransportTarget", 226 ] { 227 assert!( 228 !TARGET.contains(retired), 229 "retired external-consumer surface remains: `{retired}`" 230 ); 231 } 232 for retired in [ 233 "RadrootsTransportKind", 234 "pub const Local", 235 "pub const Nostr", 236 "pub const Reticulum", 237 ] { 238 assert!( 239 !ID.contains(retired), 240 "retired transport identity remains: `{retired}`" 241 ); 242 } 243 for required in [ 244 "id = \"RCRV1-DEV-007\"", 245 "status = \"closed\"", 246 "affected_steps = [\"122\", \"170\", \"215\", \"235\", \"305\"]", 247 "Step 313 source census confirmed every external-consumer alias and helper is absent", 248 ] { 249 assert!( 250 DEVIATIONS.contains(required), 251 "temporary downstream ownership record is missing `{required}`" 252 ); 253 } 254 } 255 256 #[test] 257 fn public_api_excludes_adapter_runtime_storage_and_retry_authority() { 258 let dependency_keys = table_keys(MANIFEST, "[dependencies]"); 259 for forbidden in [ 260 "radroots_outbox", 261 "radroots_storage", 262 "radroots_transport_nostr", 263 "radroots_transport_reticulum", 264 "nostr-sdk", 265 "nostr_sdk", 266 "reqwest", 267 "sqlx", 268 "tokio", 269 ] { 270 assert!( 271 !dependency_keys.contains(forbidden), 272 "generic transport must not depend on `{forbidden}`" 273 ); 274 } 275 276 assert!(ID.contains("pub struct TransportId(")); 277 assert!(!ID.contains("pub enum TransportId")); 278 for forbidden in [ 279 "RADROOTS_RETICULUM_", 280 "ReticulumDestination", 281 "RelayUrl", 282 "NostrRelay", 283 ] { 284 assert!( 285 !ROOT.contains(forbidden), 286 "generic transport root must not export adapter symbol `{forbidden}`" 287 ); 288 } 289 for source in [SOURCE, SINK] { 290 for forbidden in [ 291 "tokio::spawn", 292 "std::thread::spawn", 293 "retry_loop", 294 "fallback_transport", 295 ] { 296 assert!( 297 !source.contains(forbidden), 298 "transport SPI must not own runtime behavior `{forbidden}`" 299 ); 300 } 301 } 302 } 303 304 fn table_keys<'a>(source: &'a str, heading: &str) -> BTreeSet<&'a str> { 305 let mut in_table = false; 306 source 307 .lines() 308 .filter_map(|line| { 309 let line = line.trim(); 310 if line == heading { 311 in_table = true; 312 return None; 313 } 314 if in_table && line.starts_with('[') { 315 in_table = false; 316 } 317 in_table 318 .then(|| line.split_once('=').map(|(key, _)| key.trim())) 319 .flatten() 320 }) 321 .collect() 322 } 323 324 fn root_declarations(prefix: &str) -> BTreeSet<&str> { 325 ROOT.lines() 326 .map(str::trim) 327 .filter_map(|line| line.strip_prefix(prefix)) 328 .filter_map(|line| line.strip_suffix(';')) 329 .collect() 330 }