lib

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

structured_encode_default.rs (35489B)


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