lib

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

package_boundary.rs (9836B)


      1 use std::{collections::BTreeSet, fs, path::Path};
      2 
      3 #[allow(unused_imports)]
      4 use radroots_signing::{
      5     Actor, Error, SignReceipt, SignRequest, Signer, SignerStatus, actor as _, authorization as _,
      6     capability as _, error as _, identity as _, receipt as _, recovery as _, request as _,
      7     signer as _, status as _,
      8 };
      9 
     10 const MANIFEST: &str = include_str!("../Cargo.toml");
     11 const EXAMPLE: &str = include_str!("../examples/host_signer.rs");
     12 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_signing.txt");
     13 const README: &str = include_str!("../README.md");
     14 const ROOT: &str = include_str!("../src/lib.rs");
     15 
     16 #[test]
     17 fn manifest_has_final_identity_features_and_dependencies() {
     18     for required in [
     19         "name = \"radroots_signing\"",
     20         "version = \"0.1.0-alpha\"",
     21         "publish = [\"crates-io\"]",
     22         "default = [\"std\", \"serde\"]",
     23         "radroots_event = { workspace = true, default-features = false }",
     24         "radroots_event_codec = { workspace = true, default-features = false }",
     25         "radroots_identity = { workspace = true, default-features = false }",
     26         "radroots_protocol = { workspace = true, default-features = false }",
     27     ] {
     28         assert!(
     29             MANIFEST.contains(required),
     30             "manifest is missing {required}"
     31         );
     32     }
     33     assert_eq!(
     34         table_keys(MANIFEST, "[features]"),
     35         BTreeSet::from(["default", "serde", "std"])
     36     );
     37     assert_eq!(
     38         table_keys(MANIFEST, "[dependencies]"),
     39         BTreeSet::from([
     40             "hex",
     41             "radroots_event",
     42             "radroots_event_codec",
     43             "radroots_identity",
     44             "radroots_protocol",
     45             "serde",
     46             "sha2",
     47         ])
     48     );
     49     assert_eq!(
     50         table_keys(MANIFEST, "[dev-dependencies]"),
     51         BTreeSet::from(["nostr", "radroots_blossom", "serde_json"])
     52     );
     53     for forbidden in [
     54         "async-trait",
     55         "keyring",
     56         "nostr",
     57         "nostr-sdk",
     58         "reqwest",
     59         "sqlx",
     60         "tokio",
     61     ] {
     62         assert!(
     63             !table_keys(MANIFEST, "[dependencies]").contains(forbidden),
     64             "signing runtime must not depend on {forbidden}"
     65         );
     66     }
     67 }
     68 
     69 #[test]
     70 fn crate_root_declares_the_approved_module_skeleton() {
     71     assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]"));
     72     for module in [
     73         "actor",
     74         "authorization",
     75         "capability",
     76         "error",
     77         "identity",
     78         "recovery",
     79         "request",
     80         "receipt",
     81         "signer",
     82         "status",
     83     ] {
     84         let declaration = format!("pub mod {module};");
     85         assert!(
     86             ROOT.contains(&declaration),
     87             "crate root is missing {module}"
     88         );
     89     }
     90     assert_eq!(
     91         root_declarations("pub mod "),
     92         BTreeSet::from([
     93             "actor",
     94             "authorization",
     95             "capability",
     96             "error",
     97             "identity",
     98             "receipt",
     99             "recovery",
    100             "request",
    101             "signer",
    102             "status",
    103         ])
    104     );
    105     let _ = core::mem::size_of::<Actor>();
    106     let _ = core::mem::size_of::<SignRequest>();
    107     let _ = core::mem::size_of::<SignReceipt>();
    108     let _ = core::mem::size_of::<SignerStatus>();
    109     let _ = core::mem::size_of::<Error>();
    110     fn assert_object_safe(_: &dyn Signer) {}
    111     let _ = assert_object_safe;
    112     for root_export in [
    113         "pub use actor::Actor;",
    114         "pub use authorization::{CurrentAuthoringAuthority, CurrentAuthoringDecision};",
    115         "pub use error::Error;",
    116         "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};",
    117         "pub use receipt::{AuthoredSignEvidence, SignReceipt};",
    118         "pub use request::{SignRequest, SigningPurpose};",
    119         "pub use signer::Signer;",
    120         "pub use status::SignerStatus;",
    121     ] {
    122         assert!(ROOT.contains(root_export), "missing {root_export}");
    123     }
    124     assert_eq!(
    125         ROOT.lines()
    126             .map(str::trim)
    127             .filter(|line| line.starts_with("pub use "))
    128             .collect::<BTreeSet<_>>(),
    129         BTreeSet::from([
    130             "pub use actor::Actor;",
    131             "pub use authorization::{CurrentAuthoringAuthority, CurrentAuthoringDecision};",
    132             "pub use error::Error;",
    133             "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};",
    134             "pub use receipt::{AuthoredSignEvidence, SignReceipt};",
    135             "pub use request::{SignRequest, SigningPurpose};",
    136             "pub use signer::Signer;",
    137             "pub use status::SignerStatus;",
    138         ])
    139     );
    140     assert!(!ROOT.contains("prelude"));
    141 }
    142 
    143 #[test]
    144 fn package_documentation_and_reviewed_api_baseline_are_complete() {
    145     for required in [
    146         "## Typical flow",
    147         "## Host SPI contract",
    148         "## Deadlines, cancellation, and commit points",
    149         "## Serialization contract",
    150         "## Security and side effects",
    151         "## Features",
    152         "## Intended consumers",
    153         "radroots_crates_release_v1.toml",
    154         "examples/host_signer.rs",
    155     ] {
    156         assert!(README.contains(required), "README is missing {required}");
    157     }
    158     for required in [
    159         "impl Signer for HostSigner",
    160         "fn status(&self) -> BoxFuture",
    161         "fn sign(&self, _request: SignRequest) -> BoxFuture",
    162         "let signer: &dyn Signer",
    163         "drop(future)",
    164     ] {
    165         assert!(EXAMPLE.contains(required), "example is missing {required}");
    166     }
    167     for required in [
    168         "pub mod radroots_signing::actor",
    169         "pub mod radroots_signing::capability",
    170         "pub mod radroots_signing::error",
    171         "pub mod radroots_signing::receipt",
    172         "pub mod radroots_signing::request",
    173         "pub mod radroots_signing::signer",
    174         "pub mod radroots_signing::status",
    175         "pub enum radroots_signing::request::SigningPurpose",
    176         "pub radroots_signing::request::SigningPurpose::BlossomUploadAuthorization",
    177         "pub trait radroots_signing::Signer",
    178     ] {
    179         assert!(
    180             PUBLIC_API.contains(required),
    181             "public API baseline is missing {required}"
    182         );
    183     }
    184 }
    185 
    186 #[test]
    187 fn every_public_module_has_crate_level_documentation() {
    188     let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    189     for module in [
    190         "actor",
    191         "authorization",
    192         "capability",
    193         "error",
    194         "identity",
    195         "recovery",
    196         "request",
    197         "receipt",
    198         "signer",
    199         "status",
    200     ] {
    201         let path = source_root.join(format!("{module}.rs"));
    202         let source = fs::read_to_string(&path).expect("read module source");
    203         assert!(
    204             source.starts_with("//! "),
    205             "public module {module} must start with module documentation"
    206         );
    207     }
    208 }
    209 
    210 #[test]
    211 fn production_sources_publish_only_the_approved_traits_and_no_host_stack() {
    212     let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    213     let mut sources = Vec::new();
    214     collect_rust_sources(&source_root, &mut sources);
    215     assert!(!sources.is_empty());
    216     let mut public_traits = BTreeSet::new();
    217 
    218     for path in sources {
    219         let source = fs::read_to_string(&path).expect("read signing source");
    220         let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source);
    221         for line in production.lines() {
    222             let trimmed = line.trim_start();
    223             if let Some(name) = trimmed
    224                 .strip_prefix("pub trait ")
    225                 .and_then(|rest| rest.split([':', '<', ' ']).next())
    226             {
    227                 public_traits.insert(name.to_owned());
    228             }
    229             for forbidden in [
    230                 "nostr::",
    231                 "nostr_sdk::",
    232                 "reqwest::",
    233                 "sqlx::",
    234                 "tokio::",
    235                 "keyring::",
    236                 "std::fs",
    237                 "std::path",
    238                 "SecretKey",
    239                 "PrivateKey",
    240             ] {
    241                 assert!(
    242                     !line.contains(forbidden),
    243                     "signing production source must not contain {forbidden}: {}: {trimmed}",
    244                     path.display()
    245                 );
    246             }
    247         }
    248     }
    249 
    250     assert_eq!(
    251         public_traits,
    252         ["CurrentAuthoringAuthority", "ProgressObserver", "Signer"]
    253             .into_iter()
    254             .map(str::to_owned)
    255             .collect()
    256     );
    257 }
    258 
    259 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> {
    260     let table = manifest
    261         .split_once(heading)
    262         .unwrap_or_else(|| panic!("missing manifest table {heading}"))
    263         .1;
    264     table
    265         .lines()
    266         .skip(1)
    267         .take_while(|line| !line.trim_start().starts_with('['))
    268         .filter_map(|line| {
    269             let line = line.trim();
    270             (line
    271                 .bytes()
    272                 .next()
    273                 .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_')
    274                 && !line.starts_with('#'))
    275             .then(|| line.split_once('=').map(|(key, _)| key.trim()))
    276             .flatten()
    277         })
    278         .collect()
    279 }
    280 
    281 fn root_declarations(prefix: &str) -> BTreeSet<&str> {
    282     ROOT.lines()
    283         .map(str::trim)
    284         .filter_map(|line| line.strip_prefix(prefix))
    285         .filter_map(|name| name.strip_suffix(';'))
    286         .collect()
    287 }
    288 
    289 fn collect_rust_sources(directory: &Path, paths: &mut Vec<std::path::PathBuf>) {
    290     for entry in fs::read_dir(directory).expect("read signing source directory") {
    291         let path = entry.expect("source directory entry").path();
    292         if path.is_dir() {
    293             collect_rust_sources(&path, paths);
    294         } else if path.extension().and_then(|value| value.to_str()) == Some("rs") {
    295             paths.push(path);
    296         }
    297     }
    298 }