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:
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"));
+ }
+ }
+}