lib

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

commit 610f0353f05d37d2df0d3f109b880faba5e9c4b4
parent 36d87d367542e911bd1c4e0ad647b490d2b6f827
Author: triesap <tyson@radroots.org>
Date:   Fri, 12 Jun 2026 03:37:15 -0700

events_codec_wasm: add social tag builders

- expose wasm JSON tag builders for public social event families
- route article, calendar, file metadata, post, repost, and report builders through Rust codecs
- cover valid social builder inputs and invalid target validation in wasm tests
- verify wasm crate formatting, native tests, and wasm32 target check with Rust 1.92

Diffstat:
Mcrates/events_codec_wasm/src/lib.rs | 379++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 378 insertions(+), 1 deletion(-)

diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs @@ -1,5 +1,10 @@ #![forbid(unsafe_code)] +use radroots_events::article::RadrootsArticle; +use radroots_events::calendar::{ + RadrootsCalendar, RadrootsCalendarDateEvent, RadrootsCalendarEventRsvp, + RadrootsCalendarTimeEvent, +}; use radroots_events::comment::RadrootsComment; use radroots_events::coop::RadrootsCoop; use radroots_events::document::RadrootsDocument; @@ -7,6 +12,7 @@ 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::file_metadata::RadrootsFileMetadata; use radroots_events::follow::RadrootsFollow; use radroots_events::gift_wrap::RadrootsGiftWrap; use radroots_events::group::{ @@ -25,9 +31,17 @@ use radroots_events::listing::RadrootsListing; use radroots_events::message::RadrootsMessage; use radroots_events::message_file::RadrootsMessageFile; use radroots_events::plot::RadrootsPlot; +use radroots_events::post::RadrootsPost; use radroots_events::reaction::RadrootsReaction; use radroots_events::relay_auth::RadrootsRelayAuth; +use radroots_events::report::RadrootsReport; +use radroots_events::repost::{RadrootsGenericRepost, RadrootsRepost}; use radroots_events::seal::RadrootsSeal; +use radroots_events_codec::article::encode::article_build_tags; +use radroots_events_codec::calendar::encode::{ + calendar_collection_build_tags, calendar_date_event_build_tags, calendar_time_event_build_tags, + rsvp_build_tags, +}; 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; @@ -35,6 +49,7 @@ 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::file_metadata::encode::file_metadata_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::{ @@ -56,8 +71,11 @@ use radroots_events_codec::listing::tags::{ 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::post::encode::post_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::report::encode::report_build_tags; +use radroots_events_codec::repost::encode::{generic_repost_build_tags, repost_build_tags}; use radroots_events_codec::seal::encode::seal_build_tags; use serde::de::DeserializeOwned; #[cfg(target_arch = "wasm32")] @@ -131,11 +149,67 @@ pub fn listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_full_impl) } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = post_tags))] +pub fn post_tags(post_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsPost, _, _>(post_json, post_build_tags) +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = comment_tags))] pub fn comment_tags(comment_json: &str) -> Result<String, RadrootsJsValue> { build_tags_json::<RadrootsComment, _, _>(comment_json, comment_build_tags) } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))] +pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsArticle, _, _>(article_json, article_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))] +pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsFileMetadata, _, _>(metadata_json, file_metadata_build_tags) +} + +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = calendar_date_event_tags) +)] +pub fn calendar_date_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsCalendarDateEvent, _, _>(event_json, calendar_date_event_build_tags) +} + +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = calendar_time_event_tags) +)] +pub fn calendar_time_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsCalendarTimeEvent, _, _>(event_json, calendar_time_event_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = calendar_tags))] +pub fn calendar_tags(calendar_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsCalendar, _, _>(calendar_json, calendar_collection_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = calendar_rsvp_tags))] +pub fn calendar_rsvp_tags(rsvp_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsCalendarEventRsvp, _, _>(rsvp_json, rsvp_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))] +pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsRepost, _, _>(repost_json, repost_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))] +pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsGenericRepost, _, _>(repost_json, generic_repost_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))] +pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsReport, _, _>(report_json, report_build_tags) +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))] pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> { build_tags_json::<RadrootsFollow, _, _>(follow_json, follow_build_tags) @@ -339,6 +413,12 @@ mod tests { use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam}; use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct}; use radroots_events::relay_auth::RadrootsRelayAuth; + use radroots_events::social::{ + RadrootsCalendarDateValue, RadrootsCalendarEventFreeBusy, RadrootsCalendarEventRsvpStatus, + RadrootsCalendarParticipant, RadrootsReportFileTarget, RadrootsReportType, + RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaDimensions, + RadrootsSocialMediaMetadata, RadrootsSocialTarget, + }; fn sample_listing() -> RadrootsListing { let quantity = @@ -388,6 +468,226 @@ mod tests { } } + fn synthetic_pubkey(seed: char) -> String { + seed.to_string().repeat(64) + } + + fn synthetic_event_id(seed: char) -> String { + seed.to_string().repeat(64) + } + + fn social_farm_anchor() -> RadrootsSocialFarmAnchor { + RadrootsSocialFarmAnchor { + farm: RadrootsFarmRef { + pubkey: synthetic_pubkey('a'), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn event_target(kind: u32, seed: char) -> RadrootsSocialTarget { + RadrootsSocialTarget::Event { + id: synthetic_event_id(seed), + author: Some(synthetic_pubkey('b')), + event_kind: Some(kind), + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn address_target(kind: u32, d_tag: &str) -> RadrootsSocialTarget { + let author = synthetic_pubkey('c'); + RadrootsSocialTarget::Address { + address: format!("{kind}:{author}:{d_tag}"), + author: Some(author), + event_kind: Some(kind), + relays: Some(vec!["wss://relay2.example.test".to_string()]), + } + } + + fn social_location() -> RadrootsSocialLocation { + RadrootsSocialLocation { + name: Some("field edge".to_string()), + geohash: Some("c23nb62w20st".to_string()), + } + } + + fn sample_post() -> RadrootsPost { + RadrootsPost { + content: "field update".to_string(), + farm: Some(social_farm_anchor()), + address_refs: Some(vec![address_target(30023, "AAAAAAAAAAAAAAAAAAAAAQ")]), + location: Some(social_location()), + topics: Some(vec!["soil".to_string(), "market".to_string()]), + quote_refs: Some(vec![event_target(30023, 'd')]), + media: Some(vec![RadrootsSocialMediaMetadata { + url: Some("https://media.example.test/field.jpg".to_string()), + mime_type: Some("image/jpeg".to_string()), + sha256: Some( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + ), + original_sha256: None, + size: Some(4096), + dimensions: Some(RadrootsSocialMediaDimensions { + width: 1200, + height: 800, + }), + blurhash: None, + thumbnails: None, + image: None, + summary: Some("field photo".to_string()), + alt: Some("rows after harvest".to_string()), + fallback: None, + magnet: Some("magnet:?xt=urn:btih:abc".to_string()), + content_hashes: Some(vec!["sha256:field".to_string()]), + services: Some(vec!["https://media.example.test".to_string()]), + imeta: None, + }]), + } + } + + fn sample_article() -> RadrootsArticle { + RadrootsArticle { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + title: "soil notes".to_string(), + content: "# soil notes".to_string(), + summary: Some("cover crop observations".to_string()), + image: Some("https://media.example.test/article.jpg".to_string()), + published_at: Some(1_780_000_000), + farm: Some(social_farm_anchor()), + location: Some(social_location()), + topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]), + } + } + + fn sample_public_file_metadata() -> RadrootsFileMetadata { + RadrootsFileMetadata { + url: "https://media.example.test/public.jpg".to_string(), + mime_type: "image/jpeg".to_string(), + sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + original_sha256: Some( + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(), + ), + size: Some(4096), + dimensions: Some(RadrootsSocialMediaDimensions { + width: 1200, + height: 800, + }), + blurhash: None, + thumbnails: None, + summary: Some("public field photo".to_string()), + alt: Some("rows after harvest".to_string()), + fallback: Some("https://media.example.test/fallback.jpg".to_string()), + magnet: Some("magnet:?xt=urn:btih:abc".to_string()), + content_hashes: Some(vec!["sha256:field".to_string()]), + services: Some(vec!["https://media.example.test".to_string()]), + content: Some("caption".to_string()), + } + } + + fn sample_calendar_date_event() -> RadrootsCalendarDateEvent { + RadrootsCalendarDateEvent { + d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), + title: "market day".to_string(), + start: "2026-06-20".to_string(), + end: Some("2026-06-21".to_string()), + days: Some(vec![RadrootsCalendarDateValue { + value: "2026-06-20".to_string(), + }]), + location: Some(social_location()), + summary: Some("weekly pickup".to_string()), + image: None, + participants: Some(vec![RadrootsCalendarParticipant { + pubkey: synthetic_pubkey('e'), + relay: Some("wss://relay.example.test".to_string()), + role: Some("host".to_string()), + }]), + } + } + + fn sample_calendar_time_event() -> RadrootsCalendarTimeEvent { + RadrootsCalendarTimeEvent { + d_tag: "AAAAAAAAAAAAAAAAAAAA-A".to_string(), + title: "wash pack shift".to_string(), + start: 1_781_895_600, + end: Some(1_781_899_200), + start_tzid: Some("America/Vancouver".to_string()), + end_tzid: Some("America/Vancouver".to_string()), + location: Some(social_location()), + summary: Some("field crew".to_string()), + image: None, + participants: None, + } + } + + fn sample_calendar() -> RadrootsCalendar { + RadrootsCalendar { + d_tag: "AAAAAAAAAAAAAAAAAAAA_A".to_string(), + title: "farm calendar".to_string(), + events: vec![address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A")], + summary: Some("field schedule".to_string()), + image: None, + } + } + + fn sample_calendar_rsvp() -> RadrootsCalendarEventRsvp { + RadrootsCalendarEventRsvp { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + event: address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A"), + event_id: Some(synthetic_event_id('f')), + status: RadrootsCalendarEventRsvpStatus::Tentative, + free_busy: Some(RadrootsCalendarEventFreeBusy::Busy), + note: Some("depends on harvest".to_string()), + participants: None, + } + } + + fn sample_comment() -> RadrootsComment { + RadrootsComment { + root: event_target(30023, 'a'), + parent: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), + content: "great notes".to_string(), + } + } + + fn sample_reaction() -> RadrootsReaction { + RadrootsReaction { + target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), + content: String::new(), + } + } + + fn sample_repost() -> RadrootsRepost { + RadrootsRepost { + target: event_target(1, 'b'), + content: Some("field update".to_string()), + } + } + + fn sample_generic_repost() -> RadrootsGenericRepost { + RadrootsGenericRepost { + target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), + target_kind: 30023, + content: Some("article share".to_string()), + } + } + + fn sample_report() -> RadrootsReport { + RadrootsReport { + reported_pubkey: synthetic_pubkey('b'), + report_type: RadrootsReportType::Spam, + event: Some(event_target(1, 'c')), + file: Some(RadrootsReportFileTarget { + sha256: Some( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + ), + url: Some("https://media.example.test/bad.jpg".to_string()), + magnet: None, + }), + content: Some("spam report".to_string()), + } + } + fn sample_job_request() -> RadrootsJobRequest { RadrootsJobRequest { kind: 5100, @@ -528,10 +828,20 @@ mod tests { #[test] fn bindings_reject_invalid_json() { - let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 36] = [ + let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 46] = [ listing_tags, listing_tags_full, + post_tags, comment_tags, + article_tags, + file_metadata_tags, + calendar_date_event_tags, + calendar_time_event_tags, + calendar_tags, + calendar_rsvp_tags, + repost_tags, + generic_repost_tags, + report_tags, follow_tags, document_tags, coop_tags, @@ -589,6 +899,73 @@ mod tests { } #[test] + fn social_bindings_encode_to_json_when_input_is_valid() { + assert_tags_json(post_tags( + &serde_json::to_string(&sample_post()).expect("post json"), + )); + assert_tags_json(comment_tags( + &serde_json::to_string(&sample_comment()).expect("comment json"), + )); + assert_tags_json(article_tags( + &serde_json::to_string(&sample_article()).expect("article json"), + )); + assert_tags_json(file_metadata_tags( + &serde_json::to_string(&sample_public_file_metadata()).expect("file json"), + )); + assert_tags_json(calendar_date_event_tags( + &serde_json::to_string(&sample_calendar_date_event()).expect("date json"), + )); + assert_tags_json(calendar_time_event_tags( + &serde_json::to_string(&sample_calendar_time_event()).expect("time json"), + )); + assert_tags_json(calendar_tags( + &serde_json::to_string(&sample_calendar()).expect("calendar json"), + )); + assert_tags_json(calendar_rsvp_tags( + &serde_json::to_string(&sample_calendar_rsvp()).expect("rsvp json"), + )); + assert_tags_json(reaction_tags( + &serde_json::to_string(&sample_reaction()).expect("reaction json"), + )); + assert_tags_json(repost_tags( + &serde_json::to_string(&sample_repost()).expect("repost json"), + )); + assert_tags_json(generic_repost_tags( + &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"), + )); + assert_tags_json(report_tags( + &serde_json::to_string(&sample_report()).expect("report json"), + )); + } + + #[test] + fn social_bindings_surface_builder_errors() { + let mut article = sample_article(); + article.d_tag.clear(); + assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err()); + + let mut comment = sample_comment(); + comment.root = event_target(1, 'a'); + assert!(comment_tags(&serde_json::to_string(&comment).expect("comment json")).is_err()); + + let mut reaction = sample_reaction(); + reaction.target = RadrootsSocialTarget::External { + id: "https://example.test/object".to_string(), + external_kind: "web".to_string(), + hint: None, + }; + assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err()); + + let mut rsvp = sample_calendar_rsvp(); + rsvp.event = event_target(31923, 'f'); + assert!(calendar_rsvp_tags(&serde_json::to_string(&rsvp).expect("rsvp json")).is_err()); + + let mut report = sample_report(); + report.reported_pubkey.clear(); + assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err()); + } + + #[test] fn field_bindings_encode_to_json_when_input_is_valid() { let workspace_json = serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");