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:
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");
+}