lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 0570212e816f8b51e97c711173b6f42e9128c82c
parent ea95044da292e72154e767c1cf2956e1e71ea148
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Dec 2025 14:49:04 +0000

app-data: add encode/decode support for `KIND_APP_DATA`



- Add app_data module with encode/decode helpers
- Validate kind and require non-empty \"d\" tag in parsers
- Build wire parts with kind/content/tags for app data events
- Add tag builder impl and roundtrip/validation tests

Diffstat:
Aevents-codec/src/app_data/decode.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/app_data/encode.rs | 38++++++++++++++++++++++++++++++++++++++
Aevents-codec/src/app_data/mod.rs | 4++++
Mevents-codec/src/lib.rs | 1+
Mevents-codec/src/tag_builders.rs | 10++++++++++
Aevents-codec/tests/app_data.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 204 insertions(+), 0 deletions(-)

diff --git a/events-codec/src/app_data/decode.rs b/events-codec/src/app_data/decode.rs @@ -0,0 +1,92 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::{String, ToString}, vec::Vec}; + +use radroots_events::{ + RadrootsNostrEvent, + app_data::{RadrootsAppData, RadrootsAppDataEventIndex, RadrootsAppDataEventMetadata, KIND_APP_DATA}, + tags::TAG_D, +}; + +use crate::error::EventParseError; + +fn parse_d_tag(tags: &[Vec<String>]) -> Result<String, EventParseError> { + let tag = tags + .iter() + .find(|t| t.get(0).map(|s| s.as_str()) == Some(TAG_D)) + .ok_or(EventParseError::MissingTag(TAG_D))?; + let value = tag + .get(1) + .map(|s| s.to_string()) + .ok_or(EventParseError::InvalidTag(TAG_D))?; + if value.trim().is_empty() { + return Err(EventParseError::InvalidTag(TAG_D)); + } + Ok(value) +} + +pub fn app_data_from_tags( + kind: u32, + tags: &[Vec<String>], + content: &str, +) -> Result<RadrootsAppData, EventParseError> { + if kind != KIND_APP_DATA { + return Err(EventParseError::InvalidKind { + expected: "30078", + got: kind, + }); + } + let d_tag = parse_d_tag(tags)?; + Ok(RadrootsAppData { + d_tag, + content: content.to_string(), + }) +} + +pub fn metadata_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsAppDataEventMetadata, EventParseError> { + let app_data = app_data_from_tags(kind, &tags, &content)?; + Ok(RadrootsAppDataEventMetadata { + id, + author, + published_at, + kind, + app_data, + }) +} + +pub fn index_from_event( + id: String, + author: String, + published_at: u32, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, +) -> Result<RadrootsAppDataEventIndex, EventParseError> { + let metadata = metadata_from_event( + id.clone(), + author.clone(), + published_at, + kind, + content.clone(), + tags.clone(), + )?; + Ok(RadrootsAppDataEventIndex { + event: RadrootsNostrEvent { + id, + author, + created_at: published_at, + kind, + content, + tags, + sig, + }, + metadata, + }) +} diff --git a/events-codec/src/app_data/encode.rs b/events-codec/src/app_data/encode.rs @@ -0,0 +1,38 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +use radroots_events::app_data::{RadrootsAppData, KIND_APP_DATA}; +use radroots_events::tags::TAG_D; + +use crate::error::EventEncodeError; +use crate::wire::WireEventParts; + +pub fn app_data_build_tags( + app_data: &RadrootsAppData, +) -> Result<Vec<Vec<String>>, EventEncodeError> { + if app_data.d_tag.trim().is_empty() { + return Err(EventEncodeError::EmptyRequiredField("d_tag")); + } + let mut tags = Vec::with_capacity(1); + tags.push(vec![TAG_D.to_string(), app_data.d_tag.clone()]); + Ok(tags) +} + +pub fn to_wire_parts(app_data: &RadrootsAppData) -> Result<WireEventParts, EventEncodeError> { + to_wire_parts_with_kind(app_data, KIND_APP_DATA) +} + +pub fn to_wire_parts_with_kind( + app_data: &RadrootsAppData, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if kind != KIND_APP_DATA { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = app_data_build_tags(app_data)?; + Ok(WireEventParts { + kind, + content: app_data.content.clone(), + tags, + }) +} diff --git a/events-codec/src/app_data/mod.rs b/events-codec/src/app_data/mod.rs @@ -0,0 +1,4 @@ +#![forbid(unsafe_code)] + +pub mod decode; +pub mod encode; diff --git a/events-codec/src/lib.rs b/events-codec/src/lib.rs @@ -12,6 +12,7 @@ pub mod wire; pub mod comment; pub mod follow; +pub mod app_data; pub mod message; pub mod post; pub mod reaction; diff --git a/events-codec/src/tag_builders.rs b/events-codec/src/tag_builders.rs @@ -6,6 +6,7 @@ use alloc::{string::String, vec::Vec}; use core::convert::Infallible; use radroots_events::{ + app_data::RadrootsAppData, comment::RadrootsComment, follow::RadrootsFollow, job_feedback::RadrootsJobFeedback, @@ -20,6 +21,7 @@ use radroots_events::{ use crate::comment::encode::comment_build_tags; use crate::error::EventEncodeError; +use crate::app_data::encode::app_data_build_tags; use crate::follow::encode::follow_build_tags; use crate::job::encode::JobEncodeError; use crate::job::feedback::encode::job_feedback_build_tags; @@ -42,6 +44,14 @@ impl RadrootsEventTagBuilder for RadrootsListing { } } +impl RadrootsEventTagBuilder for RadrootsAppData { + type Error = EventEncodeError; + + fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { + app_data_build_tags(self) + } +} + impl RadrootsEventTagBuilder for RadrootsComment { type Error = EventEncodeError; diff --git a/events-codec/tests/app_data.rs b/events-codec/tests/app_data.rs @@ -0,0 +1,59 @@ +use radroots_events::app_data::{RadrootsAppData, KIND_APP_DATA}; +use radroots_events_codec::app_data::decode::app_data_from_tags; +use radroots_events_codec::app_data::encode::{app_data_build_tags, to_wire_parts}; +use radroots_events_codec::error::{EventEncodeError, EventParseError}; + +#[test] +fn app_data_build_tags_requires_d_tag() { + let app_data = RadrootsAppData { + d_tag: " ".to_string(), + content: "payload".to_string(), + }; + + let err = app_data_build_tags(&app_data).unwrap_err(); + assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag"))); +} + +#[test] +fn app_data_to_wire_parts_sets_kind_tags_content() { + let app_data = RadrootsAppData { + d_tag: "radroots.app".to_string(), + content: "payload".to_string(), + }; + + let parts = to_wire_parts(&app_data).unwrap(); + assert_eq!(parts.kind, KIND_APP_DATA); + assert_eq!(parts.content, "payload"); + assert_eq!( + parts.tags, + vec![vec!["d".to_string(), "radroots.app".to_string()]] + ); +} + +#[test] +fn app_data_from_tags_requires_kind() { + let tags = vec![vec!["d".to_string(), "radroots.app".to_string()]]; + let err = app_data_from_tags(1, &tags, "payload").unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidKind { + expected: "30078", + got: 1 + } + )); +} + +#[test] +fn app_data_from_tags_requires_d_tag() { + let err = app_data_from_tags(KIND_APP_DATA, &[], "payload").unwrap_err(); + assert!(matches!(err, EventParseError::MissingTag("d"))); +} + +#[test] +fn app_data_roundtrip_from_tags() { + let tags = vec![vec!["d".to_string(), "radroots.app".to_string()]]; + let app_data = app_data_from_tags(KIND_APP_DATA, &tags, "payload").unwrap(); + + assert_eq!(app_data.d_tag, "radroots.app"); + assert_eq!(app_data.content, "payload"); +}