commit 0b028cf4d1d484004611bcd88cd9c0f6caf29ecb
parent 2d1d4815678ec232b4bfcced6a95b334c52a99d4
Author: triesap <tyson@radroots.org>
Date: Thu, 11 Jun 2026 17:34:20 -0700
events_codec_wasm: add field tag builders
- expose wasm tag builders for Field event families
- add CRDT JSON input with explicit author context
- expose NIP-29 group tag builders for modeled group events
- cover each new binding with host-side JSON tests
Diffstat:
1 file changed, 408 insertions(+), 1 deletion(-)
diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs
@@ -4,8 +4,18 @@ use radroots_events::comment::RadrootsComment;
use radroots_events::coop::RadrootsCoop;
use radroots_events::document::RadrootsDocument;
use radroots_events::farm::RadrootsFarm;
+use radroots_events::farm_crdt::RadrootsFarmCrdtChange;
+use radroots_events::farm_file::RadrootsFarmFileMetadata;
+use radroots_events::farm_workspace::RadrootsFarmWorkspaceManifest;
use radroots_events::follow::RadrootsFollow;
use radroots_events::gift_wrap::RadrootsGiftWrap;
+use radroots_events::group::{
+ RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite,
+ RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata,
+ RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest, RadrootsGroupMembers,
+ RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser, RadrootsGroupRoles,
+};
+use radroots_events::http_auth::RadrootsHttpAuth;
use radroots_events::job_feedback::RadrootsJobFeedback;
use radroots_events::job_request::RadrootsJobRequest;
use radroots_events::job_result::RadrootsJobResult;
@@ -16,13 +26,25 @@ use radroots_events::message::RadrootsMessage;
use radroots_events::message_file::RadrootsMessageFile;
use radroots_events::plot::RadrootsPlot;
use radroots_events::reaction::RadrootsReaction;
+use radroots_events::relay_auth::RadrootsRelayAuth;
use radroots_events::seal::RadrootsSeal;
use radroots_events_codec::comment::encode::comment_build_tags;
use radroots_events_codec::coop::encode::coop_build_tags;
use radroots_events_codec::document::encode::document_build_tags;
use radroots_events_codec::farm::encode::farm_build_tags;
+use radroots_events_codec::farm_crdt::encode::farm_crdt_change_build_tags_with_author;
+use radroots_events_codec::farm_file::encode::farm_file_metadata_build_tags;
+use radroots_events_codec::farm_workspace::encode::farm_workspace_build_tags;
use radroots_events_codec::follow::encode::follow_build_tags;
use radroots_events_codec::gift_wrap::encode::gift_wrap_build_tags;
+use radroots_events_codec::group::encode::{
+ group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags,
+ group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags,
+ group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags,
+ group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags,
+ group_roles_build_tags,
+};
+use radroots_events_codec::http_auth::encode::http_auth_build_tags;
use radroots_events_codec::job::feedback::encode::job_feedback_build_tags;
use radroots_events_codec::job::request::encode::job_request_build_tags;
use radroots_events_codec::job::result::encode::job_result_build_tags;
@@ -35,6 +57,7 @@ use radroots_events_codec::message::encode::message_build_tags;
use radroots_events_codec::message_file::encode::message_file_build_tags;
use radroots_events_codec::plot::encode::plot_build_tags;
use radroots_events_codec::reaction::encode::reaction_build_tags;
+use radroots_events_codec::relay_auth::encode::relay_auth_build_tags;
use radroots_events_codec::seal::encode::seal_build_tags;
use serde::de::DeserializeOwned;
#[cfg(target_arch = "wasm32")]
@@ -92,6 +115,12 @@ where
tags_to_json(tags)
}
+#[derive(serde::Deserialize)]
+struct FarmCrdtTagsInput {
+ change: RadrootsFarmCrdtChange,
+ author_pubkey: String,
+}
+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags))]
pub fn listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_impl)
@@ -182,6 +211,102 @@ pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> {
build_tags_json::<RadrootsGiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags)
}
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_workspace_tags))]
+pub fn farm_workspace_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsFarmWorkspaceManifest, _, _>(
+ workspace_json,
+ farm_workspace_build_tags,
+ )
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_tags))]
+pub fn farm_crdt_tags(input_json: &str) -> Result<String, RadrootsJsValue> {
+ let input = parse_json::<FarmCrdtTagsInput>(input_json)?;
+ let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey))
+ .map_err(err_js)?;
+ tags_to_json(tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_tags))]
+pub fn farm_file_tags(file_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsFarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))]
+pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsRelayAuth, _, _>(auth_json, relay_auth_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))]
+pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsHttpAuth, _, _>(auth_json, http_auth_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))]
+pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupPutUser, _, _>(group_json, group_put_user_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))]
+pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupRemoveUser, _, _>(group_json, group_remove_user_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))]
+pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupCreateGroup, _, _>(group_json, group_create_group_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))]
+pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))]
+pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))]
+pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))]
+pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupCreateInvite, _, _>(group_json, group_create_invite_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))]
+pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupJoinRequest, _, _>(group_json, group_join_request_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))]
+pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))]
+pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupMetadata, _, _>(group_json, group_metadata_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))]
+pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupAdmins, _, _>(group_json, group_admins_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))]
+pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupMembers, _, _>(group_json, group_members_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))]
+pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsGroupRoles, _, _>(group_json, group_roles_build_tags)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -190,9 +315,30 @@ mod tests {
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_events::farm::RadrootsFarmRef;
+ use radroots_events::farm_crdt::{
+ RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RadrootsCrdtBackend, RadrootsFarmCrdtDocumentKind,
+ RadrootsFarmSemanticKind,
+ };
+ use radroots_events::farm_file::{
+ RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, RadrootsFarmFileSource,
+ };
+ use radroots_events::farm_workspace::{
+ RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, RADROOTS_FARM_WORKSPACE_SCHEMA,
+ RadrootsFarmWorkspaceManifest, RadrootsFarmWorkspaceMediaServer, RadrootsFarmWorkspaceRef,
+ RadrootsFarmWorkspaceRelay, RadrootsFarmWorkspaceRelayMode,
+ };
+ use radroots_events::group::{
+ RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite,
+ RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata,
+ RadrootsGroupEditableMetadata, RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest,
+ RadrootsGroupMembers, RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser,
+ RadrootsGroupRole, RadrootsGroupRoles, RadrootsGroupUserRef,
+ };
+ use radroots_events::http_auth::RadrootsHttpAuth;
use radroots_events::job::JobInputType;
use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam};
use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct};
+ use radroots_events::relay_auth::RadrootsRelayAuth;
fn sample_listing() -> RadrootsListing {
let quantity =
@@ -263,9 +409,119 @@ mod tests {
}
}
+ fn sample_workspace_manifest() -> RadrootsFarmWorkspaceManifest {
+ RadrootsFarmWorkspaceManifest {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(),
+ farm_group_id: "field-group".to_string(),
+ name: "Small Regen Farm".to_string(),
+ owner_pubkey: "workspace_owner_pubkey".to_string(),
+ farm: Some(RadrootsFarmRef {
+ pubkey: "farm_pubkey".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ }),
+ relays: vec![RadrootsFarmWorkspaceRelay {
+ url: "wss://relay.example.invalid/farm/field-group".to_string(),
+ mode: RadrootsFarmWorkspaceRelayMode::ReadWrite,
+ }],
+ media_servers: vec![RadrootsFarmWorkspaceMediaServer {
+ url: "https://media.example.invalid/farm/field-group".to_string(),
+ service: "RadrootsPrivateMedia".to_string(),
+ }],
+ supported_kinds: vec![78, 30078],
+ protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(),
+ created_at_ms: 1_780_000_000_000,
+ updated_at_ms: None,
+ }
+ }
+
+ fn sample_crdt_change() -> RadrootsFarmCrdtChange {
+ RadrootsFarmCrdtChange {
+ schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
+ workspace: RadrootsFarmWorkspaceRef {
+ pubkey: "workspace_pubkey".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ farm_group_id: "field-group".to_string(),
+ document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
+ document_kind: RadrootsFarmCrdtDocumentKind::FarmTask,
+ crdt_backend: RadrootsCrdtBackend::Automerge,
+ crdt_backend_version: Some("0.x".to_string()),
+ actor_id: "actor_abc".to_string(),
+ change_hash: "crdt_hash_abc".to_string(),
+ dependencies: Vec::new(),
+ encoded_change: "abc-DEF_012".to_string(),
+ semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate,
+ business_time_ms: 1_780_000_000_000,
+ author_member_id: Some("member_abc".to_string()),
+ app_version: Some("0.1.0".to_string()),
+ }
+ }
+
+ fn sample_file_metadata() -> RadrootsFarmFileMetadata {
+ RadrootsFarmFileMetadata {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ workspace: RadrootsFarmWorkspaceRef {
+ pubkey: "workspace_pubkey".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ farm_group_id: "field-group".to_string(),
+ owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
+ owner_document_kind: RadrootsFarmCrdtDocumentKind::FarmTask,
+ caption: Some("Tomatoes harvested from Patch Y.".to_string()),
+ url: "https://media.example.invalid/blob/sha256".to_string(),
+ mime_type: "image/jpeg".to_string(),
+ sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
+ original_sha256: None,
+ size_bytes: Some(123_456),
+ dimensions: Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }),
+ blurhash: None,
+ thumb: Some(RadrootsFarmFileSource {
+ url: "https://media.example.invalid/thumb/sha256".to_string(),
+ mime_type: Some("image/jpeg".to_string()),
+ dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 240 }),
+ }),
+ image: None,
+ alt: Some("Harvested tomatoes in a crate".to_string()),
+ fallbacks: Vec::new(),
+ }
+ }
+
+ fn sample_group_metadata() -> RadrootsGroupEditableMetadata {
+ RadrootsGroupEditableMetadata {
+ name: Some("Small Regen Farm".to_string()),
+ about: Some("Field app group".to_string()),
+ picture: Some("https://media.example.invalid/group.png".to_string()),
+ is_private: false,
+ is_closed: false,
+ is_hidden: false,
+ }
+ }
+
+ fn sample_group_user(role: &str) -> RadrootsGroupUserRef {
+ RadrootsGroupUserRef {
+ pubkey: format!("{role}_pubkey"),
+ roles: vec![role.to_string()],
+ }
+ }
+
+ fn sample_group_role() -> RadrootsGroupRole {
+ RadrootsGroupRole {
+ name: "member".to_string(),
+ description: Some("can read and write group events".to_string()),
+ permissions: vec!["read".to_string(), "write".to_string()],
+ }
+ }
+
+ fn assert_tags_json(value: Result<String, RadrootsJsValue>) {
+ let json = value.expect("tags json");
+ let tags: Vec<Vec<String>> = serde_json::from_str(&json).expect("tags");
+ assert!(!tags.is_empty());
+ }
+
#[test]
fn bindings_reject_invalid_json() {
- let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 18] = [
+ let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 36] = [
listing_tags,
listing_tags_full,
comment_tags,
@@ -284,6 +540,24 @@ mod tests {
message_file_tags,
seal_tags,
gift_wrap_tags,
+ farm_workspace_tags,
+ farm_crdt_tags,
+ farm_file_tags,
+ relay_auth_tags,
+ http_auth_tags,
+ group_put_user_tags,
+ group_remove_user_tags,
+ group_create_group_tags,
+ group_edit_metadata_tags,
+ group_delete_group_tags,
+ group_delete_event_tags,
+ group_create_invite_tags,
+ group_join_request_tags,
+ group_leave_request_tags,
+ group_metadata_tags,
+ group_admins_tags,
+ group_members_tags,
+ group_roles_tags,
];
for binding in bindings {
@@ -308,6 +582,139 @@ mod tests {
}
#[test]
+ fn field_bindings_encode_to_json_when_input_is_valid() {
+ let workspace_json =
+ serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
+ assert_tags_json(farm_workspace_tags(&workspace_json));
+
+ let crdt_json = serde_json::json!({
+ "change": sample_crdt_change(),
+ "author_pubkey": "author_pubkey"
+ })
+ .to_string();
+ assert_tags_json(farm_crdt_tags(&crdt_json));
+
+ let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json");
+ assert_tags_json(farm_file_tags(&file_json));
+
+ let relay_auth_json = serde_json::to_string(&RadrootsRelayAuth {
+ relay: "wss://relay.example.invalid/farm/field-group".to_string(),
+ challenge: "relay-provided-challenge".to_string(),
+ })
+ .expect("relay auth json");
+ assert_tags_json(relay_auth_tags(&relay_auth_json));
+
+ let http_auth_json = serde_json::to_string(&RadrootsHttpAuth {
+ url: "https://media.example.invalid/upload".to_string(),
+ method: "POST".to_string(),
+ payload_sha256: Some(
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
+ ),
+ })
+ .expect("http auth json");
+ assert_tags_json(http_auth_tags(&http_auth_json));
+ }
+
+ #[test]
+ fn group_bindings_encode_to_json_when_input_is_valid() {
+ let metadata = sample_group_metadata();
+ assert_tags_json(group_put_user_tags(
+ &serde_json::to_string(&RadrootsGroupPutUser {
+ group_id: "field-group".to_string(),
+ pubkey: "member_pubkey".to_string(),
+ roles: vec!["member".to_string()],
+ })
+ .expect("put user json"),
+ ));
+ assert_tags_json(group_remove_user_tags(
+ &serde_json::to_string(&RadrootsGroupRemoveUser {
+ group_id: "field-group".to_string(),
+ pubkey: "member_pubkey".to_string(),
+ })
+ .expect("remove user json"),
+ ));
+ assert_tags_json(group_create_group_tags(
+ &serde_json::to_string(&RadrootsGroupCreateGroup {
+ group_id: "field-group".to_string(),
+ metadata: metadata.clone(),
+ })
+ .expect("create group json"),
+ ));
+ assert_tags_json(group_edit_metadata_tags(
+ &serde_json::to_string(&RadrootsGroupEditMetadata {
+ group_id: "field-group".to_string(),
+ metadata: metadata.clone(),
+ })
+ .expect("edit metadata json"),
+ ));
+ assert_tags_json(group_delete_group_tags(
+ &serde_json::to_string(&RadrootsGroupDeleteGroup {
+ group_id: "field-group".to_string(),
+ })
+ .expect("delete group json"),
+ ));
+ assert_tags_json(group_delete_event_tags(
+ &serde_json::to_string(&RadrootsGroupDeleteEvent {
+ group_id: "field-group".to_string(),
+ event_id: "event_id".to_string(),
+ })
+ .expect("delete event json"),
+ ));
+ assert_tags_json(group_create_invite_tags(
+ &serde_json::to_string(&RadrootsGroupCreateInvite {
+ group_id: "field-group".to_string(),
+ invitee_pubkey: Some("member_pubkey".to_string()),
+ roles: vec!["member".to_string()],
+ expires_at: Some(1_780_000_000),
+ claim: Some("claim-token".to_string()),
+ })
+ .expect("invite json"),
+ ));
+ assert_tags_json(group_join_request_tags(
+ &serde_json::to_string(&RadrootsGroupJoinRequest {
+ group_id: "field-group".to_string(),
+ message: Some("requesting access".to_string()),
+ })
+ .expect("join json"),
+ ));
+ assert_tags_json(group_leave_request_tags(
+ &serde_json::to_string(&RadrootsGroupLeaveRequest {
+ group_id: "field-group".to_string(),
+ message: Some("leaving".to_string()),
+ })
+ .expect("leave json"),
+ ));
+ assert_tags_json(group_metadata_tags(
+ &serde_json::to_string(&RadrootsGroupMetadata {
+ d_tag: "field-group".to_string(),
+ metadata,
+ })
+ .expect("metadata json"),
+ ));
+ assert_tags_json(group_admins_tags(
+ &serde_json::to_string(&RadrootsGroupAdmins {
+ d_tag: "field-group".to_string(),
+ admins: vec![sample_group_user("admin")],
+ })
+ .expect("admins json"),
+ ));
+ assert_tags_json(group_members_tags(
+ &serde_json::to_string(&RadrootsGroupMembers {
+ d_tag: "field-group".to_string(),
+ members: vec![sample_group_user("member")],
+ })
+ .expect("members json"),
+ ));
+ assert_tags_json(group_roles_tags(
+ &serde_json::to_string(&RadrootsGroupRoles {
+ d_tag: "field-group".to_string(),
+ roles: vec![sample_group_role()],
+ })
+ .expect("roles json"),
+ ));
+ }
+
+ #[test]
fn listing_bindings_surface_builder_errors() {
let mut listing = sample_listing();
listing.d_tag.clear();