lib

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

package_boundary.rs (13615B)


      1 use std::collections::BTreeSet;
      2 
      3 use radroots_event::id::{
      4     CandidateId, DTag, EventId, EventSignature, MutationId, Nip01Coordinate, TradeId,
      5 };
      6 #[cfg(feature = "knowledge")]
      7 #[allow(unused_imports)]
      8 use radroots_event::knowledge as _;
      9 #[allow(unused_imports)]
     10 use radroots_event::{
     11     admission as _, calendar as _, contract as _, draft as _, envelope as _, farm as _, food as _,
     12     id as _, listing as _, media as _, post as _, profile as _, social as _, tag as _, trade as _,
     13     wire as _,
     14 };
     15 use radroots_identity::PublicKey;
     16 
     17 const MANIFEST: &str = include_str!("../Cargo.toml");
     18 const ROOT: &str = include_str!("../src/lib.rs");
     19 const IDENTIFIERS: &str = include_str!("../src/id.rs");
     20 const CONTRACT_REGISTRY: &str = include_str!("../src/contract/registry_v7.rs");
     21 const RELAY_HINT: &str = include_str!("../src/relay_hint.rs");
     22 const TRADE: &str = include_str!("../src/trade.rs");
     23 const ADMISSION: &str = include_str!("../src/admission.rs");
     24 const VERIFICATION: &str = include_str!("../src/verification.rs");
     25 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_event.txt");
     26 const CODEC_MANIFEST: &str = include_str!("../../event_codec/Cargo.toml");
     27 
     28 #[test]
     29 fn manifest_has_final_identity_and_required_radroots_dependencies() {
     30     assert!(MANIFEST.contains("name = \"radroots_event\""));
     31     assert!(MANIFEST.contains("version = \"0.1.0-alpha\""));
     32     assert!(MANIFEST.contains("publish = [\"crates-io\"]"));
     33     assert!(MANIFEST.contains("[lib]\nname = \"radroots_event\""));
     34     assert!(MANIFEST.contains("default = [\"std\", \"serde\"]"));
     35     assert_eq!(
     36         table_keys(MANIFEST, "[dependencies]")
     37             .into_iter()
     38             .filter(|dependency| dependency.starts_with("radroots_"))
     39             .collect::<BTreeSet<_>>(),
     40         BTreeSet::from([
     41             "radroots_blossom",
     42             "radroots_core",
     43             "radroots_identity",
     44             "radroots_protocol",
     45         ])
     46     );
     47     assert!(
     48         MANIFEST.contains("radroots_protocol = { workspace = true, default-features = false }")
     49     );
     50     assert_eq!(
     51         table_keys(MANIFEST, "[features]"),
     52         BTreeSet::from(["default", "knowledge", "serde", "std"]),
     53         "radroots_event must expose only its approved user-visible feature vocabulary"
     54     );
     55     for implementation_feature in ["dto-bindgen", "fixture", "knowledge-nip54", "signature"] {
     56         assert!(
     57             !table_keys(MANIFEST, "[features]").contains(implementation_feature),
     58             "implementation feature `{implementation_feature}` must remain private"
     59         );
     60     }
     61     assert!(!table_keys(MANIFEST, "[dependencies]").contains("dto_bindgen"));
     62     assert!(!table_keys(MANIFEST, "[dependencies]").contains("secp256k1"));
     63     for forbidden_dependency in [
     64         "libsqlite3-sys",
     65         "nostr",
     66         "nostr-sdk",
     67         "reqwest",
     68         "sqlx",
     69         "tokio",
     70     ] {
     71         assert!(
     72             !table_keys(MANIFEST, "[dependencies]").contains(forbidden_dependency),
     73             "forbidden implementation dependency `{forbidden_dependency}`"
     74         );
     75     }
     76 }
     77 
     78 #[test]
     79 fn public_traits_are_exact_and_classified_as_host_spis() {
     80     let declarations = [ADMISSION, VERIFICATION]
     81         .into_iter()
     82         .flat_map(|source| source.lines())
     83         .map(str::trim)
     84         .filter_map(|line| line.strip_prefix("pub trait "))
     85         .filter_map(|declaration| declaration.split([':', ' ', '{']).next())
     86         .collect::<BTreeSet<_>>();
     87 
     88     assert_eq!(
     89         declarations,
     90         BTreeSet::from(["AdmissionPolicy", "SignatureVerifier", "VisibilityPolicy"])
     91     );
     92     for declaration in [
     93         "pub trait AdmissionPolicy: Send + Sync",
     94         "pub trait VisibilityPolicy: Send + Sync",
     95         "pub trait SignatureVerifier: Send + Sync",
     96     ] {
     97         assert!(
     98             ADMISSION.contains(declaration) || VERIFICATION.contains(declaration),
     99             "missing native Host SPI contract `{declaration}`"
    100         );
    101     }
    102     assert_eq!(
    103         ADMISSION.matches("/// **Host SPI:**").count()
    104             + VERIFICATION.matches("/// **Host SPI:**").count(),
    105         3,
    106         "every public event trait must carry one explicit Host SPI classification"
    107     );
    108 }
    109 
    110 #[test]
    111 fn crate_root_declares_every_approved_module() {
    112     let declared = root_declarations("pub mod ");
    113     let approved = [
    114         "admission",
    115         "calendar",
    116         "contract",
    117         "draft",
    118         "envelope",
    119         "farm",
    120         "food",
    121         "generic_draft",
    122         "id",
    123         "knowledge",
    124         "listing",
    125         "media",
    126         "post",
    127         "profile",
    128         "social",
    129         "tag",
    130         "trade",
    131         "wire",
    132     ];
    133     assert_eq!(
    134         declared,
    135         approved.into_iter().collect(),
    136         "the crate root must expose exactly the approved singular module vocabulary"
    137     );
    138 }
    139 
    140 #[test]
    141 fn crate_root_exposes_the_curated_native_event_vocabulary() {
    142     type CuratedRootTypes = (
    143         radroots_event::Event,
    144         radroots_event::GenericEventDraft,
    145         radroots_event::SignedEvent,
    146         radroots_event::VerifiedEvent,
    147         radroots_event::EventId,
    148         radroots_event::EventKind,
    149         radroots_event::EventTag,
    150         radroots_event::Error,
    151     );
    152     let _: Option<CuratedRootTypes> = None;
    153     for root_export in [
    154         "Event",
    155         "GenericEventDraft",
    156         "SignedEvent",
    157         "VerifiedEvent",
    158         "EventId",
    159         "EventKind",
    160         "EventTag",
    161         "Error",
    162     ] {
    163         assert!(
    164             ROOT.contains(root_export),
    165             "missing root export {root_export}"
    166         );
    167     }
    168     assert!(!ROOT.contains("pub use *"));
    169     assert!(!ROOT.contains("pub use crate::*"));
    170 }
    171 
    172 #[test]
    173 fn public_native_items_do_not_retain_the_legacy_radroots_prefix() {
    174     let prefixed_items = PUBLIC_API
    175         .lines()
    176         .filter(|line| {
    177             line.split("::").any(|segment| {
    178                 segment
    179                     .strip_prefix("Radroots")
    180                     .and_then(|suffix| suffix.chars().next())
    181                     .is_some_and(|character| character.is_ascii_uppercase())
    182             })
    183         })
    184         .collect::<Vec<_>>();
    185 
    186     assert!(
    187         prefixed_items.is_empty(),
    188         "legacy-prefixed native items remain public: {prefixed_items:#?}"
    189     );
    190     for retired in [
    191         "radroots_event::post::Post",
    192         "radroots_event::profile::Profile",
    193     ] {
    194         let declaration = format!("pub struct {retired}");
    195         let member_prefix = format!("pub {retired}::");
    196         assert!(
    197             !PUBLIC_API
    198                 .lines()
    199                 .any(|line| line == declaration || line.starts_with(&member_prefix)),
    200             "retired compatibility type remains public: {retired}"
    201         );
    202     }
    203 }
    204 
    205 #[test]
    206 fn event_codec_has_no_lossy_post_or_profile_projection_surface() {
    207     assert!(CODEC_MANIFEST.contains("publish = [\"crates-io\"]"));
    208     let codec_root = include_str!("../../event_codec/src/lib.rs");
    209     let post_module = include_str!("../../event_codec/src/post/mod.rs");
    210     let profile_module = include_str!("../../event_codec/src/profile/mod.rs");
    211     for source in [codec_root, post_module, profile_module] {
    212         assert!(!source.contains("LegacyPost"));
    213         assert!(!source.contains("LegacyProfile"));
    214         assert!(!source.contains("RadrootsProfileData"));
    215     }
    216     assert!(!post_module.contains("pub mod decode;"));
    217     assert!(!profile_module.contains("pub mod decode;"));
    218 }
    219 
    220 #[test]
    221 fn verification_typestates_are_native_private_and_policy_gated() {
    222     let admission = include_str!("../src/admission.rs");
    223     let verification = include_str!("../src/verification.rs");
    224 
    225     for state in [
    226         "RawEvent",
    227         "IdVerifiedEvent",
    228         "SignatureVerifiedEvent",
    229         "ContractValidatedEvent",
    230         "AdmittedEvent",
    231         "VisibleEvent",
    232     ] {
    233         assert!(
    234             admission.contains(state) || verification.contains(state),
    235             "missing native typestate {state}"
    236         );
    237     }
    238     assert!(ROOT.contains("mod verification;"));
    239     assert!(!ROOT.contains("pub mod verification;"));
    240     assert!(verification.contains("pub struct IdVerifiedEvent(EventEnvelope);"));
    241     assert!(verification.contains("pub struct SignatureVerifiedEvent(EventEnvelope);"));
    242     assert!(admission.contains("policy.admit(&self)?;"));
    243     assert!(admission.contains("policy.make_visible(&self)?;"));
    244     assert!(verification.contains("pub trait SignatureVerifier: Send + Sync"));
    245     assert!(verification.contains("verifier.verify_signature(&self.0)?;"));
    246     assert!(!verification.contains("secp256k1"));
    247     assert!(!verification.contains("SignatureVerificationUnavailable"));
    248     assert!(!verification.contains("impl From<RawEvent> for IdVerifiedEvent"));
    249     assert!(!verification.contains("impl From<IdVerifiedEvent> for SignatureVerifiedEvent"));
    250     assert!(!admission.contains("impl From<ContractValidatedEvent> for AdmittedEvent"));
    251     assert!(!admission.contains("impl From<AdmittedEvent> for VisibleEvent"));
    252 }
    253 
    254 #[test]
    255 fn contract_author_roles_are_event_owned_requirements_not_signer_provenance() {
    256     let seller =
    257         radroots_event::contract::event_contract("radroots.operational_listing.published.v1")
    258             .expect("operational listing contract");
    259 
    260     assert_eq!(
    261         seller.required_author_role(),
    262         radroots_event::contract::AuthorRole::Seller
    263     );
    264     assert!(!CONTRACT_REGISTRY.contains("RadrootsActorRole"));
    265     assert!(!CONTRACT_REGISTRY.contains("pub author_role:"));
    266     assert!(!CONTRACT_REGISTRY.contains("radroots_signing"));
    267     assert!(!CONTRACT_REGISTRY.contains("RadrootsActorContext"));
    268     assert!(!CONTRACT_REGISTRY.contains("AccountId"));
    269 }
    270 
    271 #[test]
    272 fn canonical_identifier_api_owns_bytes_and_requires_explicit_text_encoding() {
    273     let event_id = EventId::parse("A".repeat(64)).expect("event id");
    274     let signature = EventSignature::parse("B".repeat(128)).expect("event signature");
    275     let d_tag = DTag::parse("listing-1").expect("d tag");
    276     let coordinate = Nip01Coordinate::parse(format!("30000:{}:listing-1", event_id.to_hex()))
    277         .expect("coordinate");
    278 
    279     assert_eq!(core::mem::size_of::<EventId>(), 32);
    280     assert_eq!(core::mem::size_of::<EventSignature>(), 64);
    281     assert_eq!(event_id.to_hex(), "a".repeat(64));
    282     assert_eq!(EventId::from_bytes(event_id.into_bytes()), event_id);
    283     assert_eq!(signature.to_hex(), "b".repeat(128));
    284     assert_eq!(d_tag.as_str(), "listing-1");
    285     assert_eq!(coordinate.identifier(), "listing-1");
    286     assert!(!IDENTIFIERS.contains("impl Deref"));
    287     assert!(!IDENTIFIERS.contains("impl Borrow<str>"));
    288     assert!(!RELAY_HINT.contains("impl Deref"));
    289     assert!(!RELAY_HINT.contains("impl Borrow<str>"));
    290 }
    291 
    292 #[test]
    293 fn event_references_use_the_identity_owned_public_key() {
    294     let author =
    295         PublicKey::from_hex("585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df")
    296             .expect("valid public key");
    297     let reference = radroots_event::tag::EventRef {
    298         id: "a".repeat(64),
    299         author,
    300         kind: 1,
    301         d_tag: None,
    302         relays: None,
    303     };
    304 
    305     assert_eq!(reference.author, author);
    306     assert!(!IDENTIFIERS.contains("pub(crate) use radroots_identity::PublicKey"));
    307 }
    308 
    309 #[test]
    310 fn trade_protocol_identifiers_have_one_definition_and_deliberate_facades() {
    311     let trade_id = TradeId::parse("11".repeat(16)).expect("trade id");
    312     let candidate_id = CandidateId::parse("22".repeat(32)).expect("candidate id");
    313     let mutation_id = MutationId::parse("33".repeat(32)).expect("mutation id");
    314 
    315     let trade_surface_id: radroots_event::trade::TradeId = trade_id;
    316     let trade_surface_candidate: radroots_event::trade::CandidateId = candidate_id;
    317     let trade_surface_mutation: radroots_event::trade::MutationId = mutation_id;
    318 
    319     assert_eq!(trade_surface_id, trade_id);
    320     assert_eq!(trade_surface_candidate, candidate_id);
    321     assert_eq!(trade_surface_mutation, mutation_id);
    322     assert_eq!(core::mem::size_of::<TradeId>(), 16);
    323     assert_eq!(core::mem::size_of::<CandidateId>(), 32);
    324     assert_eq!(core::mem::size_of::<MutationId>(), 32);
    325     assert!(TradeId::parse("order-1").is_err());
    326     assert!(radroots_event::id::OrderId::parse("order-1").is_ok());
    327 
    328     for definition in [
    329         "validated_hex_id!(TradeId, 16);",
    330         "validated_hex_id!(CandidateId, 32);",
    331         "validated_hex_id!(MutationId, 32);",
    332     ] {
    333         assert_eq!(IDENTIFIERS.matches(definition).count(), 1, "{definition}");
    334     }
    335     for duplicate in [
    336         "pub struct TradeId",
    337         "pub struct CandidateId",
    338         "pub struct MutationId",
    339     ] {
    340         assert!(
    341             !TRADE.contains(duplicate),
    342             "duplicate definition: {duplicate}"
    343         );
    344     }
    345 }
    346 
    347 fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> {
    348     let Some((_, table)) = manifest.split_once(heading) else {
    349         return BTreeSet::new();
    350     };
    351     table
    352         .lines()
    353         .skip(1)
    354         .take_while(|line| !line.trim_start().starts_with('['))
    355         .filter_map(|line| {
    356             let line = line.trim();
    357             (line
    358                 .bytes()
    359                 .next()
    360                 .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_')
    361                 && !line.starts_with('#'))
    362             .then(|| line.split_once('=').map(|(key, _)| key.trim()))
    363             .flatten()
    364         })
    365         .collect()
    366 }
    367 
    368 fn root_declarations(prefix: &str) -> BTreeSet<&str> {
    369     ROOT.lines()
    370         .map(str::trim)
    371         .filter_map(|line| line.strip_prefix(prefix))
    372         .filter_map(|name| name.strip_suffix(';'))
    373         .collect()
    374 }