tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit b182c2d7957c22b8c17bb87830befea44c80263e
parent 513ac06a6db8c1d7e37a6e235b0a7f66c96f3ada
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 23:45:23 -0700

groups: extend event view primitives

- add neutral created-at access to group event views
- expose full validated tag values for projection parsing
- implement the new surface for Pocket owned events
- cover Pocket view fields with focused group tests

Diffstat:
Mcrates/tangle_groups/src/event_view.rs | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 118 insertions(+), 15 deletions(-)

diff --git a/crates/tangle_groups/src/event_view.rs b/crates/tangle_groups/src/event_view.rs @@ -1,7 +1,7 @@ use crate::errors::{GroupError, GroupErrorKind}; use pocket_types::{Event as PocketEvent, OwnedEvent as PocketOwnedEvent, TagsStringIter}; use std::str; -use tangle_protocol::{Event, EventId, Kind, PublicKeyHex, Tag, TagName}; +use tangle_protocol::{Event, EventId, Kind, PublicKeyHex, Tag, TagName, UnixTimestamp}; pub trait GroupEventView { fn id_hex(&self) -> String; @@ -10,6 +10,8 @@ pub trait GroupEventView { fn kind_u32(&self) -> u32; + fn created_at_unix(&self) -> u64; + fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError> where F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>; @@ -25,39 +27,50 @@ pub trait GroupEventView { fn kind(&self) -> Result<Kind, GroupError> { Kind::new(u64::from(self.kind_u32())).map_err(event_view_scalar_error) } + + fn created_at(&self) -> UnixTimestamp { + UnixTimestamp::new(self.created_at_unix()) + } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GroupEventTag<'a> { - name: Option<&'a str>, - value: Option<&'a str>, + values: Vec<&'a str>, } impl<'a> GroupEventTag<'a> { pub fn first_value(&self) -> Option<&'a str> { - self.name + self.value(0) + } + + pub fn value(&self, index: usize) -> Option<&'a str> { + self.values.get(index).copied() + } + + pub fn values(&self) -> &[&'a str] { + &self.values } pub fn indexed_pair(&self) -> Option<(&'a str, &'a str)> { - let name = self.name?; + let name = self.first_value()?; if !TagName::is_indexable_name(name) { return None; } - self.value.map(|value| (name, value)) + self.value(1).map(|value| (name, value)) } fn from_tangle(tag: &'a Tag) -> Self { Self { - name: tag.values().first().map(String::as_str), - value: tag.values().get(1).map(String::as_str), + values: tag.values().iter().map(String::as_str).collect(), } } fn from_pocket(mut values: TagsStringIter<'a>) -> Result<Self, GroupError> { - Ok(Self { - name: values.next().map(tag_value_utf8).transpose()?, - value: values.next().map(tag_value_utf8).transpose()?, - }) + let mut tag_values = Vec::new(); + for value in values.by_ref() { + tag_values.push(tag_value_utf8(value)?); + } + Ok(Self { values: tag_values }) } } @@ -74,6 +87,10 @@ impl GroupEventView for Event { self.unsigned().kind().as_u32() } + fn created_at_unix(&self) -> u64 { + self.unsigned().created_at().as_u64() + } + fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError> where F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, @@ -98,6 +115,10 @@ impl GroupEventView for PocketEvent { u32::from(self.kind().as_u16()) } + fn created_at_unix(&self) -> u64 { + self.created_at().as_u64() + } + fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError> where F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, @@ -126,6 +147,11 @@ impl GroupEventView for PocketOwnedEvent { event.kind_u32() } + fn created_at_unix(&self) -> u64 { + let event: &PocketEvent = self; + event.created_at_unix() + } + fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError> where F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>, @@ -158,7 +184,7 @@ fn event_view_scalar_error(error: String) -> GroupError { #[cfg(test)] mod tests { use super::GroupEventView; - use pocket_types::Event as PocketEvent; + use pocket_types::{Event as PocketEvent, OwnedEvent as PocketOwnedEvent}; use tangle_protocol::{ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, event_to_value, @@ -169,12 +195,26 @@ mod tests { let event = event(); assert_eq!(event.kind_u32(), 1); + assert_eq!(event.created_at_unix(), 42); + assert_eq!(event.created_at(), UnixTimestamp::new(42)); assert_eq!(event.id_hex(), "0".repeat(64)); assert_eq!(event.pubkey_hex(), "1".repeat(64)); assert_eq!( indexed_pairs(&event), vec![("h".to_owned(), "Farm".to_owned())] ); + assert_eq!( + tag_values(&event, "role"), + vec![vec!["role".to_owned(), "moderator".to_owned()]] + ); + assert_eq!( + tag_values(&event, "supported_kinds"), + vec![vec![ + "supported_kinds".to_owned(), + "1".to_owned(), + "9".to_owned() + ]] + ); } #[test] @@ -185,12 +225,55 @@ mod tests { let (_, pocket) = PocketEvent::from_json(raw.as_bytes(), &mut buffer).expect("pocket"); assert_eq!(pocket.kind_u32(), 1); + assert_eq!(pocket.created_at_unix(), 42); + assert_eq!( + <PocketEvent as GroupEventView>::created_at(pocket), + UnixTimestamp::new(42) + ); assert_eq!(pocket.id_hex(), "0".repeat(64)); assert_eq!(pocket.pubkey_hex(), "1".repeat(64)); assert_eq!( indexed_pairs(pocket), vec![("h".to_owned(), "Farm".to_owned())] ); + assert_eq!( + tag_values(pocket, "role"), + vec![vec!["role".to_owned(), "moderator".to_owned()]] + ); + assert_eq!( + tag_values(pocket, "supported_kinds"), + vec![vec![ + "supported_kinds".to_owned(), + "1".to_owned(), + "9".to_owned() + ]] + ); + } + + #[test] + fn owned_pocket_event_view_exposes_group_fields() { + let event = event(); + let raw = event_to_value(&event).to_string(); + let mut buffer = vec![0; 4096]; + let (_, pocket) = PocketEvent::from_json(raw.as_bytes(), &mut buffer).expect("pocket"); + let owned: PocketOwnedEvent = pocket.to_owned(); + + assert_eq!(owned.kind_u32(), 1); + assert_eq!(owned.created_at_unix(), 42); + assert_eq!( + <PocketOwnedEvent as GroupEventView>::created_at(&owned), + UnixTimestamp::new(42) + ); + assert_eq!(owned.id_hex(), "0".repeat(64)); + assert_eq!(owned.pubkey_hex(), "1".repeat(64)); + assert_eq!( + tag_values(&owned, "supported_kinds"), + vec![vec![ + "supported_kinds".to_owned(), + "1".to_owned(), + "9".to_owned() + ]] + ); } fn indexed_pairs<E: GroupEventView + ?Sized>(event: &E) -> Vec<(String, String)> { @@ -206,15 +289,35 @@ mod tests { pairs } + fn tag_values<E: GroupEventView + ?Sized>(event: &E, name: &str) -> Vec<Vec<String>> { + let mut values = Vec::new(); + event + .visit_tags(|tag| { + if tag.first_value().is_some_and(|value| value == name) { + values.push( + tag.values() + .iter() + .map(|value| (*value).to_owned()) + .collect(), + ); + } + Ok(()) + }) + .expect("visit tags"); + values + } + fn event() -> Event { Event::new( EventId::new(&"0".repeat(64)).expect("id"), UnsignedEvent::new( PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"), - UnixTimestamp::new(1), + UnixTimestamp::new(42), Kind::new(1).expect("kind"), vec![ Tag::from_parts("h", &["Farm"]).expect("h"), + Tag::from_parts("role", &["moderator"]).expect("role"), + Tag::from_parts("supported_kinds", &["1", "9"]).expect("supported kinds"), Tag::from_parts("summary", &["Harvest"]).expect("summary"), ], "",