sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

lib.rs (5549B)


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