lib

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

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 }