damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

NdbFilter.swift (14045B)


      1 //
      2 //  NdbFilter.swift
      3 //  damus
      4 //
      5 //  Created by Daniel D'Aquino on 2025-06-02.
      6 //
      7 
      8 import Foundation
      9 
     10 /// A safe Swift wrapper around `UnsafeMutablePointer<ndb_filter>` that manages memory automatically.
     11 ///
     12 /// This class provides a safe interface to the underlying C `ndb_filter` structure, handling
     13 /// memory allocation and deallocation automatically. It eliminates the need for manual memory
     14 /// management when working with NostrDB filters.
     15 ///
     16 /// ## Usage
     17 /// ```swift
     18 /// let nostrFilter = NostrFilter(kinds: [.text_note])
     19 /// let ndbFilter = try NdbFilter(from: nostrFilter)
     20 /// // Use ndbFilter.ndbFilter or ndbFilter.unsafePointer as needed
     21 /// // Memory is automatically cleaned up when ndbFilter goes out of scope
     22 /// ```
     23 class NdbFilter {
     24     private let filterPointer: UnsafeMutablePointer<ndb_filter>
     25     
     26     /// Creates a new NdbFilter from a NostrFilter.
     27     /// - Parameter nostrFilter: The NostrFilter to convert
     28     /// - Throws: `NdbFilterError.conversionFailed` if the underlying conversion fails
     29     init(from nostrFilter: NostrFilter) throws {
     30         do {
     31             self.filterPointer = try Self.from(nostrFilter: nostrFilter)
     32         } catch {
     33             throw NdbFilterError.conversionFailed(error)
     34         }
     35     }
     36     
     37     /// Provides access to the underlying `ndb_filter` structure.
     38     /// - Returns: The underlying `ndb_filter` value (not a pointer)
     39     var ndbFilter: ndb_filter {
     40         return filterPointer.pointee
     41     }
     42     
     43     /// Provides access to the underlying unsafe pointer when needed for C interop.
     44     /// - Warning: The caller must not deallocate this pointer. It will be automatically 
     45     ///           deallocated when this NdbFilter is destroyed.
     46     /// - Returns: The unsafe mutable pointer to the underlying ndb_filter
     47     var unsafePointer: UnsafeMutablePointer<ndb_filter> {
     48         return filterPointer
     49     }
     50     
     51     /// Creates multiple NdbFilter instances from an array of NostrFilters.
     52     /// - Parameter nostrFilters: Array of NostrFilter instances to convert
     53     /// - Returns: Array of NdbFilter instances
     54     /// - Throws: `NdbFilterError.conversionFailed` if any conversion fails
     55     static func create(from nostrFilters: [NostrFilter]) throws -> [NdbFilter] {
     56         return try nostrFilters.map { try NdbFilter(from: $0) }
     57     }
     58     
     59     // MARK: - Conversion to/from ndb_filter
     60     
     61     // TODO: This function is long and repetitive, refactor it into something cleaner.
     62     private static func from(nostrFilter: NostrFilter) throws(NdbFilterConversionError) -> UnsafeMutablePointer<ndb_filter> {
     63         let filterPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: 1)
     64 
     65         guard ndb_filter_init(filterPointer) == 1 else {
     66             filterPointer.deallocate()
     67             throw NdbFilterConversionError.failedToInitialize
     68         }
     69         
     70         // Handle `ids` field
     71         if let ids = nostrFilter.ids {
     72             guard ndb_filter_start_field(filterPointer, NDB_FILTER_IDS) == 1 else {
     73                 ndb_filter_destroy(filterPointer)
     74                 filterPointer.deallocate()
     75                 throw NdbFilterConversionError.failedToStartField
     76             }
     77             
     78             for noteId in ids {
     79                 do {
     80                     try noteId.withUnsafePointer({ idPointer in
     81                         if ndb_filter_add_id_element(filterPointer, idPointer) != 1 {
     82                             ndb_filter_destroy(filterPointer)
     83                             filterPointer.deallocate()
     84                             throw NdbFilterConversionError.failedToAddElement
     85                         }
     86                     })
     87                 }
     88                 catch {
     89                     ndb_filter_destroy(filterPointer)
     90                     filterPointer.deallocate()
     91                     throw NdbFilterConversionError.failedToAddElement
     92                 }
     93             }
     94             
     95             ndb_filter_end_field(filterPointer)
     96         }
     97         
     98         // Handle `kinds` field
     99         if let kinds = nostrFilter.kinds {
    100             guard ndb_filter_start_field(filterPointer, NDB_FILTER_KINDS) == 1 else {
    101                 ndb_filter_destroy(filterPointer)
    102                 filterPointer.deallocate()
    103                 throw NdbFilterConversionError.failedToStartField
    104             }
    105             
    106             for kind in kinds {
    107                 if ndb_filter_add_int_element(filterPointer, UInt64(kind.rawValue)) != 1 {
    108                     ndb_filter_destroy(filterPointer)
    109                     filterPointer.deallocate()
    110                     throw NdbFilterConversionError.failedToAddElement
    111                 }
    112             }
    113             
    114             ndb_filter_end_field(filterPointer)
    115         }
    116         
    117         // Handle `referenced_ids` field
    118         if let referencedIds = nostrFilter.referenced_ids {
    119             guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("e").value)) == 1 else {
    120                 ndb_filter_destroy(filterPointer)
    121                 filterPointer.deallocate()
    122                 throw NdbFilterConversionError.failedToStartField
    123             }
    124             
    125             for refId in referencedIds {
    126                 do {
    127                     try refId.withUnsafePointer({ refPointer in
    128                         if ndb_filter_add_id_element(filterPointer, refPointer) != 1 {
    129                             ndb_filter_destroy(filterPointer)
    130                             filterPointer.deallocate()
    131                             throw NdbFilterConversionError.failedToAddElement
    132                         }
    133                     })
    134                 }
    135                 catch {
    136                     ndb_filter_destroy(filterPointer)
    137                     filterPointer.deallocate()
    138                     throw NdbFilterConversionError.failedToAddElement
    139                 }
    140             }
    141             
    142             ndb_filter_end_field(filterPointer)
    143         }
    144 
    145         // Handle `pubkeys`
    146         if let pubkeys = nostrFilter.pubkeys {
    147             guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("p").value)) == 1 else {
    148                 ndb_filter_destroy(filterPointer)
    149                 filterPointer.deallocate()
    150                 throw NdbFilterConversionError.failedToStartField
    151             }
    152 
    153             for pubkey in pubkeys {
    154                 do {
    155                     try pubkey.withUnsafePointer({ pubkeyPointer in
    156                         if ndb_filter_add_id_element(filterPointer, pubkeyPointer) != 1 {
    157                             ndb_filter_destroy(filterPointer)
    158                             filterPointer.deallocate()
    159                             throw NdbFilterConversionError.failedToAddElement
    160                         }
    161                     })
    162                 }
    163                 catch {
    164                     ndb_filter_destroy(filterPointer)
    165                     filterPointer.deallocate()
    166                     throw NdbFilterConversionError.failedToAddElement
    167                 }
    168             }
    169             
    170             ndb_filter_end_field(filterPointer)
    171         }
    172         
    173         // Handle `since`
    174         if let since = nostrFilter.since {
    175             if ndb_filter_start_field(filterPointer, NDB_FILTER_SINCE) != 1 {
    176                 ndb_filter_destroy(filterPointer)
    177                 filterPointer.deallocate()
    178                 throw NdbFilterConversionError.failedToAddElement
    179             }
    180             
    181             if ndb_filter_add_int_element(filterPointer, UInt64(since)) != 1 {
    182                 ndb_filter_destroy(filterPointer)
    183                 filterPointer.deallocate()
    184                 throw NdbFilterConversionError.failedToAddElement
    185             }
    186             
    187             ndb_filter_end_field(filterPointer)
    188         }
    189 
    190         // Handle `until`
    191         if let until = nostrFilter.until {
    192             if ndb_filter_start_field(filterPointer, NDB_FILTER_UNTIL) != 1 {
    193                 ndb_filter_destroy(filterPointer)
    194                 filterPointer.deallocate()
    195                 throw NdbFilterConversionError.failedToAddElement
    196             }
    197             
    198             if ndb_filter_add_int_element(filterPointer, UInt64(until)) != 1 {
    199                 ndb_filter_destroy(filterPointer)
    200                 filterPointer.deallocate()
    201                 throw NdbFilterConversionError.failedToAddElement
    202             }
    203             
    204             ndb_filter_end_field(filterPointer)
    205         }
    206 
    207         // Handle `limit`
    208         if let limit = nostrFilter.limit {
    209             if ndb_filter_start_field(filterPointer, NDB_FILTER_LIMIT) != 1 {
    210                 ndb_filter_destroy(filterPointer)
    211                 filterPointer.deallocate()
    212                 throw NdbFilterConversionError.failedToAddElement
    213             }
    214             
    215             if ndb_filter_add_int_element(filterPointer, UInt64(limit)) != 1 {
    216                 ndb_filter_destroy(filterPointer)
    217                 filterPointer.deallocate()
    218                 throw NdbFilterConversionError.failedToAddElement
    219             }
    220             
    221             ndb_filter_end_field(filterPointer)
    222         }
    223         
    224         // Handle `authors`
    225         if let authors = nostrFilter.authors {
    226             guard ndb_filter_start_field(filterPointer, NDB_FILTER_AUTHORS) == 1 else {
    227                 ndb_filter_destroy(filterPointer)
    228                 filterPointer.deallocate()
    229                 throw NdbFilterConversionError.failedToStartField
    230             }
    231 
    232             for author in authors {
    233                 do {
    234                     try author.withUnsafePointer({ authorPointer in
    235                         if ndb_filter_add_id_element(filterPointer, authorPointer) != 1 {
    236                             ndb_filter_destroy(filterPointer)
    237                             filterPointer.deallocate()
    238                             throw NdbFilterConversionError.failedToAddElement
    239                         }
    240                     })
    241                 }
    242                 catch {
    243                     ndb_filter_destroy(filterPointer)
    244                     filterPointer.deallocate()
    245                     throw NdbFilterConversionError.failedToAddElement
    246                 }
    247                 
    248             }
    249             
    250             ndb_filter_end_field(filterPointer)
    251         }
    252         
    253         // Handle `hashtag`
    254         if let hashtags = nostrFilter.hashtag {
    255             guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("t").value)) == 1 else {
    256                 ndb_filter_destroy(filterPointer)
    257                 filterPointer.deallocate()
    258                 throw NdbFilterConversionError.failedToStartField
    259             }
    260 
    261             for tag in hashtags {
    262                 if ndb_filter_add_str_element(filterPointer, tag.cString(using: .utf8)) != 1 {
    263                     ndb_filter_destroy(filterPointer)
    264                     filterPointer.deallocate()
    265                     throw NdbFilterConversionError.failedToAddElement
    266                 }
    267             }
    268             ndb_filter_end_field(filterPointer)
    269         }
    270         
    271         // Handle `parameter`
    272         if let parameters = nostrFilter.parameter {
    273             guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("d").value)) == 1 else {
    274                 ndb_filter_destroy(filterPointer)
    275                 filterPointer.deallocate()
    276                 throw NdbFilterConversionError.failedToStartField
    277             }
    278 
    279             for parameter in parameters {
    280                 if ndb_filter_add_str_element(filterPointer, parameter.cString(using: .utf8)) != 1 {
    281                     ndb_filter_destroy(filterPointer)
    282                     filterPointer.deallocate()
    283                     throw NdbFilterConversionError.failedToAddElement
    284                 }
    285             }
    286             ndb_filter_end_field(filterPointer)
    287         }
    288 
    289         // Handle `quotes`
    290         if let quotes = nostrFilter.quotes {
    291             guard ndb_filter_start_tag_field(filterPointer, CChar(UnicodeScalar("q").value)) == 1 else {
    292                 ndb_filter_destroy(filterPointer)
    293                 filterPointer.deallocate()
    294                 throw NdbFilterConversionError.failedToStartField
    295             }
    296             
    297             for quote in quotes {
    298                 do {
    299                     try quote.withUnsafePointer({ quotePointer in
    300                         if ndb_filter_add_id_element(filterPointer, quotePointer) != 1 {
    301                             ndb_filter_destroy(filterPointer)
    302                             filterPointer.deallocate()
    303                             throw NdbFilterConversionError.failedToAddElement
    304                         }
    305                     })
    306                 }
    307                 catch {
    308                     ndb_filter_destroy(filterPointer)
    309                     filterPointer.deallocate()
    310                     throw NdbFilterConversionError.failedToAddElement
    311                 }
    312                 
    313             }
    314             
    315             ndb_filter_end_field(filterPointer)
    316         }
    317 
    318         // Finalize the filter
    319         guard ndb_filter_end(filterPointer) == 1 else {
    320             ndb_filter_destroy(filterPointer)
    321             filterPointer.deallocate()
    322             throw NdbFilterConversionError.failedToFinalize
    323         }
    324 
    325         return filterPointer
    326     }
    327 
    328     enum NdbFilterConversionError: Error {
    329         case failedToInitialize
    330         case failedToStartField
    331         case failedToAddElement
    332         case failedToFinalize
    333     }
    334     
    335     deinit {
    336         ndb_filter_destroy(filterPointer)
    337         filterPointer.deallocate()
    338     }
    339 }
    340 
    341 /// Errors that can occur when working with NdbFilter.
    342 enum NdbFilterError: Error {
    343     /// Thrown when conversion from NostrFilter to NdbFilter fails.
    344     /// - Parameter Error: The underlying error that caused the conversion to fail
    345     case conversionFailed(Error)
    346 }
    347 
    348 /// Extension to create multiple NdbFilters safely from an array of NostrFilters.
    349 extension Array where Element == NostrFilter {
    350     /// Converts an array of NostrFilters to NdbFilters.
    351     /// - Returns: Array of NdbFilter instances
    352     /// - Throws: `NdbFilterError.conversionFailed` if any conversion fails
    353     func toNdbFilters() throws -> [NdbFilter] {
    354         return try self.map { try NdbFilter(from: $0) }
    355     }
    356 }