commit bd21aa73a6f854389475cd9c77cf932ca4ed717c
parent f27291c96996502fed932fc578631d38b12c7da9
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 03:51:08 -0700
groups: introduce pocket event view
- Add a group-facing EventView trait with adapters for Tangle protocol events and Pocket events.
- Expose borrowed group tag access for indexed pairs and full tag values without runtime JSON reparse.
- Pin pocket-types directly in tangle_groups using the approved triesap/pocket revision.
- Validated with cargo fmt --all -- --check, cargo check --workspace --all-targets, cargo test --workspace, and cargo clippy --workspace --all-targets -- -D warnings.
Diffstat:
4 files changed, 247 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1240,6 +1240,7 @@ dependencies = [
name = "tangle_groups"
version = "0.1.0"
dependencies = [
+ "pocket-types",
"serde",
"serde_json",
"tangle_crypto",
diff --git a/crates/tangle_groups/Cargo.toml b/crates/tangle_groups/Cargo.toml
@@ -8,6 +8,7 @@ license.workspace = true
description = "NIP-29 group domain and configuration types for tangle"
[dependencies]
+pocket-types = { git = "https://github.com/triesap/pocket", rev = "329334f20948c796c6016b673b92551ac4855ad7" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tangle_crypto = { path = "../tangle_crypto" }
diff --git a/crates/tangle_groups/src/event_view.rs b/crates/tangle_groups/src/event_view.rs
@@ -0,0 +1,243 @@
+use crate::errors::{GroupError, GroupErrorKind};
+use pocket_types::{
+ Event as PocketEvent, OwnedEvent as PocketOwnedEvent, TagsStringIter as PocketTagsStringIter,
+};
+use std::str;
+use tangle_protocol::{Event, Tag, TagName};
+
+pub trait GroupEventView {
+ fn id_hex(&self) -> String;
+
+ fn pubkey_hex(&self) -> String;
+
+ fn kind_u32(&self) -> u32;
+
+ fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError>
+ where
+ F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>;
+}
+
+#[derive(Debug)]
+pub enum GroupEventTag<'a> {
+ Tangle(&'a Tag),
+ Pocket(PocketTagsStringIter<'a>),
+}
+
+impl<'a> GroupEventTag<'a> {
+ pub fn first_value(self) -> Result<Option<&'a str>, GroupError> {
+ match self {
+ Self::Tangle(tag) => Ok(tag.values().first().map(String::as_str)),
+ Self::Pocket(mut values) => values.next().map(tag_value_utf8).transpose(),
+ }
+ }
+
+ pub fn indexed_pair(self) -> Result<Option<(&'a str, &'a str)>, GroupError> {
+ match self {
+ Self::Tangle(tag) => Ok(tag.indexed_pair()),
+ Self::Pocket(mut values) => {
+ let Some(name) = values.next() else {
+ return Ok(None);
+ };
+ let name = tag_value_utf8(name)?;
+ if !TagName::is_indexable_name(name) {
+ return Ok(None);
+ }
+ let Some(value) = values.next() else {
+ return Ok(None);
+ };
+ Ok(Some((name, tag_value_utf8(value)?)))
+ }
+ }
+ }
+
+ pub fn values(self) -> Result<Vec<&'a str>, GroupError> {
+ match self {
+ Self::Tangle(tag) => Ok(tag.values().iter().map(String::as_str).collect()),
+ Self::Pocket(values) => values.map(tag_value_utf8).collect(),
+ }
+ }
+}
+
+impl GroupEventView for Event {
+ fn id_hex(&self) -> String {
+ self.id().as_str().to_owned()
+ }
+
+ fn pubkey_hex(&self) -> String {
+ self.unsigned().pubkey().as_str().to_owned()
+ }
+
+ fn kind_u32(&self) -> u32 {
+ self.unsigned().kind().as_u32()
+ }
+
+ fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError>
+ where
+ F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>,
+ {
+ for tag in self.unsigned().tags() {
+ visitor(GroupEventTag::Tangle(tag))?;
+ }
+ Ok(())
+ }
+}
+
+impl GroupEventView for PocketEvent {
+ fn id_hex(&self) -> String {
+ self.id().as_hex_string()
+ }
+
+ fn pubkey_hex(&self) -> String {
+ self.pubkey().as_hex_string()
+ }
+
+ fn kind_u32(&self) -> u32 {
+ u32::from(self.kind().as_u16())
+ }
+
+ fn visit_tags<'a, F>(&'a self, mut visitor: F) -> Result<(), GroupError>
+ where
+ F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>,
+ {
+ let tags = self.tags().map_err(pocket_tags_error)?;
+ for tag in tags.iter() {
+ visitor(GroupEventTag::Pocket(tag))?;
+ }
+ Ok(())
+ }
+}
+
+impl GroupEventView for PocketOwnedEvent {
+ fn id_hex(&self) -> String {
+ let event: &PocketEvent = self;
+ event.id_hex()
+ }
+
+ fn pubkey_hex(&self) -> String {
+ let event: &PocketEvent = self;
+ event.pubkey_hex()
+ }
+
+ fn kind_u32(&self) -> u32 {
+ let event: &PocketEvent = self;
+ event.kind_u32()
+ }
+
+ fn visit_tags<'a, F>(&'a self, visitor: F) -> Result<(), GroupError>
+ where
+ F: FnMut(GroupEventTag<'a>) -> Result<(), GroupError>,
+ {
+ let event: &PocketEvent = self;
+ event.visit_tags(visitor)
+ }
+}
+
+fn tag_value_utf8(value: &[u8]) -> Result<&str, GroupError> {
+ str::from_utf8(value).map_err(|_| {
+ GroupError::invalid(
+ GroupErrorKind::MalformedGroupTag,
+ "group event tag is not valid UTF-8",
+ )
+ })
+}
+
+fn pocket_tags_error(error: pocket_types::Error) -> GroupError {
+ GroupError::invalid(
+ GroupErrorKind::MalformedGroupTag,
+ format!("malformed Pocket event tags: {error}"),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::GroupEventView;
+ use pocket_types::Event as PocketEvent;
+ use tangle_protocol::{
+ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent,
+ event_to_value,
+ };
+
+ #[test]
+ fn tangle_event_view_exposes_group_fields() {
+ let event = event();
+
+ assert_eq!(event.kind_u32(), 1);
+ 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),
+ vec![
+ vec!["h".to_owned(), "Farm".to_owned()],
+ vec!["summary".to_owned(), "Harvest".to_owned()],
+ ]
+ );
+ }
+
+ #[test]
+ fn pocket_event_view_exposes_group_fields_without_tangle_reparse() {
+ 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");
+
+ assert_eq!(pocket.kind_u32(), 1);
+ 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),
+ vec![
+ vec!["h".to_owned(), "Farm".to_owned()],
+ vec!["summary".to_owned(), "Harvest".to_owned()],
+ ]
+ );
+ }
+
+ fn indexed_pairs<E: GroupEventView + ?Sized>(event: &E) -> Vec<(String, String)> {
+ let mut pairs = Vec::new();
+ event
+ .visit_tags(|tag| {
+ if let Some((name, value)) = tag.indexed_pair()? {
+ pairs.push((name.to_owned(), value.to_owned()));
+ }
+ Ok(())
+ })
+ .expect("visit tags");
+ pairs
+ }
+
+ fn tag_values<E: GroupEventView + ?Sized>(event: &E) -> Vec<Vec<String>> {
+ let mut tags = Vec::new();
+ event
+ .visit_tags(|tag| {
+ tags.push(tag.values()?.into_iter().map(str::to_owned).collect());
+ Ok(())
+ })
+ .expect("visit tags");
+ tags
+ }
+
+ fn event() -> Event {
+ Event::new(
+ EventId::new(&"0".repeat(64)).expect("id"),
+ UnsignedEvent::new(
+ PublicKeyHex::new(&"1".repeat(64)).expect("pubkey"),
+ UnixTimestamp::new(1),
+ Kind::new(1).expect("kind"),
+ vec![
+ Tag::from_parts("h", &["Farm"]).expect("h"),
+ Tag::from_parts("summary", &["Harvest"]).expect("summary"),
+ ],
+ "",
+ ),
+ SignatureHex::new(&"2".repeat(128)).expect("sig"),
+ )
+ }
+}
diff --git a/crates/tangle_groups/src/lib.rs b/crates/tangle_groups/src/lib.rs
@@ -2,6 +2,7 @@
pub mod classification;
pub mod errors;
+pub mod event_view;
pub mod ids;
pub mod kinds;
pub mod metadata;
@@ -20,6 +21,7 @@ use tangle_protocol::PublicKeyHex;
pub use classification::{GroupEventClass, classify_group_event};
pub use errors::{GroupError, GroupErrorKind, GroupReplyPrefix};
+pub use event_view::{GroupEventTag, GroupEventView};
pub use ids::GroupId;
pub use kinds::{
KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, KIND_GROUP_DELETE_EVENT,