filter.rs (7715B)
1 use crate::error::RadrootsNostrNdbError; 2 3 #[derive(Debug, Clone, Eq, PartialEq, Default)] 4 pub struct RadrootsNostrNdbFilterSpec { 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 RadrootsNostrNdbFilterSpec { 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_ndb_filter(&self) -> Result<nostrdb::Filter, RadrootsNostrNdbError> { 94 let mut builder = nostrdb::Filter::new(); 95 96 if !self.event_ids_hex.is_empty() { 97 let event_ids = self 98 .event_ids_hex 99 .iter() 100 .map(|hex_value| parse_hex_32(hex_value, "event_id")) 101 .collect::<Result<Vec<_>, _>>()?; 102 builder = builder.ids(event_ids.iter()); 103 } 104 105 if !self.authors_hex.is_empty() { 106 let authors = self 107 .authors_hex 108 .iter() 109 .map(|hex_value| parse_hex_32(hex_value, "author")) 110 .collect::<Result<Vec<_>, _>>()?; 111 builder = builder.authors(authors.iter()); 112 } 113 114 if !self.kinds.is_empty() { 115 builder = builder.kinds(self.kinds.iter().map(|kind| *kind as u64)); 116 } 117 118 if let Some(since_unix) = self.since_unix { 119 builder = builder.since(since_unix); 120 } 121 122 if let Some(until_unix) = self.until_unix { 123 builder = builder.until(until_unix); 124 } 125 126 if let Some(limit) = self.limit { 127 builder = builder.limit(limit); 128 } 129 130 if let Some(search) = self.search() { 131 builder = builder.search(search); 132 } 133 134 Ok(builder.build()) 135 } 136 } 137 138 pub(crate) fn parse_hex_32( 139 value: &str, 140 field: &'static str, 141 ) -> Result<[u8; 32], RadrootsNostrNdbError> { 142 let bytes = hex::decode(value).map_err(|source| RadrootsNostrNdbError::InvalidHex { 143 field, 144 reason: source.to_string(), 145 })?; 146 147 if bytes.len() != 32 { 148 return Err(RadrootsNostrNdbError::InvalidHexLength { 149 field, 150 expected: 32, 151 actual: bytes.len(), 152 }); 153 } 154 155 let mut out = [0u8; 32]; 156 out.copy_from_slice(bytes.as_slice()); 157 Ok(out) 158 } 159 160 #[cfg(test)] 161 mod tests { 162 use super::*; 163 164 fn valid_hex_32(value: u8) -> String { 165 format!("{value:02x}").repeat(32) 166 } 167 168 #[test] 169 fn filter_spec_builders_and_accessors_round_trip() { 170 let event_id = valid_hex_32(0x11); 171 let author = valid_hex_32(0x22); 172 173 let empty_notes = RadrootsNostrNdbFilterSpec::text_notes(None, None); 174 assert_eq!(empty_notes.kinds(), &[1]); 175 assert_eq!(empty_notes.limit(), None); 176 assert_eq!(empty_notes.since_unix(), None); 177 178 let spec = RadrootsNostrNdbFilterSpec::text_notes(Some(50), Some(100)) 179 .with_event_id_hex(event_id.clone()) 180 .with_author_hex(author.clone()) 181 .with_kind(30023) 182 .with_since_unix(200) 183 .with_until_unix(300) 184 .with_limit(10) 185 .with_search("coffee"); 186 187 assert_eq!(spec.event_ids_hex(), &[event_id.clone()]); 188 assert_eq!(spec.authors_hex(), &[author.clone()]); 189 assert_eq!(spec.kinds(), &[1, 30023]); 190 assert_eq!(spec.since_unix(), Some(200)); 191 assert_eq!(spec.until_unix(), Some(300)); 192 assert_eq!(spec.limit(), Some(10)); 193 assert_eq!(spec.search(), Some("coffee")); 194 195 let empty = RadrootsNostrNdbFilterSpec::new(); 196 let _ = empty.to_ndb_filter().expect("empty ndb filter"); 197 } 198 199 #[test] 200 fn to_ndb_filter_builds_supported_success_paths() { 201 let event_id = valid_hex_32(0x11); 202 let author = valid_hex_32(0x22); 203 204 let _ = RadrootsNostrNdbFilterSpec::new() 205 .with_event_id_hex(event_id) 206 .to_ndb_filter() 207 .expect("event id filter"); 208 209 let _ = RadrootsNostrNdbFilterSpec::new() 210 .with_author_hex(author) 211 .to_ndb_filter() 212 .expect("author filter"); 213 214 let _ = RadrootsNostrNdbFilterSpec::new() 215 .with_kind(1) 216 .with_since_unix(200) 217 .with_until_unix(300) 218 .with_limit(10) 219 .to_ndb_filter() 220 .expect("range filter"); 221 222 let _ = RadrootsNostrNdbFilterSpec::new() 223 .with_search("coffee") 224 .to_ndb_filter() 225 .expect("search filter"); 226 } 227 228 #[test] 229 fn parse_hex_32_validates_input() { 230 let valid = parse_hex_32(valid_hex_32(0xab).as_str(), "value").expect("valid"); 231 assert_eq!(valid, [0xab; 32]); 232 233 let invalid_hex = parse_hex_32("zz", "value"); 234 assert!(matches!( 235 invalid_hex, 236 Err(RadrootsNostrNdbError::InvalidHex { field: "value", .. }) 237 )); 238 239 let invalid_len = parse_hex_32("abcd", "value"); 240 assert!(matches!( 241 invalid_len, 242 Err(RadrootsNostrNdbError::InvalidHexLength { 243 field: "value", 244 expected: 32, 245 .. 246 }) 247 )); 248 } 249 250 #[test] 251 fn to_ndb_filter_rejects_invalid_event_id_and_author_hex() { 252 let bad_event_id = RadrootsNostrNdbFilterSpec::new().with_event_id_hex("not-hex"); 253 let bad_event_result = bad_event_id.to_ndb_filter(); 254 assert!(matches!( 255 bad_event_result, 256 Err(RadrootsNostrNdbError::InvalidHex { 257 field: "event_id", 258 .. 259 }) 260 )); 261 262 let bad_author = RadrootsNostrNdbFilterSpec::new().with_author_hex("not-hex"); 263 let bad_author_result = bad_author.to_ndb_filter(); 264 assert!(matches!( 265 bad_author_result, 266 Err(RadrootsNostrNdbError::InvalidHex { 267 field: "author", 268 .. 269 }) 270 )); 271 } 272 }