sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

lib.rs (48658B)


      1 #![forbid(unsafe_code)]
      2 
      3 use radroots_events::article::RadrootsArticle;
      4 use radroots_events::calendar::{
      5     RadrootsCalendar, RadrootsCalendarDateEvent, RadrootsCalendarEventRsvp,
      6     RadrootsCalendarTimeEvent,
      7 };
      8 use radroots_events::comment::RadrootsComment;
      9 use radroots_events::coop::RadrootsCoop;
     10 use radroots_events::document::RadrootsDocument;
     11 use radroots_events::farm::RadrootsFarm;
     12 use radroots_events::farm_crdt::RadrootsFarmCrdtChange;
     13 use radroots_events::farm_file::RadrootsFarmFileMetadata;
     14 use radroots_events::farm_workspace::RadrootsFarmWorkspaceManifest;
     15 use radroots_events::file_metadata::RadrootsFileMetadata;
     16 use radroots_events::follow::RadrootsFollow;
     17 use radroots_events::gift_wrap::RadrootsGiftWrap;
     18 use radroots_events::group::{
     19     RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite,
     20     RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata,
     21     RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest, RadrootsGroupMembers,
     22     RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser, RadrootsGroupRoles,
     23 };
     24 use radroots_events::http_auth::RadrootsHttpAuth;
     25 use radroots_events::job_feedback::RadrootsJobFeedback;
     26 use radroots_events::job_request::RadrootsJobRequest;
     27 use radroots_events::job_result::RadrootsJobResult;
     28 use radroots_events::list::RadrootsList;
     29 use radroots_events::list_set::RadrootsListSet;
     30 use radroots_events::listing::RadrootsListing;
     31 use radroots_events::message::RadrootsMessage;
     32 use radroots_events::message_file::RadrootsMessageFile;
     33 use radroots_events::plot::RadrootsPlot;
     34 use radroots_events::post::RadrootsPost;
     35 use radroots_events::reaction::RadrootsReaction;
     36 use radroots_events::relay_auth::RadrootsRelayAuth;
     37 use radroots_events::report::RadrootsReport;
     38 use radroots_events::repost::{RadrootsGenericRepost, RadrootsRepost};
     39 use radroots_events::seal::RadrootsSeal;
     40 use radroots_events_codec::article::encode::article_build_tags;
     41 use radroots_events_codec::calendar::encode::{
     42     calendar_collection_build_tags, calendar_date_event_build_tags, calendar_time_event_build_tags,
     43     rsvp_build_tags,
     44 };
     45 use radroots_events_codec::comment::encode::comment_build_tags;
     46 use radroots_events_codec::coop::encode::coop_build_tags;
     47 use radroots_events_codec::document::encode::document_build_tags;
     48 use radroots_events_codec::farm::encode::farm_build_tags;
     49 use radroots_events_codec::farm_crdt::encode::farm_crdt_change_build_tags_with_author;
     50 use radroots_events_codec::farm_file::encode::farm_file_metadata_build_tags;
     51 use radroots_events_codec::farm_workspace::encode::farm_workspace_build_tags;
     52 use radroots_events_codec::file_metadata::encode::file_metadata_build_tags;
     53 use radroots_events_codec::follow::encode::follow_build_tags;
     54 use radroots_events_codec::gift_wrap::encode::gift_wrap_build_tags;
     55 use radroots_events_codec::group::encode::{
     56     group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags,
     57     group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags,
     58     group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags,
     59     group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags,
     60     group_roles_build_tags,
     61 };
     62 use radroots_events_codec::http_auth::encode::http_auth_build_tags;
     63 use radroots_events_codec::job::feedback::encode::job_feedback_build_tags;
     64 use radroots_events_codec::job::request::encode::job_request_build_tags;
     65 use radroots_events_codec::job::result::encode::job_result_build_tags;
     66 use radroots_events_codec::list::encode::list_build_tags;
     67 use radroots_events_codec::list_set::encode::list_set_build_tags;
     68 use radroots_events_codec::listing::tags::{
     69     listing_tags as listing_tags_impl, listing_tags_full as listing_tags_full_impl,
     70 };
     71 use radroots_events_codec::message::encode::message_build_tags;
     72 use radroots_events_codec::message_file::encode::message_file_build_tags;
     73 use radroots_events_codec::plot::encode::plot_build_tags;
     74 use radroots_events_codec::post::encode::post_build_tags;
     75 use radroots_events_codec::reaction::encode::reaction_build_tags;
     76 use radroots_events_codec::relay_auth::encode::relay_auth_build_tags;
     77 use radroots_events_codec::report::encode::report_build_tags;
     78 use radroots_events_codec::repost::encode::{generic_repost_build_tags, repost_build_tags};
     79 use radroots_events_codec::seal::encode::seal_build_tags;
     80 use serde::de::DeserializeOwned;
     81 #[cfg(target_arch = "wasm32")]
     82 use wasm_bindgen::JsValue;
     83 #[cfg(target_arch = "wasm32")]
     84 use wasm_bindgen::prelude::*;
     85 
     86 #[cfg(target_arch = "wasm32")]
     87 type RadrootsJsValue = JsValue;
     88 
     89 #[cfg(not(target_arch = "wasm32"))]
     90 type RadrootsJsValue = String;
     91 
     92 fn err_js<E: ToString>(err: E) -> RadrootsJsValue {
     93     #[cfg(target_arch = "wasm32")]
     94     {
     95         JsValue::from_str(&err.to_string())
     96     }
     97     #[cfg(not(target_arch = "wasm32"))]
     98     {
     99         err.to_string()
    100     }
    101 }
    102 
    103 fn normalized_payload(input: &str) -> &str {
    104     if input.is_empty() { "{}" } else { input }
    105 }
    106 
    107 fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
    108     serde_json::from_str(normalized_payload(input)).map_err(err_js)
    109 }
    110 
    111 fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
    112     serde_json::to_string(&tags).map_err(err_js)
    113 }
    114 
    115 fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
    116 where
    117     T: DeserializeOwned,
    118     E: ToString,
    119     F: FnOnce(&T) -> Result<Vec<Vec<String>>, E>,
    120 {
    121     let value = parse_json::<T>(input)?;
    122     let tags = build(&value).map_err(err_js)?;
    123     tags_to_json(tags)
    124 }
    125 
    126 fn build_tags_json_infallible<T, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
    127 where
    128     T: DeserializeOwned,
    129     F: FnOnce(&T) -> Vec<Vec<String>>,
    130 {
    131     let value = parse_json::<T>(input)?;
    132     let tags = build(&value);
    133     tags_to_json(tags)
    134 }
    135 
    136 #[derive(serde::Deserialize)]
    137 struct FarmCrdtTagsInput {
    138     change: RadrootsFarmCrdtChange,
    139     author_pubkey: String,
    140 }
    141 
    142 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags))]
    143 pub fn listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
    144     build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_impl)
    145 }
    146 
    147 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags_full))]
    148 pub fn listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> {
    149     build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_full_impl)
    150 }
    151 
    152 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = post_tags))]
    153 pub fn post_tags(post_json: &str) -> Result<String, RadrootsJsValue> {
    154     build_tags_json::<RadrootsPost, _, _>(post_json, post_build_tags)
    155 }
    156 
    157 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = comment_tags))]
    158 pub fn comment_tags(comment_json: &str) -> Result<String, RadrootsJsValue> {
    159     build_tags_json::<RadrootsComment, _, _>(comment_json, comment_build_tags)
    160 }
    161 
    162 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))]
    163 pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
    164     build_tags_json::<RadrootsArticle, _, _>(article_json, article_build_tags)
    165 }
    166 
    167 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))]
    168 pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> {
    169     build_tags_json::<RadrootsFileMetadata, _, _>(metadata_json, file_metadata_build_tags)
    170 }
    171 
    172 #[cfg_attr(
    173     target_arch = "wasm32",
    174     wasm_bindgen(js_name = calendar_date_event_tags)
    175 )]
    176 pub fn calendar_date_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> {
    177     build_tags_json::<RadrootsCalendarDateEvent, _, _>(event_json, calendar_date_event_build_tags)
    178 }
    179 
    180 #[cfg_attr(
    181     target_arch = "wasm32",
    182     wasm_bindgen(js_name = calendar_time_event_tags)
    183 )]
    184 pub fn calendar_time_event_tags(event_json: &str) -> Result<String, RadrootsJsValue> {
    185     build_tags_json::<RadrootsCalendarTimeEvent, _, _>(event_json, calendar_time_event_build_tags)
    186 }
    187 
    188 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = calendar_tags))]
    189 pub fn calendar_tags(calendar_json: &str) -> Result<String, RadrootsJsValue> {
    190     build_tags_json::<RadrootsCalendar, _, _>(calendar_json, calendar_collection_build_tags)
    191 }
    192 
    193 #[cfg_attr(
    194     target_arch = "wasm32",
    195     wasm_bindgen(js_name = calendar_event_rsvp_tags)
    196 )]
    197 pub fn calendar_event_rsvp_tags(rsvp_json: &str) -> Result<String, RadrootsJsValue> {
    198     build_tags_json::<RadrootsCalendarEventRsvp, _, _>(rsvp_json, rsvp_build_tags)
    199 }
    200 
    201 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))]
    202 pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
    203     build_tags_json::<RadrootsRepost, _, _>(repost_json, repost_build_tags)
    204 }
    205 
    206 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))]
    207 pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
    208     build_tags_json::<RadrootsGenericRepost, _, _>(repost_json, generic_repost_build_tags)
    209 }
    210 
    211 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))]
    212 pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
    213     build_tags_json::<RadrootsReport, _, _>(report_json, report_build_tags)
    214 }
    215 
    216 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))]
    217 pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> {
    218     build_tags_json::<RadrootsFollow, _, _>(follow_json, follow_build_tags)
    219 }
    220 
    221 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = document_tags))]
    222 pub fn document_tags(document_json: &str) -> Result<String, RadrootsJsValue> {
    223     build_tags_json::<RadrootsDocument, _, _>(document_json, document_build_tags)
    224 }
    225 
    226 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = coop_tags))]
    227 pub fn coop_tags(coop_json: &str) -> Result<String, RadrootsJsValue> {
    228     build_tags_json::<RadrootsCoop, _, _>(coop_json, coop_build_tags)
    229 }
    230 
    231 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_tags))]
    232 pub fn farm_tags(farm_json: &str) -> Result<String, RadrootsJsValue> {
    233     build_tags_json::<RadrootsFarm, _, _>(farm_json, farm_build_tags)
    234 }
    235 
    236 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_tags))]
    237 pub fn list_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
    238     build_tags_json::<RadrootsList, _, _>(list_json, list_build_tags)
    239 }
    240 
    241 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_set_tags))]
    242 pub fn list_set_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
    243     build_tags_json::<RadrootsListSet, _, _>(list_json, list_set_build_tags)
    244 }
    245 
    246 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = plot_tags))]
    247 pub fn plot_tags(plot_json: &str) -> Result<String, RadrootsJsValue> {
    248     build_tags_json::<RadrootsPlot, _, _>(plot_json, plot_build_tags)
    249 }
    250 
    251 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_request_tags))]
    252 pub fn job_request_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    253     build_tags_json_infallible::<RadrootsJobRequest, _>(job_json, job_request_build_tags)
    254 }
    255 
    256 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_result_tags))]
    257 pub fn job_result_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    258     build_tags_json_infallible::<RadrootsJobResult, _>(job_json, job_result_build_tags)
    259 }
    260 
    261 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_feedback_tags))]
    262 pub fn job_feedback_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    263     build_tags_json_infallible::<RadrootsJobFeedback, _>(job_json, job_feedback_build_tags)
    264 }
    265 
    266 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = reaction_tags))]
    267 pub fn reaction_tags(reaction_json: &str) -> Result<String, RadrootsJsValue> {
    268     build_tags_json::<RadrootsReaction, _, _>(reaction_json, reaction_build_tags)
    269 }
    270 
    271 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_tags))]
    272 pub fn message_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
    273     build_tags_json::<RadrootsMessage, _, _>(message_json, message_build_tags)
    274 }
    275 
    276 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_file_tags))]
    277 pub fn message_file_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
    278     build_tags_json::<RadrootsMessageFile, _, _>(message_json, message_file_build_tags)
    279 }
    280 
    281 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = seal_tags))]
    282 pub fn seal_tags(seal_json: &str) -> Result<String, RadrootsJsValue> {
    283     build_tags_json::<RadrootsSeal, _, _>(seal_json, seal_build_tags)
    284 }
    285 
    286 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = gift_wrap_tags))]
    287 pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> {
    288     build_tags_json::<RadrootsGiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags)
    289 }
    290 
    291 #[cfg_attr(
    292     target_arch = "wasm32",
    293     wasm_bindgen(js_name = farm_workspace_manifest_tags)
    294 )]
    295 pub fn farm_workspace_manifest_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> {
    296     build_tags_json::<RadrootsFarmWorkspaceManifest, _, _>(
    297         workspace_json,
    298         farm_workspace_build_tags,
    299     )
    300 }
    301 
    302 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_change_tags))]
    303 pub fn farm_crdt_change_tags(input_json: &str) -> Result<String, RadrootsJsValue> {
    304     let input = parse_json::<FarmCrdtTagsInput>(input_json)?;
    305     let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey))
    306         .map_err(err_js)?;
    307     tags_to_json(tags)
    308 }
    309 
    310 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_metadata_tags))]
    311 pub fn farm_file_metadata_tags(file_json: &str) -> Result<String, RadrootsJsValue> {
    312     build_tags_json::<RadrootsFarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags)
    313 }
    314 
    315 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))]
    316 pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
    317     build_tags_json::<RadrootsRelayAuth, _, _>(auth_json, relay_auth_build_tags)
    318 }
    319 
    320 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))]
    321 pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
    322     build_tags_json::<RadrootsHttpAuth, _, _>(auth_json, http_auth_build_tags)
    323 }
    324 
    325 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))]
    326 pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    327     build_tags_json::<RadrootsGroupPutUser, _, _>(group_json, group_put_user_build_tags)
    328 }
    329 
    330 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))]
    331 pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    332     build_tags_json::<RadrootsGroupRemoveUser, _, _>(group_json, group_remove_user_build_tags)
    333 }
    334 
    335 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))]
    336 pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    337     build_tags_json::<RadrootsGroupCreateGroup, _, _>(group_json, group_create_group_build_tags)
    338 }
    339 
    340 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))]
    341 pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    342     build_tags_json::<RadrootsGroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags)
    343 }
    344 
    345 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))]
    346 pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    347     build_tags_json::<RadrootsGroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags)
    348 }
    349 
    350 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))]
    351 pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    352     build_tags_json::<RadrootsGroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags)
    353 }
    354 
    355 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))]
    356 pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    357     build_tags_json::<RadrootsGroupCreateInvite, _, _>(group_json, group_create_invite_build_tags)
    358 }
    359 
    360 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))]
    361 pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    362     build_tags_json::<RadrootsGroupJoinRequest, _, _>(group_json, group_join_request_build_tags)
    363 }
    364 
    365 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))]
    366 pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    367     build_tags_json::<RadrootsGroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags)
    368 }
    369 
    370 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))]
    371 pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    372     build_tags_json::<RadrootsGroupMetadata, _, _>(group_json, group_metadata_build_tags)
    373 }
    374 
    375 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))]
    376 pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    377     build_tags_json::<RadrootsGroupAdmins, _, _>(group_json, group_admins_build_tags)
    378 }
    379 
    380 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))]
    381 pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    382     build_tags_json::<RadrootsGroupMembers, _, _>(group_json, group_members_build_tags)
    383 }
    384 
    385 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))]
    386 pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    387     build_tags_json::<RadrootsGroupRoles, _, _>(group_json, group_roles_build_tags)
    388 }
    389 
    390 #[cfg(test)]
    391 mod tests {
    392     use super::*;
    393     use radroots_core::{
    394         RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
    395         RadrootsCoreQuantityPrice, RadrootsCoreUnit,
    396     };
    397     use radroots_events::farm::RadrootsFarmRef;
    398     use radroots_events::farm_crdt::{
    399         RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RadrootsCrdtBackend, RadrootsFarmCrdtDocumentKind,
    400         RadrootsFarmSemanticKind,
    401     };
    402     use radroots_events::farm_file::{
    403         RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, RadrootsFarmFileSource,
    404     };
    405     use radroots_events::farm_workspace::{
    406         RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, RADROOTS_FARM_WORKSPACE_SCHEMA,
    407         RadrootsFarmWorkspaceManifest, RadrootsFarmWorkspaceMediaServer, RadrootsFarmWorkspaceRef,
    408         RadrootsFarmWorkspaceRelay, RadrootsFarmWorkspaceRelayMode,
    409     };
    410     use radroots_events::group::{
    411         RadrootsGroupAdmins, RadrootsGroupCreateGroup, RadrootsGroupCreateInvite,
    412         RadrootsGroupDeleteEvent, RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata,
    413         RadrootsGroupEditableMetadata, RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest,
    414         RadrootsGroupMembers, RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser,
    415         RadrootsGroupRole, RadrootsGroupRoles, RadrootsGroupUserRef,
    416     };
    417     use radroots_events::http_auth::RadrootsHttpAuth;
    418     use radroots_events::job::JobInputType;
    419     use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam};
    420     use radroots_events::kinds::KIND_FARM_FILE_METADATA;
    421     use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct};
    422     use radroots_events::relay_auth::RadrootsRelayAuth;
    423     use radroots_events::social::{
    424         RadrootsCalendarDateValue, RadrootsCalendarEventFreeBusy, RadrootsCalendarEventRsvpStatus,
    425         RadrootsCalendarParticipant, RadrootsReportFileTarget, RadrootsReportType,
    426         RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaDimensions,
    427         RadrootsSocialMediaMetadata, RadrootsSocialTarget,
    428     };
    429 
    430     fn sample_listing() -> RadrootsListing {
    431         let quantity =
    432             RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
    433         let price = RadrootsCoreQuantityPrice::new(
    434             RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
    435             quantity.clone(),
    436         );
    437 
    438         RadrootsListing {
    439             d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"),
    440             published_at: None,
    441             farm: RadrootsFarmRef {
    442                 pubkey: "farm_pubkey".to_string(),
    443                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    444             },
    445             product: RadrootsListingProduct {
    446                 key: "sku".to_string(),
    447                 title: "widget".to_string(),
    448                 category: "tools".to_string(),
    449                 summary: None,
    450                 process: None,
    451                 lot: None,
    452                 location: None,
    453                 profile: None,
    454                 year: None,
    455             },
    456             primary_bin_id: "bin-1".parse().expect("primary bin id"),
    457             bins: vec![RadrootsListingBin {
    458                 bin_id: "bin-1".parse().expect("bin id"),
    459                 quantity,
    460                 price_per_canonical_unit: price,
    461                 display_amount: None,
    462                 display_unit: None,
    463                 display_label: None,
    464                 display_price: None,
    465                 display_price_unit: None,
    466             }],
    467             resource_area: None,
    468             plot: None,
    469             discounts: None,
    470             inventory_available: None,
    471             availability: None,
    472             delivery_method: None,
    473             location: None,
    474             images: None,
    475         }
    476     }
    477 
    478     fn synthetic_pubkey(seed: char) -> String {
    479         seed.to_string().repeat(64)
    480     }
    481 
    482     fn synthetic_event_id(seed: char) -> String {
    483         seed.to_string().repeat(64)
    484     }
    485 
    486     fn social_farm_anchor() -> RadrootsSocialFarmAnchor {
    487         RadrootsSocialFarmAnchor {
    488             farm: RadrootsFarmRef {
    489                 pubkey: synthetic_pubkey('a'),
    490                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    491             },
    492             relays: Some(vec!["wss://relay.example.test".to_string()]),
    493         }
    494     }
    495 
    496     fn event_target(kind: u32, seed: char) -> RadrootsSocialTarget {
    497         RadrootsSocialTarget::Event {
    498             id: synthetic_event_id(seed),
    499             author: Some(synthetic_pubkey('b')),
    500             event_kind: Some(kind),
    501             relays: Some(vec!["wss://relay.example.test".to_string()]),
    502         }
    503     }
    504 
    505     fn address_target(kind: u32, d_tag: &str) -> RadrootsSocialTarget {
    506         let author = synthetic_pubkey('c');
    507         RadrootsSocialTarget::Address {
    508             address: format!("{kind}:{author}:{d_tag}"),
    509             author: Some(author),
    510             event_kind: Some(kind),
    511             relays: Some(vec!["wss://relay2.example.test".to_string()]),
    512         }
    513     }
    514 
    515     fn social_location() -> RadrootsSocialLocation {
    516         RadrootsSocialLocation {
    517             name: Some("field edge".to_string()),
    518             geohash: Some("c23nb62w20st".to_string()),
    519         }
    520     }
    521 
    522     fn sample_post() -> RadrootsPost {
    523         RadrootsPost {
    524             content: "field update".to_string(),
    525             farm: Some(social_farm_anchor()),
    526             address_refs: Some(vec![address_target(30023, "AAAAAAAAAAAAAAAAAAAAAQ")]),
    527             location: Some(social_location()),
    528             topics: Some(vec!["soil".to_string(), "market".to_string()]),
    529             quote_refs: Some(vec![event_target(30023, 'd')]),
    530             media: Some(vec![RadrootsSocialMediaMetadata {
    531                 url: Some("https://media.example.test/field.jpg".to_string()),
    532                 mime_type: Some("image/jpeg".to_string()),
    533                 sha256: Some(
    534                     "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
    535                 ),
    536                 original_sha256: None,
    537                 size: Some(4096),
    538                 dimensions: Some(RadrootsSocialMediaDimensions {
    539                     width: 1200,
    540                     height: 800,
    541                 }),
    542                 blurhash: None,
    543                 thumbnails: None,
    544                 image: None,
    545                 summary: Some("field photo".to_string()),
    546                 alt: Some("rows after harvest".to_string()),
    547                 fallback: None,
    548                 magnet: Some("magnet:?xt=urn:btih:abc".to_string()),
    549                 content_hashes: Some(vec!["sha256:field".to_string()]),
    550                 services: Some(vec!["https://media.example.test".to_string()]),
    551                 imeta: None,
    552             }]),
    553         }
    554     }
    555 
    556     fn sample_article() -> RadrootsArticle {
    557         RadrootsArticle {
    558             d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    559             title: "soil notes".to_string(),
    560             content: "# soil notes".to_string(),
    561             summary: Some("cover crop observations".to_string()),
    562             image: Some("https://media.example.test/article.jpg".to_string()),
    563             published_at: Some(1_780_000_000),
    564             farm: Some(social_farm_anchor()),
    565             location: Some(social_location()),
    566             topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]),
    567         }
    568     }
    569 
    570     fn sample_public_file_metadata() -> RadrootsFileMetadata {
    571         RadrootsFileMetadata {
    572             url: "https://media.example.test/public.jpg".to_string(),
    573             mime_type: "image/jpeg".to_string(),
    574             sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
    575             original_sha256: Some(
    576                 "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
    577             ),
    578             size: Some(4096),
    579             dimensions: Some(RadrootsSocialMediaDimensions {
    580                 width: 1200,
    581                 height: 800,
    582             }),
    583             blurhash: None,
    584             thumbnails: None,
    585             summary: Some("public field photo".to_string()),
    586             alt: Some("rows after harvest".to_string()),
    587             fallback: Some("https://media.example.test/fallback.jpg".to_string()),
    588             magnet: Some("magnet:?xt=urn:btih:abc".to_string()),
    589             content_hashes: Some(vec!["sha256:field".to_string()]),
    590             services: Some(vec!["https://media.example.test".to_string()]),
    591             content: Some("caption".to_string()),
    592         }
    593     }
    594 
    595     fn sample_calendar_date_event() -> RadrootsCalendarDateEvent {
    596         RadrootsCalendarDateEvent {
    597             d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    598             title: "market day".to_string(),
    599             start: "2026-06-20".to_string(),
    600             description: Some("Farm stand pickup window.".to_string()),
    601             end: Some("2026-06-21".to_string()),
    602             days: Some(vec![RadrootsCalendarDateValue {
    603                 value: "2026-06-20".to_string(),
    604             }]),
    605             location: Some(social_location()),
    606             summary: Some("weekly pickup".to_string()),
    607             image: None,
    608             participants: Some(vec![RadrootsCalendarParticipant {
    609                 pubkey: synthetic_pubkey('e'),
    610                 relay: Some("wss://relay.example.test".to_string()),
    611                 role: Some("host".to_string()),
    612             }]),
    613         }
    614     }
    615 
    616     fn sample_calendar_time_event() -> RadrootsCalendarTimeEvent {
    617         RadrootsCalendarTimeEvent {
    618             d_tag: "AAAAAAAAAAAAAAAAAAAA-A".to_string(),
    619             title: "wash pack shift".to_string(),
    620             start: 1_781_895_600,
    621             dates: vec![RadrootsCalendarDateValue {
    622                 value: "2026-06-20".to_string(),
    623             }],
    624             description: Some("Prepare CSA bins before pickup.".to_string()),
    625             end: Some(1_781_899_200),
    626             start_tzid: Some("America/Vancouver".to_string()),
    627             end_tzid: Some("America/Vancouver".to_string()),
    628             location: Some(social_location()),
    629             summary: Some("field crew".to_string()),
    630             image: None,
    631             participants: None,
    632         }
    633     }
    634 
    635     fn sample_calendar() -> RadrootsCalendar {
    636         RadrootsCalendar {
    637             d_tag: "AAAAAAAAAAAAAAAAAAAA_A".to_string(),
    638             title: "farm calendar".to_string(),
    639             events: vec![address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A")],
    640             description: Some("Shared schedule for farm operations.".to_string()),
    641             summary: Some("field schedule".to_string()),
    642             image: None,
    643         }
    644     }
    645 
    646     fn sample_calendar_rsvp() -> RadrootsCalendarEventRsvp {
    647         RadrootsCalendarEventRsvp {
    648             d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    649             event: address_target(31923, "AAAAAAAAAAAAAAAAAAAA-A"),
    650             event_id: Some(synthetic_event_id('f')),
    651             status: RadrootsCalendarEventRsvpStatus::Tentative,
    652             free_busy: Some(RadrootsCalendarEventFreeBusy::Busy),
    653             note: Some("depends on harvest".to_string()),
    654             participants: None,
    655         }
    656     }
    657 
    658     fn sample_comment() -> RadrootsComment {
    659         RadrootsComment {
    660             root: event_target(30023, 'a'),
    661             parent: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
    662             content: "great notes".to_string(),
    663         }
    664     }
    665 
    666     fn sample_reaction() -> RadrootsReaction {
    667         RadrootsReaction {
    668             target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
    669             content: String::new(),
    670         }
    671     }
    672 
    673     fn sample_repost() -> RadrootsRepost {
    674         RadrootsRepost {
    675             target: event_target(1, 'b'),
    676             content: Some("field update".to_string()),
    677         }
    678     }
    679 
    680     fn sample_generic_repost() -> RadrootsGenericRepost {
    681         RadrootsGenericRepost {
    682             target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
    683             target_kind: 30023,
    684             content: Some("article share".to_string()),
    685         }
    686     }
    687 
    688     fn sample_report() -> RadrootsReport {
    689         RadrootsReport {
    690             reported_pubkey: synthetic_pubkey('b'),
    691             report_type: RadrootsReportType::Spam,
    692             event: Some(event_target(1, 'c')),
    693             file: Some(RadrootsReportFileTarget {
    694                 sha256: Some(
    695                     "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
    696                 ),
    697                 url: Some("https://media.example.test/bad.jpg".to_string()),
    698                 magnet: None,
    699             }),
    700             content: Some("spam report".to_string()),
    701         }
    702     }
    703 
    704     fn sample_job_request() -> RadrootsJobRequest {
    705         RadrootsJobRequest {
    706             kind: 5100,
    707             inputs: vec![RadrootsJobInput {
    708                 data: "alpha".to_string(),
    709                 input_type: JobInputType::Text,
    710                 relay: None,
    711                 marker: None,
    712             }],
    713             output: None,
    714             params: vec![RadrootsJobParam {
    715                 key: "mode".to_string(),
    716                 value: "fast".to_string(),
    717             }],
    718             bid_sat: Some(42),
    719             relays: vec!["wss://relay.example.com".to_string()],
    720             providers: vec!["provider-a".to_string()],
    721             topics: vec!["topic-a".to_string()],
    722             encrypted: false,
    723         }
    724     }
    725 
    726     fn sample_workspace_manifest() -> RadrootsFarmWorkspaceManifest {
    727         RadrootsFarmWorkspaceManifest {
    728             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    729             schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(),
    730             farm_group_id: "field-group".to_string(),
    731             name: "Small Regen Farm".to_string(),
    732             owner_pubkey: "workspace_owner_pubkey".to_string(),
    733             farm: Some(RadrootsFarmRef {
    734                 pubkey: "farm_pubkey".to_string(),
    735                 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    736             }),
    737             relays: vec![RadrootsFarmWorkspaceRelay {
    738                 url: "wss://relay.example.invalid/farm/field-group".to_string(),
    739                 mode: RadrootsFarmWorkspaceRelayMode::ReadWrite,
    740             }],
    741             media_servers: vec![RadrootsFarmWorkspaceMediaServer {
    742                 url: "https://media.example.invalid/farm/field-group".to_string(),
    743                 service: "RadrootsPrivateMedia".to_string(),
    744             }],
    745             supported_kinds: vec![78, 30078, KIND_FARM_FILE_METADATA],
    746             protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(),
    747             created_at_ms: 1_780_000_000_000,
    748             updated_at_ms: None,
    749         }
    750     }
    751 
    752     fn sample_crdt_change() -> RadrootsFarmCrdtChange {
    753         RadrootsFarmCrdtChange {
    754             schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
    755             workspace: RadrootsFarmWorkspaceRef {
    756                 pubkey: "workspace_pubkey".to_string(),
    757                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    758             },
    759             farm_group_id: "field-group".to_string(),
    760             document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    761             document_kind: RadrootsFarmCrdtDocumentKind::FarmTask,
    762             crdt_backend: RadrootsCrdtBackend::Automerge,
    763             crdt_backend_version: Some("0.x".to_string()),
    764             actor_id: "actor_abc".to_string(),
    765             change_hash: "crdt_hash_abc".to_string(),
    766             dependencies: Vec::new(),
    767             encoded_change: "abc-DEF_012".to_string(),
    768             semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate,
    769             business_time_ms: 1_780_000_000_000,
    770             author_member_id: Some("member_abc".to_string()),
    771             app_version: Some("0.1.0".to_string()),
    772         }
    773     }
    774 
    775     fn sample_file_metadata() -> RadrootsFarmFileMetadata {
    776         RadrootsFarmFileMetadata {
    777             d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    778             workspace: RadrootsFarmWorkspaceRef {
    779                 pubkey: "workspace_pubkey".to_string(),
    780                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    781             },
    782             farm_group_id: "field-group".to_string(),
    783             owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    784             owner_document_kind: RadrootsFarmCrdtDocumentKind::FarmTask,
    785             caption: Some("Tomatoes harvested from Patch Y.".to_string()),
    786             url: "https://media.example.invalid/blob/sha256".to_string(),
    787             mime_type: "image/jpeg".to_string(),
    788             sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
    789             original_sha256: None,
    790             size_bytes: Some(123_456),
    791             dimensions: Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }),
    792             blurhash: None,
    793             thumb: Some(RadrootsFarmFileSource {
    794                 url: "https://media.example.invalid/thumb/sha256".to_string(),
    795                 mime_type: Some("image/jpeg".to_string()),
    796                 dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 240 }),
    797             }),
    798             image: None,
    799             alt: Some("Harvested tomatoes in a crate".to_string()),
    800             fallbacks: Vec::new(),
    801         }
    802     }
    803 
    804     fn sample_group_metadata() -> RadrootsGroupEditableMetadata {
    805         RadrootsGroupEditableMetadata {
    806             name: Some("Small Regen Farm".to_string()),
    807             about: Some("Field app group".to_string()),
    808             picture: Some("https://media.example.invalid/group.png".to_string()),
    809             is_private: false,
    810             is_restricted: true,
    811             is_closed: false,
    812             is_hidden: false,
    813             supported_kinds: Some(vec![78, 30078, KIND_FARM_FILE_METADATA]),
    814         }
    815     }
    816 
    817     fn sample_group_user(role: &str) -> RadrootsGroupUserRef {
    818         RadrootsGroupUserRef {
    819             pubkey: format!("{role}_pubkey"),
    820             roles: vec![role.to_string()],
    821         }
    822     }
    823 
    824     fn sample_group_role() -> RadrootsGroupRole {
    825         RadrootsGroupRole {
    826             name: "member".to_string(),
    827             description: Some("can read and write group events".to_string()),
    828             permissions: vec!["read".to_string(), "write".to_string()],
    829         }
    830     }
    831 
    832     fn assert_tags_json(value: Result<String, RadrootsJsValue>) {
    833         let tags = tags_json(value);
    834         assert!(!tags.is_empty());
    835     }
    836 
    837     fn tags_json(value: Result<String, RadrootsJsValue>) -> Vec<Vec<String>> {
    838         let json = value.expect("tags json");
    839         serde_json::from_str(&json).expect("tags")
    840     }
    841 
    842     fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
    843         tags.iter().any(|tag| {
    844             tag.first().map(|entry| entry.as_str()) == Some(key)
    845                 && tag.get(1).map(|entry| entry.as_str()) == Some(value)
    846         })
    847     }
    848 
    849     #[test]
    850     fn bindings_reject_invalid_json() {
    851         let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 46] = [
    852             listing_tags,
    853             listing_tags_full,
    854             post_tags,
    855             comment_tags,
    856             article_tags,
    857             file_metadata_tags,
    858             calendar_date_event_tags,
    859             calendar_time_event_tags,
    860             calendar_tags,
    861             calendar_event_rsvp_tags,
    862             repost_tags,
    863             generic_repost_tags,
    864             report_tags,
    865             follow_tags,
    866             document_tags,
    867             coop_tags,
    868             farm_tags,
    869             list_tags,
    870             list_set_tags,
    871             plot_tags,
    872             job_request_tags,
    873             job_result_tags,
    874             job_feedback_tags,
    875             reaction_tags,
    876             message_tags,
    877             message_file_tags,
    878             seal_tags,
    879             gift_wrap_tags,
    880             farm_workspace_manifest_tags,
    881             farm_crdt_change_tags,
    882             farm_file_metadata_tags,
    883             relay_auth_tags,
    884             http_auth_tags,
    885             group_put_user_tags,
    886             group_remove_user_tags,
    887             group_create_group_tags,
    888             group_edit_metadata_tags,
    889             group_delete_group_tags,
    890             group_delete_event_tags,
    891             group_create_invite_tags,
    892             group_join_request_tags,
    893             group_leave_request_tags,
    894             group_metadata_tags,
    895             group_admins_tags,
    896             group_members_tags,
    897             group_roles_tags,
    898         ];
    899 
    900         for binding in bindings {
    901             assert!(binding("{").is_err());
    902         }
    903         assert!(listing_tags("").is_err());
    904     }
    905 
    906     #[test]
    907     fn bindings_encode_to_json_when_input_is_valid() {
    908         let listing_json = serde_json::to_string(&sample_listing()).expect("listing json");
    909         let listing_tags_json = listing_tags(&listing_json).expect("listing tags");
    910         let listing_tags: Vec<Vec<String>> =
    911             serde_json::from_str(&listing_tags_json).expect("listing tags json");
    912         assert!(!listing_tags.is_empty());
    913 
    914         let request_json = serde_json::to_string(&sample_job_request()).expect("request json");
    915         let request_tags_json = job_request_tags(&request_json).expect("request tags");
    916         let request_tags: Vec<Vec<String>> =
    917             serde_json::from_str(&request_tags_json).expect("request tags json");
    918         assert!(!request_tags.is_empty());
    919     }
    920 
    921     #[test]
    922     fn social_bindings_encode_to_json_when_input_is_valid() {
    923         assert_tags_json(post_tags(
    924             &serde_json::to_string(&sample_post()).expect("post json"),
    925         ));
    926         assert_tags_json(comment_tags(
    927             &serde_json::to_string(&sample_comment()).expect("comment json"),
    928         ));
    929         assert_tags_json(article_tags(
    930             &serde_json::to_string(&sample_article()).expect("article json"),
    931         ));
    932         assert_tags_json(file_metadata_tags(
    933             &serde_json::to_string(&sample_public_file_metadata()).expect("file json"),
    934         ));
    935         assert_tags_json(calendar_date_event_tags(
    936             &serde_json::to_string(&sample_calendar_date_event()).expect("date json"),
    937         ));
    938         let time_tags = tags_json(calendar_time_event_tags(
    939             &serde_json::to_string(&sample_calendar_time_event()).expect("time json"),
    940         ));
    941         assert!(has_tag(&time_tags, "D", "2026-06-20"));
    942         assert_tags_json(calendar_tags(
    943             &serde_json::to_string(&sample_calendar()).expect("calendar json"),
    944         ));
    945         assert_tags_json(calendar_event_rsvp_tags(
    946             &serde_json::to_string(&sample_calendar_rsvp()).expect("rsvp json"),
    947         ));
    948         assert_tags_json(reaction_tags(
    949             &serde_json::to_string(&sample_reaction()).expect("reaction json"),
    950         ));
    951         assert_tags_json(repost_tags(
    952             &serde_json::to_string(&sample_repost()).expect("repost json"),
    953         ));
    954         assert_tags_json(generic_repost_tags(
    955             &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"),
    956         ));
    957         assert_tags_json(report_tags(
    958             &serde_json::to_string(&sample_report()).expect("report json"),
    959         ));
    960     }
    961 
    962     #[test]
    963     fn social_bindings_surface_builder_errors() {
    964         let mut article = sample_article();
    965         article.d_tag.clear();
    966         assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err());
    967 
    968         let mut comment = sample_comment();
    969         comment.root = event_target(1, 'a');
    970         assert!(comment_tags(&serde_json::to_string(&comment).expect("comment json")).is_err());
    971 
    972         let mut reaction = sample_reaction();
    973         reaction.target = RadrootsSocialTarget::External {
    974             id: "https://example.test/object".to_string(),
    975             external_kind: "web".to_string(),
    976             hint: None,
    977         };
    978         assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err());
    979 
    980         let mut rsvp = sample_calendar_rsvp();
    981         rsvp.event = event_target(31923, 'f');
    982         assert!(
    983             calendar_event_rsvp_tags(&serde_json::to_string(&rsvp).expect("rsvp json")).is_err()
    984         );
    985 
    986         let mut report = sample_report();
    987         report.reported_pubkey.clear();
    988         assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err());
    989     }
    990 
    991     #[test]
    992     fn field_bindings_encode_to_json_when_input_is_valid() {
    993         let workspace_json =
    994             serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
    995         assert_tags_json(farm_workspace_manifest_tags(&workspace_json));
    996 
    997         let crdt_json = serde_json::json!({
    998             "change": sample_crdt_change(),
    999             "author_pubkey": "author_pubkey"
   1000         })
   1001         .to_string();
   1002         assert_tags_json(farm_crdt_change_tags(&crdt_json));
   1003 
   1004         let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json");
   1005         assert_tags_json(farm_file_metadata_tags(&file_json));
   1006 
   1007         let relay_auth_json = serde_json::to_string(&RadrootsRelayAuth {
   1008             relay: "wss://relay.example.invalid/farm/field-group".to_string(),
   1009             challenge: "relay-provided-challenge".to_string(),
   1010         })
   1011         .expect("relay auth json");
   1012         assert_tags_json(relay_auth_tags(&relay_auth_json));
   1013 
   1014         let http_auth_json = serde_json::to_string(&RadrootsHttpAuth {
   1015             url: "https://media.example.invalid/upload".to_string(),
   1016             method: "POST".to_string(),
   1017             payload_sha256: Some(
   1018                 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
   1019             ),
   1020         })
   1021         .expect("http auth json");
   1022         assert_tags_json(http_auth_tags(&http_auth_json));
   1023     }
   1024 
   1025     #[test]
   1026     fn field_bindings_surface_builder_errors() {
   1027         let crdt_json = serde_json::json!({
   1028             "change": sample_crdt_change(),
   1029             "author_pubkey": " "
   1030         })
   1031         .to_string();
   1032 
   1033         assert!(farm_crdt_change_tags(&crdt_json).is_err());
   1034     }
   1035 
   1036     #[test]
   1037     fn group_bindings_encode_to_json_when_input_is_valid() {
   1038         let metadata = sample_group_metadata();
   1039         assert_tags_json(group_put_user_tags(
   1040             &serde_json::to_string(&RadrootsGroupPutUser {
   1041                 group_id: "field-group".to_string(),
   1042                 message: Some("add member".to_string()),
   1043                 pubkey: "member_pubkey".to_string(),
   1044                 roles: vec!["member".to_string()],
   1045             })
   1046             .expect("put user json"),
   1047         ));
   1048         assert_tags_json(group_remove_user_tags(
   1049             &serde_json::to_string(&RadrootsGroupRemoveUser {
   1050                 group_id: "field-group".to_string(),
   1051                 message: Some("remove member".to_string()),
   1052                 pubkey: "member_pubkey".to_string(),
   1053             })
   1054             .expect("remove user json"),
   1055         ));
   1056         assert_tags_json(group_create_group_tags(
   1057             &serde_json::to_string(&RadrootsGroupCreateGroup {
   1058                 group_id: "field-group".to_string(),
   1059                 message: Some("create group".to_string()),
   1060                 metadata: metadata.clone(),
   1061             })
   1062             .expect("create group json"),
   1063         ));
   1064         assert_tags_json(group_edit_metadata_tags(
   1065             &serde_json::to_string(&RadrootsGroupEditMetadata {
   1066                 group_id: "field-group".to_string(),
   1067                 message: Some("edit metadata".to_string()),
   1068                 metadata: metadata.clone(),
   1069             })
   1070             .expect("edit metadata json"),
   1071         ));
   1072         assert_tags_json(group_delete_group_tags(
   1073             &serde_json::to_string(&RadrootsGroupDeleteGroup {
   1074                 group_id: "field-group".to_string(),
   1075                 message: Some("delete group".to_string()),
   1076             })
   1077             .expect("delete group json"),
   1078         ));
   1079         assert_tags_json(group_delete_event_tags(
   1080             &serde_json::to_string(&RadrootsGroupDeleteEvent {
   1081                 group_id: "field-group".to_string(),
   1082                 message: Some("delete event".to_string()),
   1083                 event_id: "event_id".to_string(),
   1084             })
   1085             .expect("delete event json"),
   1086         ));
   1087         let invite_tags = tags_json(group_create_invite_tags(
   1088             &serde_json::to_string(&RadrootsGroupCreateInvite {
   1089                 group_id: "field-group".to_string(),
   1090                 message: Some("join the field group".to_string()),
   1091                 code: "invite-code".to_string(),
   1092             })
   1093             .expect("invite json"),
   1094         ));
   1095         assert!(invite_tags.contains(&vec!["code".to_string(), "invite-code".to_string()]));
   1096         assert_tags_json(group_join_request_tags(
   1097             &serde_json::to_string(&RadrootsGroupJoinRequest {
   1098                 group_id: "field-group".to_string(),
   1099                 message: Some("requesting access".to_string()),
   1100                 code: Some("invite-code".to_string()),
   1101             })
   1102             .expect("join json"),
   1103         ));
   1104         assert_tags_json(group_leave_request_tags(
   1105             &serde_json::to_string(&RadrootsGroupLeaveRequest {
   1106                 group_id: "field-group".to_string(),
   1107                 message: Some("leaving".to_string()),
   1108             })
   1109             .expect("leave json"),
   1110         ));
   1111         let metadata_tags = tags_json(group_metadata_tags(
   1112             &serde_json::to_string(&RadrootsGroupMetadata {
   1113                 d_tag: "field-group".to_string(),
   1114                 metadata,
   1115             })
   1116             .expect("metadata json"),
   1117         ));
   1118         assert!(metadata_tags.contains(&vec!["restricted".to_string()]));
   1119         assert!(metadata_tags.contains(&vec![
   1120             "supported_kinds".to_string(),
   1121             "78".to_string(),
   1122             "30078".to_string(),
   1123             KIND_FARM_FILE_METADATA.to_string()
   1124         ]));
   1125         assert_tags_json(group_admins_tags(
   1126             &serde_json::to_string(&RadrootsGroupAdmins {
   1127                 d_tag: "field-group".to_string(),
   1128                 description: Some("group admins".to_string()),
   1129                 admins: vec![sample_group_user("admin")],
   1130             })
   1131             .expect("admins json"),
   1132         ));
   1133         assert_tags_json(group_members_tags(
   1134             &serde_json::to_string(&RadrootsGroupMembers {
   1135                 d_tag: "field-group".to_string(),
   1136                 description: Some("group members".to_string()),
   1137                 members: vec![sample_group_user("member")],
   1138             })
   1139             .expect("members json"),
   1140         ));
   1141         assert_tags_json(group_roles_tags(
   1142             &serde_json::to_string(&RadrootsGroupRoles {
   1143                 d_tag: "field-group".to_string(),
   1144                 description: Some("group roles".to_string()),
   1145                 roles: vec![sample_group_role()],
   1146             })
   1147             .expect("roles json"),
   1148         ));
   1149     }
   1150 
   1151     #[test]
   1152     fn listing_bindings_surface_builder_errors() {
   1153         let mut listing_json = serde_json::to_value(sample_listing()).expect("listing value");
   1154         listing_json["bins"] = serde_json::Value::Array(Vec::new());
   1155         let listing_json = serde_json::to_string(&listing_json).expect("listing json");
   1156 
   1157         assert!(listing_tags(&listing_json).is_err());
   1158         assert!(listing_tags_full(&listing_json).is_err());
   1159     }
   1160 }