nostrdb-rs

nostrdb in rust!
git clone git://jb55.com/nostrdb-rs
Log | Files | Refs | Submodules | README | LICENSE

commit 5010624faf49f174e0a1761b535ad65c1644ecc3
parent ebd8109ec7f5081bf30982fd83d30f9c61435ac5
Author: William Casarin <jb55@jb55.com>
Date:   Tue,  2 Apr 2024 21:12:29 +0200

Initial note tag support

This commit adds support for iterating over note tags and tag elements.
This is zero-copy and heavily leverages rust lifetimes to ensure we
always have access within a transaction and note reference.

New types
=========

NdbStr - ndb note strings, offsets into the note string table
NdbStrVariant - ndb note string variants. can be 32-byte values or strings
Tag - A tag ["e", "abcdef..."]
Tags - Note tags [["hi", "3"], ["proxy", "..."]]
TagIter - An iterator over tag elements, producing NdbStr's
TagsIter - An iterator over tags elements, producing Tag's

Usage
=====

for tag in note.tags().iter() {
    for nstr in tag.iter() {
        match nstr.variant() {
	    NdbStrVariant::Str(s) => // string
	    NdbStrVariant::Id(s) =>  // 32-byte id
	}
    }
}

Changelog-Added: Add tags and tag iterators to Note
Fixes: https://github.com/damus-io/nostrdb-rs/issues/2
Cc: yukikishimoto@protonmail.com

Diffstat:
Msrc/bindings.rs | 2+-
Msrc/lib.rs | 3+++
Msrc/ndb.rs | 16++++++++--------
Asrc/ndb_str.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/note.rs | 8+++++++-
Asrc/tags.rs | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 294 insertions(+), 10 deletions(-)

diff --git a/src/bindings.rs b/src/bindings.rs @@ -1,4 +1,4 @@ -/* automatically generated by rust-bindgen 0.69.2 */ +/* automatically generated by rust-bindgen 0.69.1 */ #[repr(C)] #[derive(Default)] diff --git a/src/lib.rs b/src/lib.rs @@ -13,11 +13,13 @@ mod config; mod error; mod filter; mod ndb; +mod ndb_str; mod note; mod profile; mod query; mod result; mod subscription; +mod tags; mod transaction; pub use block::{Block, BlockType, Blocks, Mention}; @@ -26,6 +28,7 @@ pub use error::Error; pub use filter::Filter; pub use ndb::Ndb; pub use ndb_profile::{NdbProfile, NdbProfileRecord}; +pub use ndb_str::{NdbStr, NdbStrVariant}; pub use note::{Note, NoteKey}; pub use profile::ProfileRecord; pub use query::QueryResult; diff --git a/src/ndb.rs b/src/ndb.rs @@ -129,7 +129,7 @@ impl Ndb { } } - pub fn poll_for_notes(&self, sub: &Subscription, max_notes: u32) -> Vec<u64> { + pub fn poll_for_notes(&self, sub: &Subscription, max_notes: u32) -> Vec<NoteKey> { let mut vec = vec![]; vec.reserve_exact(max_notes as usize); let sub_id = sub.id; @@ -144,14 +144,14 @@ impl Ndb { vec.set_len(res as usize); }; - vec + vec.into_iter().map(|n| NoteKey::new(n)).collect() } - pub async fn wait_for_notes(&self, sub: &Subscription, max_notes: u32) -> Result<Vec<u64>> { + pub async fn wait_for_notes(&self, sub: &Subscription, max_notes: u32) -> Result<Vec<NoteKey>> { let ndb = self.clone(); let sub_id = sub.id; let handle = task::spawn_blocking(move || { - let mut vec = vec![]; + let mut vec: Vec<u64> = vec![]; vec.reserve_exact(max_notes as usize); let res = unsafe { bindings::ndb_wait_for_notes( @@ -172,7 +172,7 @@ impl Ndb { }); match handle.await { - Ok(Ok(res)) => Ok(res), + Ok(Ok(res)) => Ok(res.into_iter().map(|n| NoteKey::new(n)).collect()), Ok(Err(err)) => Err(err), Err(_) => Err(Error::SubscriptionError), } @@ -335,7 +335,7 @@ mod tests { let waiter = ndb.wait_for_notes(&sub, 1); ndb.process_event(r#"["EVENT","b",{"id": "702555e52e82cc24ad517ba78c21879f6e47a7c0692b9b20df147916ae8731a3","pubkey": "32bf915904bfde2d136ba45dde32c88f4aca863783999faea2e847a8fafd2f15","created_at": 1702675561,"kind": 1,"tags": [],"content": "hello, world","sig": "2275c5f5417abfd644b7bc74f0388d70feb5d08b6f90fa18655dda5c95d013bfbc5258ea77c05b7e40e0ee51d8a2efa931dc7a0ec1db4c0a94519762c6625675"}]"#).expect("process ok"); let res = waiter.await.expect("await ok"); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); let txn = Transaction::new(&ndb).expect("txn"); let res = ndb.query(&txn, filters, 1).expect("query ok"); assert_eq!(res.len(), 1); @@ -360,7 +360,7 @@ mod tests { let waiter = ndb.wait_for_notes(&sub, 1); ndb.process_event(r#"["EVENT","b",{"id": "702555e52e82cc24ad517ba78c21879f6e47a7c0692b9b20df147916ae8731a3","pubkey": "32bf915904bfde2d136ba45dde32c88f4aca863783999faea2e847a8fafd2f15","created_at": 1702675561,"kind": 1,"tags": [],"content": "hello, world","sig": "2275c5f5417abfd644b7bc74f0388d70feb5d08b6f90fa18655dda5c95d013bfbc5258ea77c05b7e40e0ee51d8a2efa931dc7a0ec1db4c0a94519762c6625675"}]"#).expect("process ok"); let res = waiter.await.expect("await ok"); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); } } @@ -383,7 +383,7 @@ mod tests { std::thread::sleep(std::time::Duration::from_millis(100)); // now we should have something let res = ndb.poll_for_notes(&sub, 1); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); } } diff --git a/src/ndb_str.rs b/src/ndb_str.rs @@ -0,0 +1,54 @@ +use crate::{bindings, Note}; + +pub struct NdbStr<'a> { + ndb_str: bindings::ndb_str, + note: &'a Note<'a>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NdbStrVariant<'a> { + Id(&'a [u8; 32]), + Str(&'a str), +} + +impl bindings::ndb_str { + pub fn str(&self) -> *const ::std::os::raw::c_char { + unsafe { self.__bindgen_anon_1.str_ } + } + + pub fn id(&self) -> *const ::std::os::raw::c_uchar { + unsafe { self.__bindgen_anon_1.id } + } +} + +impl<'a> NdbStr<'a> { + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub(crate) fn new(ndb_str: bindings::ndb_str, note: &'a Note<'a>) -> Self { + NdbStr { ndb_str, note } + } + + pub fn len(&self) -> usize { + if self.ndb_str.flag == (bindings::NDB_PACKED_ID as u8) { + 32 + } else { + unsafe { libc::strlen(self.ndb_str.str()) } + } + } + + pub fn variant(&self) -> NdbStrVariant<'a> { + if self.ndb_str.flag == (bindings::NDB_PACKED_ID as u8) { + unsafe { NdbStrVariant::Id(&*(self.ndb_str.id() as *const [u8; 32])) } + } else { + let s = unsafe { + let byte_slice = + std::slice::from_raw_parts(self.ndb_str.str() as *const u8, self.len()); + std::str::from_utf8_unchecked(byte_slice) + }; + + NdbStrVariant::Str(s) + } + } +} diff --git a/src/note.rs b/src/note.rs @@ -1,4 +1,5 @@ use crate::bindings; +use crate::tags::Tags; use crate::transaction::Transaction; use std::hash::Hash; @@ -15,7 +16,7 @@ impl NoteKey { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Note<'a> { /// A note in-memory outside of nostrdb. This note is a pointer to a note in /// memory and will be free'd when [Drop]ped. Method such as [Note::from_json] @@ -135,6 +136,11 @@ impl<'a> Note<'a> { pub fn kind(&self) -> u32 { unsafe { bindings::ndb_note_kind(self.as_ptr()) } } + + pub fn tags(&'a self) -> Tags<'a> { + let tags = unsafe { bindings::ndb_note_tags(self.as_ptr()) }; + Tags::new(tags, self) + } } impl<'a> Drop for Note<'a> { diff --git a/src/tags.rs b/src/tags.rs @@ -0,0 +1,221 @@ +use crate::{bindings, NdbStr, Note}; + +#[derive(Debug, Copy, Clone)] +pub struct Tag<'a> { + ptr: *mut bindings::ndb_tag, + note: &'a Note<'a>, +} + +impl<'a> Tag<'a> { + pub(crate) fn new(ptr: *mut bindings::ndb_tag, note: &'a Note<'a>) -> Self { + Tag { ptr, note } + } + + pub fn count(&self) -> u16 { + unsafe { bindings::ndb_tag_count(self.as_ptr()) } + } + + pub fn get(&self, ind: u16) -> Option<NdbStr<'a>> { + if ind >= self.count() { + return None; + } + let nstr = unsafe { + bindings::ndb_tag_str( + self.note().as_ptr(), + self.as_ptr(), + ind as ::std::os::raw::c_int, + ) + }; + Some(NdbStr::new(nstr, self.note)) + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *mut bindings::ndb_tag { + self.ptr + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Tags<'a> { + ptr: *mut bindings::ndb_tags, + note: &'a Note<'a>, +} + +impl<'a> Tags<'a> { + pub(crate) fn new(ptr: *mut bindings::ndb_tags, note: &'a Note<'a>) -> Self { + Tags { ptr, note } + } + + pub fn count(&self) -> u16 { + unsafe { bindings::ndb_tags_count(self.as_ptr()) } + } + + pub fn iter(&self) -> TagsIter<'a> { + TagsIter::new(self.note) + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *mut bindings::ndb_tags { + self.ptr + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TagsIter<'a> { + iter: bindings::ndb_iterator, + note: &'a Note<'a>, +} + +impl<'a> TagsIter<'a> { + pub fn new(note: &'a Note<'a>) -> Self { + let iter = bindings::ndb_iterator { + note: std::ptr::null_mut(), + tag: std::ptr::null_mut(), + index: 0, + }; + let mut iter = TagsIter { note, iter }; + unsafe { + bindings::ndb_tags_iterate_start(note.as_ptr(), &mut iter.iter); + }; + iter + } + + pub fn tag(&self) -> Option<Tag<'a>> { + let tag_ptr = unsafe { *self.as_ptr() }.tag; + if tag_ptr.is_null() { + None + } else { + Some(Tag::new(tag_ptr, self.note())) + } + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *const bindings::ndb_iterator { + &self.iter + } + + pub fn as_mut_ptr(&mut self) -> *mut bindings::ndb_iterator { + &mut self.iter + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TagIter<'a> { + tag: Tag<'a>, + index: u16, +} + +impl<'a> TagIter<'a> { + pub fn new(tag: Tag<'a>) -> Self { + let index = 0; + TagIter { tag, index } + } + + pub fn done(&self) -> bool { + self.index >= self.tag.count() + } +} + +impl<'a> Iterator for TagIter<'a> { + type Item = NdbStr<'a>; + + fn next(&mut self) -> Option<Self::Item> { + let tag = self.tag.get(self.index); + if tag.is_some() { + self.index += 1; + tag + } else { + None + } + } +} + +impl<'a> Iterator for TagsIter<'a> { + type Item = Tag<'a>; + + fn next(&mut self) -> Option<Self::Item> { + unsafe { + bindings::ndb_tags_iterate_next(self.as_mut_ptr()); + }; + self.tag() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::test_util; + use crate::{Filter, Ndb, NdbStrVariant, Transaction}; + + #[tokio::test] + async fn tag_iter_works() { + let db = "target/testdbs/tag_iter_works"; + test_util::cleanup_db(&db); + + { + let ndb = Ndb::new(db, &Config::new()).expect("ndb"); + let sub = ndb + .subscribe(vec![Filter::new() + .ids(vec![[ + 0xc5, 0xd9, 0x8c, 0xbf, 0x4b, 0xcd, 0x81, 0x1e, 0x28, 0x66, 0x77, 0x0c, + 0x3d, 0x38, 0x0c, 0x02, 0x84, 0xce, 0x1d, 0xaf, 0x3a, 0xe9, 0x98, 0x3d, + 0x22, 0x56, 0x5c, 0xb0, 0x66, 0xcf, 0x2a, 0x19, + ]]) + .build()]) + .expect("sub"); + let waiter = ndb.wait_for_notes(&sub, 1); + ndb.process_event(r#"["EVENT","s",{"id": "c5d98cbf4bcd811e2866770c3d380c0284ce1daf3ae9983d22565cb066cf2a19","pubkey": "083727b7a6051673f399102dc48c229c0ec08186ecd7e54ad0e9116d38429c4f","created_at": 1712517119,"kind": 1,"tags": [["e","b9e548b4aa30fa4ce9edf552adaf458385716704994fbaa9e0aa0042a5a5e01e"],["p","140ee9ff21da6e6671f750a0a747c5a3487ee8835159c7ca863e867a1c537b4f"],["hi","3"]],"content": "hi","sig": "1eed792e4db69c2bde2f5be33a383ef8b17c6afd1411598d0c4618fbdf4dbcb9689354276a74614511907a45eec234e0786733e8a6fbb312e6abf153f15fd437"}]"#).expect("process ok"); + let res = waiter.await.expect("await ok"); + assert_eq!(res.len(), 1); + let note_key = res[0]; + let txn = Transaction::new(&ndb).expect("txn"); + let note = ndb.get_note_by_key(&txn, note_key).expect("note"); + let tags = note.tags(); + assert_eq!(tags.count(), 3); + + let mut tags_iter = tags.iter(); + + let t0 = tags_iter.next().expect("t0"); + let t0_e0 = t0.get(0).expect("e tag ok"); + let t0_e1 = t0.get(1).expect("e id ok"); + assert_eq!(t0_e0.variant(), NdbStrVariant::Str("e")); + assert_eq!( + t0_e1.variant(), + NdbStrVariant::Id(&[ + 0xb9, 0xe5, 0x48, 0xb4, 0xaa, 0x30, 0xfa, 0x4c, 0xe9, 0xed, 0xf5, 0x52, 0xad, + 0xaf, 0x45, 0x83, 0x85, 0x71, 0x67, 0x04, 0x99, 0x4f, 0xba, 0xa9, 0xe0, 0xaa, + 0x00, 0x42, 0xa5, 0xa5, 0xe0, 0x1e + ]) + ); + + let t1 = tags_iter.next().expect("t1"); + let t1_e0 = t1.get(0).expect("p tag ok"); + let t1_e1 = t1.get(1).expect("p id ok"); + assert_eq!(t1_e0.variant(), NdbStrVariant::Str("p")); + assert_eq!( + t1_e1.variant(), + NdbStrVariant::Id(&[ + 0x14, 0x0e, 0xe9, 0xff, 0x21, 0xda, 0x6e, 0x66, 0x71, 0xf7, 0x50, 0xa0, 0xa7, + 0x47, 0xc5, 0xa3, 0x48, 0x7e, 0xe8, 0x83, 0x51, 0x59, 0xc7, 0xca, 0x86, 0x3e, + 0x86, 0x7a, 0x1c, 0x53, 0x7b, 0x4f + ]) + ); + + let t2 = tags_iter.next().expect("t2"); + let t2_e0 = t2.get(0).expect("hi tag ok"); + let t2_e1 = t2.get(1).expect("hi value ok"); + assert_eq!(t2_e0.variant(), NdbStrVariant::Str("hi")); + assert_eq!(t2_e1.variant(), NdbStrVariant::Str("3")); + } + } +}