filter.rs (8726B)
1 use crate::error::RadrootsNostrdbError; 2 3 #[derive(Debug, Clone, Eq, PartialEq, Default)] 4 pub struct RadrootsNostrdbFilterSpec { 5 event_ids_hex: Vec<String>, 6 authors_hex: Vec<String>, 7 kinds: Vec<u16>, 8 since_unix: Option<u64>, 9 until_unix: Option<u64>, 10 limit: Option<u64>, 11 search: Option<String>, 12 } 13 14 impl RadrootsNostrdbFilterSpec { 15 pub fn new() -> Self { 16 Self::default() 17 } 18 19 pub fn text_notes(limit: Option<u64>, since_unix: Option<u64>) -> Self { 20 let mut filter = Self::new().with_kind(1); 21 if let Some(limit) = limit { 22 filter = filter.with_limit(limit); 23 } 24 if let Some(since_unix) = since_unix { 25 filter = filter.with_since_unix(since_unix); 26 } 27 filter 28 } 29 30 pub fn with_event_id_hex(mut self, id_hex: impl Into<String>) -> Self { 31 self.event_ids_hex.push(id_hex.into()); 32 self 33 } 34 35 pub fn with_author_hex(mut self, author_hex: impl Into<String>) -> Self { 36 self.authors_hex.push(author_hex.into()); 37 self 38 } 39 40 pub fn with_kind(mut self, kind: u16) -> Self { 41 self.kinds.push(kind); 42 self 43 } 44 45 pub fn with_since_unix(mut self, since_unix: u64) -> Self { 46 self.since_unix = Some(since_unix); 47 self 48 } 49 50 pub fn with_until_unix(mut self, until_unix: u64) -> Self { 51 self.until_unix = Some(until_unix); 52 self 53 } 54 55 pub fn with_limit(mut self, limit: u64) -> Self { 56 self.limit = Some(limit); 57 self 58 } 59 60 pub fn with_search(mut self, search: impl Into<String>) -> Self { 61 self.search = Some(search.into()); 62 self 63 } 64 65 pub fn event_ids_hex(&self) -> &[String] { 66 &self.event_ids_hex 67 } 68 69 pub fn authors_hex(&self) -> &[String] { 70 &self.authors_hex 71 } 72 73 pub fn kinds(&self) -> &[u16] { 74 &self.kinds 75 } 76 77 pub fn since_unix(&self) -> Option<u64> { 78 self.since_unix 79 } 80 81 pub fn until_unix(&self) -> Option<u64> { 82 self.until_unix 83 } 84 85 pub fn limit(&self) -> Option<u64> { 86 self.limit 87 } 88 89 pub fn search(&self) -> Option<&str> { 90 self.search.as_deref() 91 } 92 93 pub(crate) fn to_nostrdb_filter(&self) -> Result<nostrdb::Filter, RadrootsNostrdbError> { 94 let is_empty = self.event_ids_hex.is_empty() 95 && self.authors_hex.is_empty() 96 && self.kinds.is_empty() 97 && self.since_unix.is_none() 98 && self.until_unix.is_none() 99 && self.limit.is_none() 100 && self.search.is_none(); 101 102 let mut builder = nostrdb::Filter::new(); 103 104 // nostrdb 0.9 finalizes an empty native filter through realloc(ptr, 0), 105 // which can free the allocation before its Rust Drop frees it again. 106 // A lower bound of zero preserves the match-all meaning for u64 times 107 // while ensuring the native filter owns a nonempty finalized buffer. 108 if is_empty { 109 builder = builder.since(0); 110 } 111 112 if !self.event_ids_hex.is_empty() { 113 let event_ids = self 114 .event_ids_hex 115 .iter() 116 .map(|hex_value| parse_hex_32(hex_value, "event_id")) 117 .collect::<Result<Vec<_>, _>>()?; 118 builder = builder.ids(event_ids.iter()); 119 } 120 121 if !self.authors_hex.is_empty() { 122 let authors = self 123 .authors_hex 124 .iter() 125 .map(|hex_value| parse_hex_32(hex_value, "author")) 126 .collect::<Result<Vec<_>, _>>()?; 127 builder = builder.authors(authors.iter()); 128 } 129 130 if !self.kinds.is_empty() { 131 builder = builder.kinds(self.kinds.iter().map(|kind| *kind as u64)); 132 } 133 134 if let Some(since_unix) = self.since_unix { 135 builder = builder.since(since_unix); 136 } 137 138 if let Some(until_unix) = self.until_unix { 139 builder = builder.until(until_unix); 140 } 141 142 if let Some(limit) = self.limit { 143 builder = builder.limit(limit); 144 } 145 146 if let Some(search) = self.search() { 147 builder = builder.search(search); 148 } 149 150 Ok(builder.build()) 151 } 152 } 153 154 pub(crate) fn parse_hex_32( 155 value: &str, 156 field: &'static str, 157 ) -> Result<[u8; 32], RadrootsNostrdbError> { 158 let bytes = hex::decode(value).map_err(|source| RadrootsNostrdbError::InvalidHex { 159 field, 160 reason: source.to_string(), 161 })?; 162 163 if bytes.len() != 32 { 164 return Err(RadrootsNostrdbError::InvalidHexLength { 165 field, 166 expected: 32, 167 actual: bytes.len(), 168 }); 169 } 170 171 let mut out = [0u8; 32]; 172 out.copy_from_slice(bytes.as_slice()); 173 Ok(out) 174 } 175 176 #[cfg(test)] 177 mod tests { 178 use super::*; 179 180 fn valid_hex_32(value: u8) -> String { 181 format!("{value:02x}").repeat(32) 182 } 183 184 #[test] 185 fn filter_spec_builders_and_accessors_round_trip() { 186 let event_id = valid_hex_32(0x11); 187 let author = valid_hex_32(0x22); 188 189 let empty_notes = RadrootsNostrdbFilterSpec::text_notes(None, None); 190 assert_eq!(empty_notes.kinds(), &[1]); 191 assert_eq!(empty_notes.limit(), None); 192 assert_eq!(empty_notes.since_unix(), None); 193 194 let spec = RadrootsNostrdbFilterSpec::text_notes(Some(50), Some(100)) 195 .with_event_id_hex(event_id.clone()) 196 .with_author_hex(author.clone()) 197 .with_kind(30023) 198 .with_since_unix(200) 199 .with_until_unix(300) 200 .with_limit(10) 201 .with_search("coffee"); 202 203 assert_eq!(spec.event_ids_hex(), std::slice::from_ref(&event_id)); 204 assert_eq!(spec.authors_hex(), std::slice::from_ref(&author)); 205 assert_eq!(spec.kinds(), &[1, 30023]); 206 assert_eq!(spec.since_unix(), Some(200)); 207 assert_eq!(spec.until_unix(), Some(300)); 208 assert_eq!(spec.limit(), Some(10)); 209 assert_eq!(spec.search(), Some("coffee")); 210 211 let empty = RadrootsNostrdbFilterSpec::new(); 212 let empty_filter = empty.to_nostrdb_filter().expect("empty nostrdb filter"); 213 assert_eq!(empty_filter.num_elements(), 1); 214 assert_eq!(empty_filter.since(), Some(0)); 215 216 for _ in 0..512 { 217 let filter = empty.to_nostrdb_filter().expect("repeated empty filter"); 218 assert_eq!(filter.since(), Some(0)); 219 } 220 } 221 222 #[test] 223 fn to_nostrdb_filter_builds_supported_success_paths() { 224 let event_id = valid_hex_32(0x11); 225 let author = valid_hex_32(0x22); 226 227 let _ = RadrootsNostrdbFilterSpec::new() 228 .with_event_id_hex(event_id) 229 .to_nostrdb_filter() 230 .expect("event id filter"); 231 232 let _ = RadrootsNostrdbFilterSpec::new() 233 .with_author_hex(author) 234 .to_nostrdb_filter() 235 .expect("author filter"); 236 237 let _ = RadrootsNostrdbFilterSpec::new() 238 .with_kind(1) 239 .with_since_unix(200) 240 .with_until_unix(300) 241 .with_limit(10) 242 .to_nostrdb_filter() 243 .expect("range filter"); 244 245 let _ = RadrootsNostrdbFilterSpec::new() 246 .with_search("coffee") 247 .to_nostrdb_filter() 248 .expect("search filter"); 249 } 250 251 #[test] 252 fn parse_hex_32_validates_input() { 253 let valid = parse_hex_32(valid_hex_32(0xab).as_str(), "value").expect("valid"); 254 assert_eq!(valid, [0xab; 32]); 255 256 let invalid_hex = parse_hex_32("zz", "value"); 257 assert!(matches!( 258 invalid_hex, 259 Err(RadrootsNostrdbError::InvalidHex { field: "value", .. }) 260 )); 261 262 let invalid_len = parse_hex_32("abcd", "value"); 263 assert!(matches!( 264 invalid_len, 265 Err(RadrootsNostrdbError::InvalidHexLength { 266 field: "value", 267 expected: 32, 268 .. 269 }) 270 )); 271 } 272 273 #[test] 274 fn to_nostrdb_filter_rejects_invalid_event_id_and_author_hex() { 275 let bad_event_id = RadrootsNostrdbFilterSpec::new().with_event_id_hex("not-hex"); 276 let bad_event_result = bad_event_id.to_nostrdb_filter(); 277 assert!(matches!( 278 bad_event_result, 279 Err(RadrootsNostrdbError::InvalidHex { 280 field: "event_id", 281 .. 282 }) 283 )); 284 285 let bad_author = RadrootsNostrdbFilterSpec::new().with_author_hex("not-hex"); 286 let bad_author_result = bad_author.to_nostrdb_filter(); 287 assert!(matches!( 288 bad_author_result, 289 Err(RadrootsNostrdbError::InvalidHex { 290 field: "author", 291 .. 292 }) 293 )); 294 } 295 }