lib

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

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 }