lib

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

generic_builder_boundary.rs (4654B)


      1 use std::fs;
      2 use std::path::{Path, PathBuf};
      3 
      4 #[test]
      5 fn public_source_does_not_expose_the_upstream_event_builder() {
      6     let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
      7         .parent()
      8         .and_then(Path::parent)
      9         .expect("lib repo root");
     10     let mut findings = Vec::new();
     11 
     12     for path in rust_source_files(repo_root.join("crates").as_path()) {
     13         if path.file_name().and_then(|name| name.to_str()) == Some("generic_builder_boundary.rs") {
     14             continue;
     15         }
     16         let source = fs::read_to_string(&path).expect("read Rust source");
     17         let normalized = source.split_whitespace().collect::<Vec<_>>().join(" ");
     18         for statement in normalized.split(';') {
     19             let statement = statement.trim();
     20             let exposes_alias =
     21                 statement.starts_with("pub type ") && statement.contains("EventBuilder");
     22             let exposes_reexport =
     23                 statement.starts_with("pub use nostr") && statement.contains("EventBuilder");
     24             if exposes_alias || exposes_reexport {
     25                 findings.push(format!(
     26                     "{} exposes upstream EventBuilder: {statement}",
     27                     relative_path(repo_root, &path)
     28                 ));
     29             }
     30         }
     31 
     32         for forbidden in [
     33             "impl Deref for GenericBuilder",
     34             "impl AsRef<nostr::EventBuilder> for GenericBuilder",
     35             "impl From<GenericBuilder> for nostr::EventBuilder",
     36             "impl Into<nostr::EventBuilder> for GenericBuilder",
     37             "impl Deref for ExternalSigningRequest",
     38             "impl AsRef<nostr::UnsignedEvent> for ExternalSigningRequest",
     39             "impl From<ExternalSigningRequest> for nostr::UnsignedEvent",
     40             "impl Into<nostr::UnsignedEvent> for ExternalSigningRequest",
     41             "impl Deserialize for ExternalSigningRequest",
     42             "fn into_unsigned_event",
     43             "fn as_unsigned_event",
     44             "fn unsigned_event_mut",
     45         ] {
     46             if normalized.contains(forbidden) {
     47                 findings.push(format!(
     48                     "{} contains forbidden raw-builder escape `{forbidden}`",
     49                     relative_path(repo_root, &path)
     50                 ));
     51             }
     52         }
     53     }
     54 
     55     assert!(
     56         findings.is_empty(),
     57         "public generic-builder boundary violations:\n{}",
     58         findings.join("\n")
     59     );
     60 }
     61 
     62 #[test]
     63 fn every_strict_builder_is_plan_backed_and_has_no_unchecked_mapping_path() {
     64     let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
     65     for (module, expected_body_conversions) in [
     66         ("metadata.rs", 1),
     67         ("post.rs", 3),
     68         ("reply.rs", 1),
     69         ("deletion.rs", 1),
     70         ("comment.rs", 1),
     71         ("food_availability.rs", 1),
     72     ] {
     73         let source = fs::read_to_string(crate_root.join("src/events").join(module))
     74             .unwrap_or_else(|error| panic!("read {module}: {error}"));
     75         assert!(
     76             source.contains("SealedBuilderCore"),
     77             "strict builder module {module} does not use the shared plan core"
     78         );
     79         assert_eq!(
     80             source.matches("AuthoredEventBody::from_").count(),
     81             expected_body_conversions,
     82             "strict builder module {module} has an incomplete body-conversion inventory"
     83         );
     84         for forbidden in [
     85             "build_event_unchecked",
     86             "RadrootsNostrEventBuilderUnchecked",
     87             "_to_wire_parts",
     88         ] {
     89             assert!(
     90                 !source.contains(forbidden),
     91                 "strict builder module {module} retains unchecked mapping `{forbidden}`"
     92             );
     93         }
     94         for required in ["sign_with_keys", "into_external_signing_request"] {
     95             assert!(
     96                 source.contains(required),
     97                 "strict builder module {module} is missing `{required}`"
     98             );
     99         }
    100     }
    101 }
    102 
    103 fn rust_source_files(root: &Path) -> Vec<PathBuf> {
    104     let mut paths = Vec::new();
    105     collect_rust_source_files(root, &mut paths);
    106     paths.sort();
    107     paths
    108 }
    109 
    110 fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) {
    111     for entry in fs::read_dir(root).expect("read source directory") {
    112         let path = entry.expect("source entry").path();
    113         if path.is_dir() {
    114             collect_rust_source_files(&path, paths);
    115         } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
    116             paths.push(path);
    117         }
    118     }
    119 }
    120 
    121 fn relative_path(root: &Path, path: &Path) -> String {
    122     path.strip_prefix(root)
    123         .expect("source path is under repo root")
    124         .to_string_lossy()
    125         .replace('\\', "/")
    126 }