Skip to content

Commit

Permalink
Initial note tag support
Browse files Browse the repository at this point in the history
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: #2
Cc: [email protected]
  • Loading branch information
jb55 committed Apr 7, 2024
1 parent ebd8109 commit 5010624
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* automatically generated by rust-bindgen 0.69.2 */
/* automatically generated by rust-bindgen 0.69.1 */

#[repr(C)]
#[derive(Default)]
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions src/ndb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -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),
}
Expand Down Expand Up @@ -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);
Expand All @@ -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)]);
}
}

Expand All @@ -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)]);
}
}

Expand Down
54 changes: 54 additions & 0 deletions src/ndb_str.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
8 changes: 7 additions & 1 deletion src/note.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::bindings;
use crate::tags::Tags;
use crate::transaction::Transaction;
use std::hash::Hash;

Expand All @@ -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]
Expand Down Expand Up @@ -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> {
Expand Down
221 changes: 221 additions & 0 deletions src/tags.rs
Original file line number Diff line number Diff line change
@@ -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"));
}
}
}

0 comments on commit 5010624

Please sign in to comment.