lib

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

lib.rs (5981B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[cfg(target_arch = "wasm32")]
      4 use base64::Engine;
      5 #[cfg(target_arch = "wasm32")]
      6 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
      7 use radroots_event::envelope::{EventEnvelope, EventEnvelopeParts};
      8 #[cfg(any(target_arch = "wasm32", test))]
      9 use radroots_replica_sync::RadrootsReplicaIngestOutcome;
     10 use radroots_replica_sync::RadrootsReplicaSyncRequest;
     11 #[cfg(target_arch = "wasm32")]
     12 use radroots_replica_sync::{
     13     RadrootsReplicaIdFactory, radroots_replica_ingest_event_with_factory, radroots_replica_sync_all,
     14 };
     15 #[cfg(target_arch = "wasm32")]
     16 use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor;
     17 use serde::Deserialize;
     18 #[cfg(target_arch = "wasm32")]
     19 use uuid::Uuid;
     20 #[cfg(target_arch = "wasm32")]
     21 use wasm_bindgen::prelude::*;
     22 
     23 #[cfg(target_arch = "wasm32")]
     24 fn err_js<E: ToString>(err: E) -> JsValue {
     25     JsValue::from_str(&err.to_string())
     26 }
     27 
     28 #[cfg(target_arch = "wasm32")]
     29 struct WasmIdFactory;
     30 
     31 #[cfg(target_arch = "wasm32")]
     32 impl RadrootsReplicaIdFactory for WasmIdFactory {
     33     fn new_d_tag(&self) -> String {
     34         let uuid = Uuid::now_v7();
     35         URL_SAFE_NO_PAD.encode(uuid.as_bytes())
     36     }
     37 }
     38 
     39 #[derive(Deserialize)]
     40 #[serde(deny_unknown_fields)]
     41 struct EventEnvelopeInput {
     42     id: String,
     43     author: String,
     44     created_at: u64,
     45     kind: u32,
     46     tags: Vec<Vec<String>>,
     47     content: String,
     48     sig: String,
     49 }
     50 
     51 pub fn parse_request_model(request_json: &str) -> Result<RadrootsReplicaSyncRequest, String> {
     52     serde_json::from_str(request_json).map_err(|error| error.to_string())
     53 }
     54 
     55 pub fn parse_event_model(event_json: &str) -> Result<EventEnvelope, String> {
     56     let envelope: EventEnvelopeInput =
     57         serde_json::from_str(event_json).map_err(|error| error.to_string())?;
     58     EventEnvelope::new(EventEnvelopeParts {
     59         id: envelope.id,
     60         author: envelope.author,
     61         created_at: envelope.created_at,
     62         kind: envelope.kind,
     63         tags: envelope.tags,
     64         content: envelope.content,
     65         sig: envelope.sig,
     66     })
     67     .map_err(|error| error.to_string())
     68 }
     69 
     70 #[cfg(any(target_arch = "wasm32", test))]
     71 fn ingest_outcome_label(outcome: RadrootsReplicaIngestOutcome) -> &'static str {
     72     match outcome {
     73         RadrootsReplicaIngestOutcome::Applied => "applied",
     74         RadrootsReplicaIngestOutcome::Excluded => "excluded",
     75         RadrootsReplicaIngestOutcome::Rejected => "rejected",
     76         RadrootsReplicaIngestOutcome::Skipped => "skipped",
     77     }
     78 }
     79 
     80 #[cfg(target_arch = "wasm32")]
     81 #[wasm_bindgen(js_name = replica_sync_sync_all)]
     82 pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> {
     83     let request = parse_request_model(request_json).map_err(err_js)?;
     84     let exec = WasmSqlExecutor::new();
     85     let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?;
     86     serde_wasm_bindgen::to_value(&bundle).map_err(err_js)
     87 }
     88 
     89 #[cfg(target_arch = "wasm32")]
     90 #[wasm_bindgen(js_name = replica_sync_ingest_event)]
     91 pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> {
     92     let event = parse_event_model(event_json).map_err(err_js)?;
     93     let exec = WasmSqlExecutor::new();
     94     let factory = WasmIdFactory;
     95     let outcome =
     96         radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?;
     97     Ok(JsValue::from_str(ingest_outcome_label(outcome)))
     98 }
     99 
    100 #[cfg(test)]
    101 mod tests {
    102     use super::{ingest_outcome_label, parse_event_model, parse_request_model};
    103     use radroots_replica_sync::RadrootsReplicaIngestOutcome;
    104 
    105     fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String {
    106         let mut fields = vec![
    107             format!(r#""id":"{}""#, "0".repeat(64)),
    108             r#""created_at":123"#.to_owned(),
    109             r#""kind":30023"#.to_owned(),
    110             r#""tags":[["d","one"]]"#.to_owned(),
    111             r#""content":"content""#.to_owned(),
    112             format!(r#""sig":"{}""#, "f".repeat(128)),
    113         ];
    114         if let Some(author) = author {
    115             fields.push(format!(r#""author":"{author}""#));
    116         }
    117         if let Some(pubkey) = pubkey {
    118             fields.push(format!(r#""pubkey":"{pubkey}""#));
    119         }
    120         format!("{{{}}}", fields.join(","))
    121     }
    122 
    123     #[test]
    124     fn parse_event_accepts_author_domain_envelope() {
    125         let author = "a".repeat(64);
    126         let event = parse_event_model(&event_json(Some(author.as_str()), None)).expect("event");
    127         assert_eq!(event.author().to_hex(), author);
    128         assert_eq!(
    129             event.tags_as_vec(),
    130             vec![vec!["d".to_owned(), "one".to_owned()]]
    131         );
    132     }
    133 
    134     #[test]
    135     fn parse_event_rejects_pubkey_wire_alias() {
    136         let author = "a".repeat(64);
    137         let error = parse_event_model(&event_json(Some(author.as_str()), Some(author.as_str())))
    138             .expect_err("error");
    139         assert!(error.contains("unknown field `pubkey`"));
    140     }
    141 
    142     #[test]
    143     fn parse_event_rejects_missing_author() {
    144         let error = parse_event_model(&event_json(None, None)).expect_err("error");
    145         assert!(error.contains("missing field `author`"));
    146     }
    147 
    148     #[test]
    149     fn parse_event_rejects_malformed_json() {
    150         let error = parse_event_model("{").expect_err("error");
    151         assert!(error.contains("EOF"));
    152     }
    153 
    154     #[test]
    155     fn parse_request_rejects_malformed_json() {
    156         let error = parse_request_model("{").expect_err("error");
    157         assert!(error.contains("EOF"));
    158     }
    159 
    160     #[test]
    161     fn ingest_outcome_labels_cover_the_replica_contract() {
    162         assert_eq!(
    163             ingest_outcome_label(RadrootsReplicaIngestOutcome::Applied),
    164             "applied"
    165         );
    166         assert_eq!(
    167             ingest_outcome_label(RadrootsReplicaIngestOutcome::Excluded),
    168             "excluded"
    169         );
    170         assert_eq!(
    171             ingest_outcome_label(RadrootsReplicaIngestOutcome::Rejected),
    172             "rejected"
    173         );
    174         assert_eq!(
    175             ingest_outcome_label(RadrootsReplicaIngestOutcome::Skipped),
    176             "skipped"
    177         );
    178     }
    179 }