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:
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"),
],
"",