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 }