lib

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

structured_encode_default.rs (36376B)


      1 #[path = "../src/test_fixtures.rs"]
      2 mod test_fixtures;
      3 
      4 use radroots_core::{
      5     RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
      6     RadrootsCoreQuantityPrice, RadrootsCoreUnit,
      7 };
      8 use radroots_events::coop::{RadrootsCoop, RadrootsCoopLocation, RadrootsCoopRef};
      9 use radroots_events::document::{RadrootsDocument, RadrootsDocumentSubject};
     10 use radroots_events::farm::{
     11     RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint,
     12     RadrootsGeoJsonPolygon,
     13 };
     14 use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId};
     15 use radroots_events::kinds::{
     16     KIND_COOP, KIND_DOCUMENT, KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA, KIND_RESOURCE_HARVEST_CAP,
     17 };
     18 use radroots_events::list_set::RadrootsListSet;
     19 use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct};
     20 use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation, RadrootsPlotRef};
     21 use radroots_events::resource_area::{
     22     RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef,
     23 };
     24 use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
     25 use radroots_events_codec::coop::encode::{
     26     coop_build_tags, coop_ref_tags, to_wire_parts as coop_to_wire_parts,
     27     to_wire_parts_with_kind as coop_to_wire_parts_with_kind,
     28 };
     29 use radroots_events_codec::coop::list_sets::{
     30     coop_admins_list_set, coop_items_list_set, coop_members_farms_list_set, coop_members_list_set,
     31     coop_owners_list_set, member_of_coops_list_set,
     32 };
     33 use radroots_events_codec::document::encode::{
     34     document_build_tags, to_wire_parts as document_to_wire_parts,
     35     to_wire_parts_with_kind as document_to_wire_parts_with_kind,
     36 };
     37 use radroots_events_codec::error::EventEncodeError;
     38 use radroots_events_codec::farm::encode::{
     39     farm_build_tags, farm_ref_tags, to_wire_parts as farm_to_wire_parts,
     40     to_wire_parts_with_kind as farm_to_wire_parts_with_kind,
     41 };
     42 use radroots_events_codec::farm::list_sets::{
     43     farm_listings_list_set, farm_listings_list_set_from_listings, farm_members_list_set,
     44     farm_owners_list_set, farm_plots_list_set, farm_plots_list_set_from_plots,
     45     farm_workers_list_set, member_of_farms_list_set,
     46 };
     47 use radroots_events_codec::plot::encode::{
     48     plot_address, plot_build_tags, to_wire_parts as plot_to_wire_parts,
     49     to_wire_parts_with_kind as plot_to_wire_parts_with_kind,
     50 };
     51 use radroots_events_codec::resource_area::encode::{
     52     resource_area_build_tags, resource_area_ref_tags, to_wire_parts as resource_area_to_wire_parts,
     53     to_wire_parts_with_kind as resource_area_to_wire_parts_with_kind,
     54 };
     55 use radroots_events_codec::resource_area::list_sets::{
     56     resource_area_members_farms_list_set, resource_area_members_plots_list_set,
     57     resource_area_stewards_list_set,
     58 };
     59 use radroots_events_codec::resource_cap::encode::{
     60     resource_harvest_cap_build_tags, to_wire_parts as resource_cap_to_wire_parts,
     61     to_wire_parts_with_kind as resource_cap_to_wire_parts_with_kind,
     62 };
     63 use test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX;
     64 
     65 const TEST_PUBKEY_HEX: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX;
     66 
     67 fn listing_d_tag(raw: &str) -> RadrootsDTag {
     68     raw.parse().unwrap()
     69 }
     70 
     71 fn bin_id(raw: &str) -> RadrootsInventoryBinId {
     72     raw.parse().unwrap()
     73 }
     74 
     75 fn sample_gcs() -> RadrootsGcsLocation {
     76     RadrootsGcsLocation {
     77         lat: 37.0,
     78         lng: -122.0,
     79         geohash: "9q8yy".to_string(),
     80         point: RadrootsGeoJsonPoint {
     81             r#type: "Point".to_string(),
     82             coordinates: [-122.0, 37.0],
     83         },
     84         polygon: RadrootsGeoJsonPolygon {
     85             r#type: "Polygon".to_string(),
     86             coordinates: vec![vec![
     87                 [-122.0, 37.0],
     88                 [-122.0, 37.0001],
     89                 [-122.0001, 37.0001],
     90                 [-122.0, 37.0],
     91             ]],
     92         },
     93         accuracy: None,
     94         altitude: None,
     95         tag_0: None,
     96         label: None,
     97         area: None,
     98         elevation: None,
     99         soil: None,
    100         climate: None,
    101         gc_id: None,
    102         gc_name: None,
    103         gc_admin1_id: None,
    104         gc_admin1_name: None,
    105         gc_country_id: None,
    106         gc_country_name: None,
    107     }
    108 }
    109 
    110 fn sample_listing(d_tag: &str) -> RadrootsListing {
    111     let quantity =
    112         RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
    113     let price = RadrootsCoreQuantityPrice::new(
    114         RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
    115         quantity.clone(),
    116     );
    117     RadrootsListing {
    118         d_tag: listing_d_tag(d_tag),
    119         published_at: None,
    120         farm: RadrootsFarmRef {
    121             pubkey: TEST_PUBKEY_HEX.to_string(),
    122             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    123         },
    124         product: RadrootsListingProduct {
    125             key: "sku".to_string(),
    126             title: "Widget".to_string(),
    127             category: "Tools".to_string(),
    128             summary: None,
    129             process: None,
    130             lot: None,
    131             location: None,
    132             profile: None,
    133             year: None,
    134         },
    135         primary_bin_id: bin_id("bin-1"),
    136         bins: vec![RadrootsListingBin {
    137             bin_id: bin_id("bin-1"),
    138             quantity,
    139             price_per_canonical_unit: price,
    140             display_amount: None,
    141             display_unit: None,
    142             display_label: None,
    143             display_price: None,
    144             display_price_unit: None,
    145         }],
    146         resource_area: None,
    147         plot: None,
    148         discounts: None,
    149         inventory_available: None,
    150         availability: None,
    151         delivery_method: None,
    152         location: None,
    153         images: None,
    154     }
    155 }
    156 
    157 fn sample_farm() -> RadrootsFarm {
    158     RadrootsFarm {
    159         d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    160         name: "Farm".to_string(),
    161         about: None,
    162         website: None,
    163         picture: None,
    164         banner: None,
    165         location: Some(RadrootsFarmLocation {
    166             primary: Some("farm".to_string()),
    167             city: None,
    168             region: None,
    169             country: None,
    170             gcs: Some(sample_gcs()),
    171         }),
    172         tags: Some(vec!["organic".to_string(), " ".to_string()]),
    173     }
    174 }
    175 
    176 fn sample_coop() -> RadrootsCoop {
    177     RadrootsCoop {
    178         d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    179         name: "Coop".to_string(),
    180         about: None,
    181         website: None,
    182         picture: None,
    183         banner: None,
    184         location: Some(RadrootsCoopLocation {
    185             primary: Some("coop".to_string()),
    186             city: None,
    187             region: None,
    188             country: None,
    189             gcs: sample_gcs(),
    190         }),
    191         tags: Some(vec!["co-op".to_string(), " ".to_string()]),
    192     }
    193 }
    194 
    195 fn sample_document() -> RadrootsDocument {
    196     RadrootsDocument {
    197         d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    198         doc_type: "charter".to_string(),
    199         title: "Charter".to_string(),
    200         version: "1.0.0".to_string(),
    201         summary: None,
    202         effective_at: None,
    203         body_markdown: None,
    204         subject: RadrootsDocumentSubject {
    205             pubkey: TEST_PUBKEY_HEX.to_string(),
    206             address: Some(format!("30340:{TEST_PUBKEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA")),
    207         },
    208         tags: Some(vec!["policy".to_string(), " ".to_string()]),
    209     }
    210 }
    211 
    212 fn sample_plot() -> RadrootsPlot {
    213     RadrootsPlot {
    214         d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
    215         farm: RadrootsFarmRef {
    216             pubkey: TEST_PUBKEY_HEX.to_string(),
    217             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    218         },
    219         name: "Plot".to_string(),
    220         about: None,
    221         location: Some(RadrootsPlotLocation {
    222             primary: Some("plot".to_string()),
    223             city: None,
    224             region: None,
    225             country: None,
    226             gcs: sample_gcs(),
    227         }),
    228         tags: Some(vec!["shade-grown".to_string(), " ".to_string()]),
    229     }
    230 }
    231 
    232 fn sample_resource_area() -> RadrootsResourceArea {
    233     RadrootsResourceArea {
    234         d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    235         name: "Area".to_string(),
    236         about: None,
    237         location: RadrootsResourceAreaLocation {
    238             primary: None,
    239             city: None,
    240             region: None,
    241             country: None,
    242             gcs: sample_gcs(),
    243         },
    244         tags: Some(vec!["orchard".to_string(), " ".to_string()]),
    245     }
    246 }
    247 
    248 fn sample_resource_cap() -> RadrootsResourceHarvestCap {
    249     RadrootsResourceHarvestCap {
    250         d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
    251         resource_area: RadrootsResourceAreaRef {
    252             pubkey: TEST_PUBKEY_HEX.to_string(),
    253             d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    254         },
    255         product: RadrootsResourceHarvestProduct {
    256             key: "nutmeg".to_string(),
    257             category: Some("spice".to_string()),
    258         },
    259         start: 1,
    260         end: 2,
    261         cap_quantity: RadrootsCoreQuantity::new(
    262             RadrootsCoreDecimal::from(1000u32),
    263             RadrootsCoreUnit::MassG,
    264         ),
    265         display_amount: None,
    266         display_unit: None,
    267         display_label: None,
    268         tags: Some(vec!["seasonal".to_string(), " ".to_string()]),
    269     }
    270 }
    271 
    272 #[test]
    273 fn structured_wire_parts_cover_default_kind_and_error_paths() {
    274     let farm = sample_farm();
    275     let wire = farm_to_wire_parts(&farm).unwrap();
    276     assert_eq!(wire.kind, KIND_FARM);
    277     assert!(wire.tags.iter().any(|tag| tag[0] == "d"));
    278     assert!(wire.content.contains("\"Farm\""));
    279     assert!(matches!(
    280         farm_to_wire_parts_with_kind(&farm, KIND_COOP).unwrap_err(),
    281         EventEncodeError::InvalidKind(KIND_COOP)
    282     ));
    283     let mut invalid_farm = farm.clone();
    284     invalid_farm.d_tag = " ".to_string();
    285     assert!(matches!(
    286         farm_to_wire_parts(&invalid_farm).unwrap_err(),
    287         EventEncodeError::EmptyRequiredField("d_tag")
    288     ));
    289 
    290     let coop = sample_coop();
    291     let wire = coop_to_wire_parts(&coop).unwrap();
    292     assert_eq!(wire.kind, KIND_COOP);
    293     assert!(wire.tags.iter().any(|tag| tag[0] == "g"));
    294     assert!(wire.content.contains("\"Coop\""));
    295     assert!(matches!(
    296         coop_to_wire_parts_with_kind(&coop, KIND_FARM).unwrap_err(),
    297         EventEncodeError::InvalidKind(KIND_FARM)
    298     ));
    299     let mut invalid_coop = coop.clone();
    300     invalid_coop.name = " ".to_string();
    301     assert!(matches!(
    302         coop_to_wire_parts(&invalid_coop).unwrap_err(),
    303         EventEncodeError::EmptyRequiredField("name")
    304     ));
    305 
    306     let document = sample_document();
    307     let wire = document_to_wire_parts(&document).unwrap();
    308     assert_eq!(wire.kind, KIND_DOCUMENT);
    309     assert!(wire.tags.iter().any(|tag| tag[0] == "a"));
    310     assert!(wire.content.contains("\"Charter\""));
    311     assert!(matches!(
    312         document_to_wire_parts_with_kind(&document, KIND_FARM).unwrap_err(),
    313         EventEncodeError::InvalidKind(KIND_FARM)
    314     ));
    315     let mut invalid_document = document.clone();
    316     invalid_document.subject.pubkey = " ".to_string();
    317     assert!(matches!(
    318         document_to_wire_parts(&invalid_document).unwrap_err(),
    319         EventEncodeError::EmptyRequiredField("subject.pubkey")
    320     ));
    321 
    322     let plot = sample_plot();
    323     let wire = plot_to_wire_parts(&plot).unwrap();
    324     assert_eq!(wire.kind, KIND_PLOT);
    325     assert!(wire.tags.iter().any(|tag| tag[0] == "p"));
    326     assert!(wire.content.contains("\"Plot\""));
    327     assert!(matches!(
    328         plot_to_wire_parts_with_kind(&plot, KIND_FARM).unwrap_err(),
    329         EventEncodeError::InvalidKind(KIND_FARM)
    330     ));
    331     let mut invalid_plot = plot.clone();
    332     invalid_plot.farm.pubkey = " ".to_string();
    333     assert!(matches!(
    334         plot_to_wire_parts(&invalid_plot).unwrap_err(),
    335         EventEncodeError::EmptyRequiredField("farm.pubkey")
    336     ));
    337 
    338     let area = sample_resource_area();
    339     let wire = resource_area_to_wire_parts(&area).unwrap();
    340     assert_eq!(wire.kind, KIND_RESOURCE_AREA);
    341     assert!(wire.tags.iter().any(|tag| tag[0] == "g"));
    342     assert!(wire.content.contains("\"Area\""));
    343     assert!(matches!(
    344         resource_area_to_wire_parts_with_kind(&area, KIND_FARM).unwrap_err(),
    345         EventEncodeError::InvalidKind(KIND_FARM)
    346     ));
    347     let mut invalid_area = area.clone();
    348     invalid_area.location.gcs.geohash = " ".to_string();
    349     assert!(matches!(
    350         resource_area_to_wire_parts(&invalid_area).unwrap_err(),
    351         EventEncodeError::EmptyRequiredField("location.gcs.geohash")
    352     ));
    353 
    354     let cap = sample_resource_cap();
    355     let wire = resource_cap_to_wire_parts(&cap).unwrap();
    356     assert_eq!(wire.kind, KIND_RESOURCE_HARVEST_CAP);
    357     assert!(wire.tags.iter().any(|tag| tag[0] == "category"));
    358     assert!(wire.content.contains("\"nutmeg\""));
    359     assert!(matches!(
    360         resource_cap_to_wire_parts_with_kind(&cap, KIND_FARM).unwrap_err(),
    361         EventEncodeError::InvalidKind(KIND_FARM)
    362     ));
    363     let mut invalid_cap = cap.clone();
    364     invalid_cap.resource_area.d_tag = " ".to_string();
    365     assert!(matches!(
    366         resource_cap_to_wire_parts(&invalid_cap).unwrap_err(),
    367         EventEncodeError::EmptyRequiredField("resource_area.d_tag")
    368     ));
    369 }
    370 
    371 #[test]
    372 fn structured_build_tags_cover_optional_and_error_paths() {
    373     let farm = RadrootsFarm {
    374         d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    375         name: "Farm".to_string(),
    376         about: None,
    377         website: None,
    378         picture: None,
    379         banner: None,
    380         location: Some(RadrootsFarmLocation {
    381             primary: Some("farm".to_string()),
    382             city: None,
    383             region: None,
    384             country: None,
    385             gcs: Some(sample_gcs()),
    386         }),
    387         tags: Some(vec!["organic".to_string(), " ".to_string()]),
    388     };
    389     let farm_tags = farm_build_tags(&farm).unwrap();
    390     assert!(farm_tags.iter().any(|tag| tag[0] == "d"));
    391     assert!(
    392         farm_tags
    393             .iter()
    394             .any(|tag| tag[0] == "t" && tag[1] == "organic")
    395     );
    396     assert!(farm_tags.iter().any(|tag| tag[0] == "g"));
    397 
    398     let mut invalid_farm = farm.clone();
    399     invalid_farm
    400         .location
    401         .as_mut()
    402         .unwrap()
    403         .gcs
    404         .as_mut()
    405         .unwrap()
    406         .geohash = " ".to_string();
    407     let err = farm_build_tags(&invalid_farm).unwrap_err();
    408     assert!(matches!(
    409         err,
    410         EventEncodeError::EmptyRequiredField("location.gcs.geohash")
    411     ));
    412 
    413     let mut string_only_farm = farm.clone();
    414     string_only_farm.location.as_mut().unwrap().gcs = None;
    415     let string_only_tags = farm_build_tags(&string_only_farm).unwrap();
    416     assert!(
    417         !string_only_tags
    418             .iter()
    419             .any(|tag| tag.first().map(|v| v.as_str()) == Some("g"))
    420     );
    421 
    422     let farm_ref_tags = farm_ref_tags(&RadrootsFarmRef {
    423         pubkey: TEST_PUBKEY_HEX.to_string(),
    424         d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    425     })
    426     .unwrap();
    427     assert_eq!(farm_ref_tags.len(), 2);
    428 
    429     let coop = RadrootsCoop {
    430         d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    431         name: "Coop".to_string(),
    432         about: None,
    433         website: None,
    434         picture: None,
    435         banner: None,
    436         location: Some(RadrootsCoopLocation {
    437             primary: Some("coop".to_string()),
    438             city: None,
    439             region: None,
    440             country: None,
    441             gcs: sample_gcs(),
    442         }),
    443         tags: Some(vec!["co-op".to_string(), " ".to_string()]),
    444     };
    445     let coop_tags = coop_build_tags(&coop).unwrap();
    446     assert!(coop_tags.iter().any(|tag| tag[0] == "g"));
    447     assert!(
    448         coop_tags
    449             .iter()
    450             .any(|tag| tag[0] == "t" && tag[1] == "co-op")
    451     );
    452     let coop_ref_tags = coop_ref_tags(&RadrootsCoopRef {
    453         pubkey: TEST_PUBKEY_HEX.to_string(),
    454         d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    455     })
    456     .unwrap();
    457     assert_eq!(coop_ref_tags.len(), 2);
    458 
    459     let document = RadrootsDocument {
    460         d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    461         doc_type: "charter".to_string(),
    462         title: "Charter".to_string(),
    463         version: "1.0.0".to_string(),
    464         summary: None,
    465         effective_at: None,
    466         body_markdown: None,
    467         subject: RadrootsDocumentSubject {
    468             pubkey: TEST_PUBKEY_HEX.to_string(),
    469             address: Some(format!("30340:{TEST_PUBKEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA")),
    470         },
    471         tags: Some(vec!["policy".to_string(), " ".to_string()]),
    472     };
    473     let doc_tags = document_build_tags(&document).unwrap();
    474     assert!(doc_tags.iter().any(|tag| tag[0] == "p"));
    475     assert!(doc_tags.iter().any(|tag| tag[0] == "a"));
    476     assert!(
    477         doc_tags
    478             .iter()
    479             .any(|tag| tag[0] == "t" && tag[1] == "policy")
    480     );
    481 
    482     let mut invalid_document = document.clone();
    483     invalid_document.subject.address = Some(" ".to_string());
    484     let err = document_build_tags(&invalid_document).unwrap_err();
    485     assert!(matches!(
    486         err,
    487         EventEncodeError::EmptyRequiredField("subject.address")
    488     ));
    489 
    490     let plot = RadrootsPlot {
    491         d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
    492         farm: RadrootsFarmRef {
    493             pubkey: TEST_PUBKEY_HEX.to_string(),
    494             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    495         },
    496         name: "Plot".to_string(),
    497         about: None,
    498         location: Some(RadrootsPlotLocation {
    499             primary: Some("plot".to_string()),
    500             city: None,
    501             region: None,
    502             country: None,
    503             gcs: sample_gcs(),
    504         }),
    505         tags: Some(vec!["shade-grown".to_string(), " ".to_string()]),
    506     };
    507     let plot_tags = plot_build_tags(&plot).unwrap();
    508     assert!(plot_tags.iter().any(|tag| tag[0] == "a"));
    509     assert!(plot_tags.iter().any(|tag| tag[0] == "p"));
    510     assert!(plot_tags.iter().any(|tag| tag[0] == "g"));
    511     assert!(
    512         plot_tags
    513             .iter()
    514             .any(|tag| tag[0] == "t" && tag[1] == "shade-grown")
    515     );
    516 
    517     let mut invalid_plot = plot.clone();
    518     invalid_plot.location.as_mut().unwrap().gcs.geohash = " ".to_string();
    519     let err = plot_build_tags(&invalid_plot).unwrap_err();
    520     assert!(matches!(
    521         err,
    522         EventEncodeError::EmptyRequiredField("location.gcs.geohash")
    523     ));
    524 
    525     let err = plot_address("", "AAAAAAAAAAAAAAAAAAAABQ").unwrap_err();
    526     assert!(matches!(
    527         err,
    528         EventEncodeError::EmptyRequiredField("plot.author_pubkey")
    529     ));
    530 
    531     let area = RadrootsResourceArea {
    532         d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    533         name: "Area".to_string(),
    534         about: None,
    535         location: RadrootsResourceAreaLocation {
    536             primary: None,
    537             city: None,
    538             region: None,
    539             country: None,
    540             gcs: sample_gcs(),
    541         },
    542         tags: Some(vec!["orchard".to_string(), " ".to_string()]),
    543     };
    544     let area_tags = resource_area_build_tags(&area).unwrap();
    545     assert!(area_tags.iter().any(|tag| tag[0] == "d"));
    546     assert!(area_tags.iter().any(|tag| tag[0] == "g"));
    547     assert!(
    548         area_tags
    549             .iter()
    550             .any(|tag| tag[0] == "t" && tag[1] == "orchard")
    551     );
    552     let area_ref_tags = resource_area_ref_tags(&RadrootsResourceAreaRef {
    553         pubkey: TEST_PUBKEY_HEX.to_string(),
    554         d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    555     })
    556     .unwrap();
    557     assert_eq!(area_ref_tags.len(), 2);
    558 
    559     let mut invalid_area = area.clone();
    560     invalid_area.location.gcs.geohash = " ".to_string();
    561     let err = resource_area_build_tags(&invalid_area).unwrap_err();
    562     assert!(matches!(
    563         err,
    564         EventEncodeError::EmptyRequiredField("location.gcs.geohash")
    565     ));
    566 
    567     let cap = RadrootsResourceHarvestCap {
    568         d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
    569         resource_area: RadrootsResourceAreaRef {
    570             pubkey: TEST_PUBKEY_HEX.to_string(),
    571             d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    572         },
    573         product: RadrootsResourceHarvestProduct {
    574             key: "nutmeg".to_string(),
    575             category: Some("spice".to_string()),
    576         },
    577         start: 1,
    578         end: 2,
    579         cap_quantity: RadrootsCoreQuantity::new(
    580             RadrootsCoreDecimal::from(1000u32),
    581             RadrootsCoreUnit::MassG,
    582         ),
    583         display_amount: None,
    584         display_unit: None,
    585         display_label: None,
    586         tags: Some(vec!["seasonal".to_string(), " ".to_string()]),
    587     };
    588     let cap_tags = resource_harvest_cap_build_tags(&cap).unwrap();
    589     assert!(
    590         cap_tags
    591             .iter()
    592             .any(|tag| tag[0] == "category" && tag[1] == "spice")
    593     );
    594     assert!(
    595         cap_tags
    596             .iter()
    597             .any(|tag| tag[0] == "t" && tag[1] == "seasonal")
    598     );
    599 
    600     let mut invalid_cap = cap.clone();
    601     invalid_cap.product.key = " ".to_string();
    602     let err = resource_harvest_cap_build_tags(&invalid_cap).unwrap_err();
    603     assert!(matches!(
    604         err,
    605         EventEncodeError::EmptyRequiredField("product.key")
    606     ));
    607 }
    608 
    609 #[test]
    610 fn structured_build_tags_cover_required_field_errors() {
    611     let document = RadrootsDocument {
    612         d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
    613         doc_type: "charter".to_string(),
    614         title: "Charter".to_string(),
    615         version: "1.0.0".to_string(),
    616         summary: None,
    617         effective_at: None,
    618         body_markdown: None,
    619         subject: RadrootsDocumentSubject {
    620             pubkey: TEST_PUBKEY_HEX.to_string(),
    621             address: Some(format!("30340:{TEST_PUBKEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA")),
    622         },
    623         tags: None,
    624     };
    625     let document_tags = document_build_tags(&document).unwrap();
    626     assert!(document_tags.iter().any(|tag| tag[0] == "a"));
    627 
    628     let mut invalid_document = document.clone();
    629     invalid_document.d_tag = " ".to_string();
    630     let err = document_build_tags(&invalid_document).unwrap_err();
    631     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    632     invalid_document = document.clone();
    633     invalid_document.doc_type = " ".to_string();
    634     let err = document_build_tags(&invalid_document).unwrap_err();
    635     assert!(matches!(
    636         err,
    637         EventEncodeError::EmptyRequiredField("doc_type")
    638     ));
    639     invalid_document = document.clone();
    640     invalid_document.title = " ".to_string();
    641     let err = document_build_tags(&invalid_document).unwrap_err();
    642     assert!(matches!(err, EventEncodeError::EmptyRequiredField("title")));
    643     invalid_document = document.clone();
    644     invalid_document.version = " ".to_string();
    645     let err = document_build_tags(&invalid_document).unwrap_err();
    646     assert!(matches!(
    647         err,
    648         EventEncodeError::EmptyRequiredField("version")
    649     ));
    650     invalid_document = document.clone();
    651     invalid_document.subject.pubkey = " ".to_string();
    652     let err = document_build_tags(&invalid_document).unwrap_err();
    653     assert!(matches!(
    654         err,
    655         EventEncodeError::EmptyRequiredField("subject.pubkey")
    656     ));
    657 
    658     let farm = RadrootsFarm {
    659         d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    660         name: "Farm".to_string(),
    661         about: None,
    662         website: None,
    663         picture: None,
    664         banner: None,
    665         location: None,
    666         tags: None,
    667     };
    668     let mut invalid_farm = farm.clone();
    669     invalid_farm.d_tag = " ".to_string();
    670     let err = farm_build_tags(&invalid_farm).unwrap_err();
    671     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    672     invalid_farm = farm.clone();
    673     invalid_farm.name = " ".to_string();
    674     let err = farm_build_tags(&invalid_farm).unwrap_err();
    675     assert!(matches!(err, EventEncodeError::EmptyRequiredField("name")));
    676     let err = farm_ref_tags(&RadrootsFarmRef {
    677         pubkey: " ".to_string(),
    678         d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    679     })
    680     .unwrap_err();
    681     assert!(matches!(
    682         err,
    683         EventEncodeError::EmptyRequiredField("farm.pubkey")
    684     ));
    685     let err = farm_ref_tags(&RadrootsFarmRef {
    686         pubkey: TEST_PUBKEY_HEX.to_string(),
    687         d_tag: " ".to_string(),
    688     })
    689     .unwrap_err();
    690     assert!(matches!(
    691         err,
    692         EventEncodeError::EmptyRequiredField("farm.d_tag")
    693     ));
    694 
    695     let coop = RadrootsCoop {
    696         d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    697         name: "Coop".to_string(),
    698         about: None,
    699         website: None,
    700         picture: None,
    701         banner: None,
    702         location: None,
    703         tags: None,
    704     };
    705     let mut invalid_coop = coop.clone();
    706     invalid_coop.d_tag = " ".to_string();
    707     let err = coop_build_tags(&invalid_coop).unwrap_err();
    708     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    709     invalid_coop = coop.clone();
    710     invalid_coop.name = " ".to_string();
    711     let err = coop_build_tags(&invalid_coop).unwrap_err();
    712     assert!(matches!(err, EventEncodeError::EmptyRequiredField("name")));
    713     invalid_coop = coop.clone();
    714     invalid_coop.location = Some(RadrootsCoopLocation {
    715         primary: None,
    716         city: None,
    717         region: None,
    718         country: None,
    719         gcs: RadrootsGcsLocation {
    720             geohash: " ".to_string(),
    721             ..sample_gcs()
    722         },
    723     });
    724     let err = coop_build_tags(&invalid_coop).unwrap_err();
    725     assert!(matches!(
    726         err,
    727         EventEncodeError::EmptyRequiredField("location.gcs.geohash")
    728     ));
    729     let err = coop_ref_tags(&RadrootsCoopRef {
    730         pubkey: " ".to_string(),
    731         d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    732     })
    733     .unwrap_err();
    734     assert!(matches!(
    735         err,
    736         EventEncodeError::EmptyRequiredField("coop.pubkey")
    737     ));
    738     let err = coop_ref_tags(&RadrootsCoopRef {
    739         pubkey: TEST_PUBKEY_HEX.to_string(),
    740         d_tag: " ".to_string(),
    741     })
    742     .unwrap_err();
    743     assert!(matches!(
    744         err,
    745         EventEncodeError::EmptyRequiredField("coop.d_tag")
    746     ));
    747 
    748     let plot = RadrootsPlot {
    749         d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
    750         farm: RadrootsFarmRef {
    751             pubkey: TEST_PUBKEY_HEX.to_string(),
    752             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    753         },
    754         name: "Plot".to_string(),
    755         about: None,
    756         location: None,
    757         tags: None,
    758     };
    759     let mut invalid_plot = plot.clone();
    760     invalid_plot.d_tag = " ".to_string();
    761     let err = plot_build_tags(&invalid_plot).unwrap_err();
    762     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    763     invalid_plot = plot.clone();
    764     invalid_plot.name = " ".to_string();
    765     let err = plot_build_tags(&invalid_plot).unwrap_err();
    766     assert!(matches!(err, EventEncodeError::EmptyRequiredField("name")));
    767     invalid_plot = plot.clone();
    768     invalid_plot.farm.pubkey = " ".to_string();
    769     let err = plot_build_tags(&invalid_plot).unwrap_err();
    770     assert!(matches!(
    771         err,
    772         EventEncodeError::EmptyRequiredField("farm.pubkey")
    773     ));
    774     invalid_plot = plot.clone();
    775     invalid_plot.farm.d_tag = " ".to_string();
    776     let err = plot_build_tags(&invalid_plot).unwrap_err();
    777     assert!(matches!(
    778         err,
    779         EventEncodeError::EmptyRequiredField("farm.d_tag")
    780     ));
    781     let err = plot_address(TEST_PUBKEY_HEX, " ").unwrap_err();
    782     assert!(matches!(
    783         err,
    784         EventEncodeError::EmptyRequiredField("plot.d_tag")
    785     ));
    786 
    787     let area = RadrootsResourceArea {
    788         d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    789         name: "Area".to_string(),
    790         about: None,
    791         location: RadrootsResourceAreaLocation {
    792             primary: None,
    793             city: None,
    794             region: None,
    795             country: None,
    796             gcs: sample_gcs(),
    797         },
    798         tags: None,
    799     };
    800     let mut invalid_area = area.clone();
    801     invalid_area.d_tag = " ".to_string();
    802     let err = resource_area_build_tags(&invalid_area).unwrap_err();
    803     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    804     invalid_area = area.clone();
    805     invalid_area.name = " ".to_string();
    806     let err = resource_area_build_tags(&invalid_area).unwrap_err();
    807     assert!(matches!(err, EventEncodeError::EmptyRequiredField("name")));
    808     let err = resource_area_ref_tags(&RadrootsResourceAreaRef {
    809         pubkey: " ".to_string(),
    810         d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    811     })
    812     .unwrap_err();
    813     assert!(matches!(
    814         err,
    815         EventEncodeError::EmptyRequiredField("resource_area.pubkey")
    816     ));
    817     let err = resource_area_ref_tags(&RadrootsResourceAreaRef {
    818         pubkey: TEST_PUBKEY_HEX.to_string(),
    819         d_tag: " ".to_string(),
    820     })
    821     .unwrap_err();
    822     assert!(matches!(
    823         err,
    824         EventEncodeError::EmptyRequiredField("resource_area.d_tag")
    825     ));
    826 
    827     let cap = RadrootsResourceHarvestCap {
    828         d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
    829         resource_area: RadrootsResourceAreaRef {
    830             pubkey: TEST_PUBKEY_HEX.to_string(),
    831             d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    832         },
    833         product: RadrootsResourceHarvestProduct {
    834             key: "nutmeg".to_string(),
    835             category: Some("spice".to_string()),
    836         },
    837         start: 1,
    838         end: 2,
    839         cap_quantity: RadrootsCoreQuantity::new(
    840             RadrootsCoreDecimal::from(1000u32),
    841             RadrootsCoreUnit::MassG,
    842         ),
    843         display_amount: None,
    844         display_unit: None,
    845         display_label: None,
    846         tags: None,
    847     };
    848     let mut invalid_cap = cap.clone();
    849     invalid_cap.d_tag = " ".to_string();
    850     let err = resource_harvest_cap_build_tags(&invalid_cap).unwrap_err();
    851     assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    852     invalid_cap = cap.clone();
    853     invalid_cap.resource_area.pubkey = " ".to_string();
    854     let err = resource_harvest_cap_build_tags(&invalid_cap).unwrap_err();
    855     assert!(matches!(
    856         err,
    857         EventEncodeError::EmptyRequiredField("resource_area.pubkey")
    858     ));
    859     invalid_cap = cap.clone();
    860     invalid_cap.resource_area.d_tag = " ".to_string();
    861     let err = resource_harvest_cap_build_tags(&invalid_cap).unwrap_err();
    862     assert!(matches!(
    863         err,
    864         EventEncodeError::EmptyRequiredField("resource_area.d_tag")
    865     ));
    866     let mut no_category = cap.clone();
    867     no_category.product.category = Some(" ".to_string());
    868     let tags = resource_harvest_cap_build_tags(&no_category).unwrap();
    869     assert!(!tags.iter().any(|tag| tag[0] == "category"));
    870 }
    871 
    872 #[test]
    873 fn structured_list_sets_cover_success_and_error_paths() {
    874     let farm_id = "AAAAAAAAAAAAAAAAAAAAAA";
    875     let members = farm_members_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
    876     assert_eq!(members.d_tag, format!("farm:{farm_id}:members"));
    877     let owners = farm_owners_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
    878     assert_eq!(owners.d_tag, format!("farm:{farm_id}:members.owners"));
    879     let workers = farm_workers_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
    880     assert_eq!(workers.d_tag, format!("farm:{farm_id}:members.workers"));
    881 
    882     let plots = farm_plots_list_set(farm_id, TEST_PUBKEY_HEX, ["AAAAAAAAAAAAAAAAAAAABQ"]).unwrap();
    883     assert_eq!(plots.d_tag, format!("farm:{farm_id}:plots"));
    884     assert_eq!(plots.entries.len(), 1);
    885 
    886     let listings =
    887         farm_listings_list_set(farm_id, TEST_PUBKEY_HEX, ["AAAAAAAAAAAAAAAAAAAAAg"]).unwrap();
    888     assert_eq!(listings.d_tag, format!("farm:{farm_id}:listings"));
    889     assert_eq!(listings.entries.len(), 1);
    890 
    891     let listings_from = farm_listings_list_set_from_listings(
    892         farm_id,
    893         TEST_PUBKEY_HEX,
    894         [sample_listing("AAAAAAAAAAAAAAAAAAAAAg")].iter(),
    895     )
    896     .unwrap();
    897     assert_eq!(listings_from.entries.len(), 1);
    898 
    899     let plots_from = farm_plots_list_set_from_plots(
    900         farm_id,
    901         TEST_PUBKEY_HEX,
    902         [RadrootsPlot {
    903             d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
    904             farm: RadrootsFarmRef {
    905                 pubkey: TEST_PUBKEY_HEX.to_string(),
    906                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    907             },
    908             name: "plot".to_string(),
    909             about: None,
    910             location: None,
    911             tags: None,
    912         }]
    913         .iter(),
    914     )
    915     .unwrap();
    916     assert_eq!(plots_from.entries.len(), 1);
    917 
    918     let member_of_farms = member_of_farms_list_set([TEST_PUBKEY_HEX]).unwrap();
    919     assert_eq!(member_of_farms.d_tag, "member_of.farms");
    920 
    921     let err = farm_members_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
    922     assert!(matches!(
    923         err,
    924         EventEncodeError::EmptyRequiredField("farm_id")
    925     ));
    926     let err = farm_members_list_set(farm_id, [" "]).unwrap_err();
    927     assert!(matches!(
    928         err,
    929         EventEncodeError::EmptyRequiredField("entry.values")
    930     ));
    931     let err = farm_listings_list_set(farm_id, TEST_PUBKEY_HEX, [" "]).unwrap_err();
    932     assert!(matches!(
    933         err,
    934         EventEncodeError::EmptyRequiredField("listing_id")
    935     ));
    936 
    937     let coop_id = "AAAAAAAAAAAAAAAAAAAAAQ";
    938     let coop_members = coop_members_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
    939     assert_eq!(coop_members.d_tag, format!("coop:{coop_id}:members"));
    940     let coop_owners = coop_owners_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
    941     assert_eq!(coop_owners.d_tag, format!("coop:{coop_id}:members.owners"));
    942     let coop_admins = coop_admins_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
    943     assert_eq!(coop_admins.d_tag, format!("coop:{coop_id}:members.admins"));
    944     let coop_items = coop_items_list_set(coop_id, ["30340:pubkey:AAAAAAAAAAAAAAAAAAAAAA"]).unwrap();
    945     assert_eq!(coop_items.d_tag, format!("coop:{coop_id}:items"));
    946     let member_of_coops = member_of_coops_list_set([TEST_PUBKEY_HEX]).unwrap();
    947     assert_eq!(member_of_coops.d_tag, "member_of.coops");
    948 
    949     let coop_farms = coop_members_farms_list_set(
    950         coop_id,
    951         [RadrootsFarmRef {
    952             pubkey: TEST_PUBKEY_HEX.to_string(),
    953             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    954         }],
    955     )
    956     .unwrap();
    957     assert_eq!(coop_farms.entries.len(), 2);
    958 
    959     let err = coop_members_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
    960     assert!(matches!(
    961         err,
    962         EventEncodeError::EmptyRequiredField("coop_id")
    963     ));
    964     let err = coop_members_list_set(coop_id, [" "]).unwrap_err();
    965     assert!(matches!(
    966         err,
    967         EventEncodeError::EmptyRequiredField("entry.values")
    968     ));
    969     let err = coop_members_farms_list_set(
    970         coop_id,
    971         [RadrootsFarmRef {
    972             pubkey: "".to_string(),
    973             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    974         }],
    975     )
    976     .unwrap_err();
    977     assert!(matches!(
    978         err,
    979         EventEncodeError::EmptyRequiredField("farm.pubkey")
    980     ));
    981     let err = coop_members_farms_list_set(
    982         "invalid",
    983         [RadrootsFarmRef {
    984             pubkey: TEST_PUBKEY_HEX.to_string(),
    985             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    986         }],
    987     )
    988     .unwrap_err();
    989     assert!(matches!(err, EventEncodeError::InvalidField("coop_id")));
    990 
    991     let area_id = "AAAAAAAAAAAAAAAAAAAAAw";
    992     let resource_farms = resource_area_members_farms_list_set(
    993         area_id,
    994         [RadrootsFarmRef {
    995             pubkey: TEST_PUBKEY_HEX.to_string(),
    996             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    997         }],
    998     )
    999     .unwrap();
   1000     assert_eq!(resource_farms.entries.len(), 2);
   1001 
   1002     let resource_plots = resource_area_members_plots_list_set(
   1003         area_id,
   1004         [RadrootsPlotRef {
   1005             pubkey: TEST_PUBKEY_HEX.to_string(),
   1006             d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
   1007         }],
   1008     )
   1009     .unwrap();
   1010     assert_eq!(resource_plots.entries.len(), 2);
   1011 
   1012     let resource_stewards = resource_area_stewards_list_set(area_id, [TEST_PUBKEY_HEX]).unwrap();
   1013     assert_eq!(resource_stewards.entries.len(), 1);
   1014 
   1015     let err = resource_area_stewards_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
   1016     assert!(matches!(
   1017         err,
   1018         EventEncodeError::EmptyRequiredField("area_id")
   1019     ));
   1020     let err = resource_area_stewards_list_set(area_id, [" "]).unwrap_err();
   1021     assert!(matches!(
   1022         err,
   1023         EventEncodeError::EmptyRequiredField("entry.values")
   1024     ));
   1025     let err = resource_area_members_plots_list_set(
   1026         area_id,
   1027         [RadrootsPlotRef {
   1028             pubkey: "".to_string(),
   1029             d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
   1030         }],
   1031     )
   1032     .unwrap_err();
   1033     assert!(matches!(
   1034         err,
   1035         EventEncodeError::EmptyRequiredField("plot.pubkey")
   1036     ));
   1037 }
   1038 
   1039 #[test]
   1040 fn structured_list_set_outputs_remain_deterministic() {
   1041     let list_set: RadrootsListSet =
   1042         farm_members_list_set("AAAAAAAAAAAAAAAAAAAAAA", [TEST_PUBKEY_HEX, TEST_PUBKEY_HEX])
   1043             .unwrap();
   1044     assert_eq!(list_set.entries.len(), 2);
   1045     assert_eq!(list_set.entries[0].tag, "p");
   1046 }