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 }