lib

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

structured_decode.rs (30743B)


      1 #![cfg(feature = "serde_json")]
      2 
      3 #[path = "../src/test_fixtures.rs"]
      4 mod test_fixtures;
      5 
      6 use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit};
      7 use radroots_events::coop::RadrootsCoop;
      8 use radroots_events::document::{RadrootsDocument, RadrootsDocumentSubject};
      9 use radroots_events::farm::{
     10     RadrootsFarm, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint,
     11     RadrootsGeoJsonPolygon,
     12 };
     13 use radroots_events::kinds::{
     14     KIND_COOP, KIND_DOCUMENT, KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA, KIND_RESOURCE_HARVEST_CAP,
     15 };
     16 use radroots_events::plot::RadrootsPlot;
     17 use radroots_events::resource_area::{
     18     RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef,
     19 };
     20 use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
     21 use radroots_events::tags::TAG_D;
     22 use radroots_events_codec::coop::decode::{
     23     coop_from_event, data_from_event as coop_metadata_from_event,
     24     parsed_from_event as coop_index_from_event,
     25 };
     26 use radroots_events_codec::document::decode::{
     27     data_from_event as document_metadata_from_event, document_from_event,
     28     parsed_from_event as document_index_from_event,
     29 };
     30 use radroots_events_codec::error::EventParseError;
     31 use radroots_events_codec::farm::decode::{
     32     data_from_event as farm_metadata_from_event, farm_from_event,
     33     parsed_from_event as farm_index_from_event,
     34 };
     35 use radroots_events_codec::parsed::{RadrootsParsedData, RadrootsParsedEvent};
     36 use radroots_events_codec::plot::decode::{
     37     data_from_event as plot_metadata_from_event, parsed_from_event as plot_index_from_event,
     38     plot_from_event,
     39 };
     40 use radroots_events_codec::resource_area::decode::{
     41     data_from_event as resource_area_metadata_from_event,
     42     parsed_from_event as resource_area_index_from_event, resource_area_from_event,
     43 };
     44 use radroots_events_codec::resource_cap::decode::{
     45     data_from_event as resource_cap_metadata_from_event,
     46     parsed_from_event as resource_cap_index_from_event, resource_harvest_cap_from_event,
     47 };
     48 use test_fixtures::{FIXTURE_ALICE_NPUB, FIXTURE_ALICE_PUBLIC_KEY_HEX};
     49 
     50 const TEST_NPUB: &str = FIXTURE_ALICE_NPUB;
     51 const TEST_PUBKEY_HEX: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX;
     52 
     53 fn sample_gcs() -> RadrootsGcsLocation {
     54     RadrootsGcsLocation {
     55         lat: 37.0,
     56         lng: -122.0,
     57         geohash: "9q8yy".to_string(),
     58         point: RadrootsGeoJsonPoint {
     59             r#type: "Point".to_string(),
     60             coordinates: [-122.0, 37.0],
     61         },
     62         polygon: RadrootsGeoJsonPolygon {
     63             r#type: "Polygon".to_string(),
     64             coordinates: vec![vec![
     65                 [-122.0, 37.0],
     66                 [-122.0, 37.0001],
     67                 [-122.0001, 37.0001],
     68                 [-122.0, 37.0],
     69             ]],
     70         },
     71         accuracy: None,
     72         altitude: None,
     73         tag_0: None,
     74         label: None,
     75         area: None,
     76         elevation: None,
     77         soil: None,
     78         climate: None,
     79         gc_id: None,
     80         gc_name: None,
     81         gc_admin1_id: None,
     82         gc_admin1_name: None,
     83         gc_country_id: None,
     84         gc_country_name: None,
     85     }
     86 }
     87 
     88 fn sample_farm(d_tag: &str) -> RadrootsFarm {
     89     RadrootsFarm {
     90         d_tag: d_tag.to_string(),
     91         name: "Farm".to_string(),
     92         about: None,
     93         website: None,
     94         picture: None,
     95         banner: None,
     96         location: None,
     97         tags: None,
     98     }
     99 }
    100 
    101 fn sample_coop(d_tag: &str) -> RadrootsCoop {
    102     RadrootsCoop {
    103         d_tag: d_tag.to_string(),
    104         name: "Coop".to_string(),
    105         about: None,
    106         website: None,
    107         picture: None,
    108         banner: None,
    109         location: None,
    110         tags: None,
    111     }
    112 }
    113 
    114 fn sample_plot(d_tag: &str, farm_pubkey: &str, farm_d_tag: &str) -> RadrootsPlot {
    115     RadrootsPlot {
    116         d_tag: d_tag.to_string(),
    117         farm: RadrootsFarmRef {
    118             pubkey: farm_pubkey.to_string(),
    119             d_tag: farm_d_tag.to_string(),
    120         },
    121         name: "Plot".to_string(),
    122         about: None,
    123         location: None,
    124         tags: None,
    125     }
    126 }
    127 
    128 fn sample_document(
    129     d_tag: &str,
    130     subject_pubkey: &str,
    131     subject_address: Option<&str>,
    132 ) -> RadrootsDocument {
    133     RadrootsDocument {
    134         d_tag: d_tag.to_string(),
    135         doc_type: "charter".to_string(),
    136         title: "Charter".to_string(),
    137         version: "1.0.0".to_string(),
    138         summary: None,
    139         effective_at: None,
    140         body_markdown: None,
    141         subject: RadrootsDocumentSubject {
    142             pubkey: subject_pubkey.to_string(),
    143             address: subject_address.map(str::to_string),
    144         },
    145         tags: None,
    146     }
    147 }
    148 
    149 fn sample_resource_area(d_tag: &str) -> RadrootsResourceArea {
    150     RadrootsResourceArea {
    151         d_tag: d_tag.to_string(),
    152         name: "Area".to_string(),
    153         about: None,
    154         location: RadrootsResourceAreaLocation {
    155             primary: None,
    156             city: None,
    157             region: None,
    158             country: None,
    159             gcs: sample_gcs(),
    160         },
    161         tags: None,
    162     }
    163 }
    164 
    165 fn sample_resource_cap(d_tag: &str) -> RadrootsResourceHarvestCap {
    166     RadrootsResourceHarvestCap {
    167         d_tag: d_tag.to_string(),
    168         resource_area: RadrootsResourceAreaRef {
    169             pubkey: TEST_PUBKEY_HEX.to_string(),
    170             d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
    171         },
    172         product: RadrootsResourceHarvestProduct {
    173             key: "nutmeg".to_string(),
    174             category: Some("spice".to_string()),
    175         },
    176         start: 100,
    177         end: 200,
    178         cap_quantity: RadrootsCoreQuantity::new(
    179             RadrootsCoreDecimal::from(1000u32),
    180             RadrootsCoreUnit::MassG,
    181         ),
    182         display_amount: None,
    183         display_unit: None,
    184         display_label: None,
    185         tags: None,
    186     }
    187 }
    188 
    189 fn d_tag_tags(d_tag: &str) -> Vec<Vec<String>> {
    190     vec![vec![TAG_D.to_string(), d_tag.to_string()]]
    191 }
    192 
    193 #[test]
    194 fn farm_decode_handles_success_fill_and_error_paths() {
    195     let d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
    196     let tags = d_tag_tags(d_tag);
    197     let farm = sample_farm(d_tag);
    198     let content = serde_json::to_string(&farm).expect("farm content");
    199     let parsed = farm_from_event(KIND_FARM, &tags, &content).expect("farm parse");
    200     assert_eq!(parsed.d_tag, d_tag);
    201 
    202     let mut farm_missing = sample_farm("");
    203     let content_missing = serde_json::to_string(&farm_missing).expect("farm missing content");
    204     let filled = farm_from_event(KIND_FARM, &tags, &content_missing).expect("farm fill d");
    205     assert_eq!(filled.d_tag, d_tag);
    206 
    207     farm_missing.d_tag = "AAAAAAAAAAAAAAAAAAAAAQ".to_string();
    208     let mismatch_content = serde_json::to_string(&farm_missing).expect("farm mismatch content");
    209     let mismatch = farm_from_event(KIND_FARM, &tags, &mismatch_content).expect_err("mismatch");
    210     assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    211 
    212     let wrong_kind = farm_from_event(KIND_COOP, &tags, &content).expect_err("wrong kind");
    213     assert!(matches!(
    214         wrong_kind,
    215         EventParseError::InvalidKind {
    216             expected: "30340",
    217             got: KIND_COOP
    218         }
    219     ));
    220 
    221     let missing_d = farm_from_event(KIND_FARM, &[], &content).expect_err("missing d");
    222     assert!(matches!(missing_d, EventParseError::MissingTag("d")));
    223 
    224     let invalid_d = farm_from_event(
    225         KIND_FARM,
    226         &[vec![TAG_D.to_string(), "farm:invalid".to_string()]],
    227         &content,
    228     )
    229     .expect_err("invalid d");
    230     assert!(matches!(invalid_d, EventParseError::InvalidTag("d")));
    231 
    232     let invalid_json = farm_from_event(KIND_FARM, &tags, "").expect_err("invalid content");
    233     assert!(matches!(
    234         invalid_json,
    235         EventParseError::InvalidJson("content")
    236     ));
    237 
    238     let private_field_content = serde_json::json!({
    239         "d_tag": d_tag,
    240         "name": "Farm",
    241         "workspace": {"pubkey": TEST_PUBKEY_HEX}
    242     })
    243     .to_string();
    244     let private_field =
    245         farm_from_event(KIND_FARM, &tags, &private_field_content).expect_err("private field");
    246     assert!(matches!(
    247         private_field,
    248         EventParseError::InvalidJson("content")
    249     ));
    250 
    251     let non_object = farm_from_event(KIND_FARM, &tags, "[]").expect_err("non object");
    252     assert!(matches!(
    253         non_object,
    254         EventParseError::InvalidJson("content")
    255     ));
    256 }
    257 
    258 #[test]
    259 fn farm_metadata_and_index_decode_roundtrip() {
    260     let d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
    261     let content = serde_json::to_string(&sample_farm(d_tag)).expect("farm content");
    262     let tags = d_tag_tags(d_tag);
    263     let metadata: RadrootsParsedData<RadrootsFarm> = farm_metadata_from_event(
    264         "id1".to_string(),
    265         TEST_PUBKEY_HEX.to_string(),
    266         55,
    267         KIND_FARM,
    268         content.clone(),
    269         tags.clone(),
    270     )
    271     .expect("farm metadata");
    272     assert_eq!(metadata.id, "id1");
    273     assert_eq!(metadata.data.d_tag, d_tag);
    274 
    275     let index: RadrootsParsedEvent<RadrootsFarm> = farm_index_from_event(
    276         "id1".to_string(),
    277         TEST_PUBKEY_HEX.to_string(),
    278         55,
    279         KIND_FARM,
    280         content,
    281         tags,
    282         "sig1".to_string(),
    283     )
    284     .expect("farm index");
    285     assert_eq!(index.event.id, "id1");
    286     assert_eq!(index.data.data.d_tag, d_tag);
    287 }
    288 
    289 #[test]
    290 fn coop_decode_handles_success_fill_and_error_paths() {
    291     let d_tag = "BAAAAAAAAAAAAAAAAAAAAA";
    292     let tags = d_tag_tags(d_tag);
    293     let coop = sample_coop(d_tag);
    294     let content = serde_json::to_string(&coop).expect("coop content");
    295     let parsed = coop_from_event(KIND_COOP, &tags, &content).expect("coop parse");
    296     assert_eq!(parsed.d_tag, d_tag);
    297 
    298     let content_missing = serde_json::to_string(&sample_coop("")).expect("coop missing content");
    299     let filled = coop_from_event(KIND_COOP, &tags, &content_missing).expect("coop fill d");
    300     assert_eq!(filled.d_tag, d_tag);
    301 
    302     let mismatch_content =
    303         serde_json::to_string(&sample_coop("AAAAAAAAAAAAAAAAAAAAAQ")).expect("coop mismatch");
    304     let mismatch = coop_from_event(KIND_COOP, &tags, &mismatch_content).expect_err("mismatch");
    305     assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    306 
    307     let wrong_kind = coop_from_event(KIND_FARM, &tags, &content).expect_err("wrong kind");
    308     assert!(matches!(
    309         wrong_kind,
    310         EventParseError::InvalidKind {
    311             expected: "30360",
    312             got: KIND_FARM
    313         }
    314     ));
    315 
    316     let missing_d = coop_from_event(KIND_COOP, &[], &content).expect_err("missing d");
    317     assert!(matches!(missing_d, EventParseError::MissingTag("d")));
    318 }
    319 
    320 #[test]
    321 fn coop_metadata_and_index_decode_roundtrip() {
    322     let d_tag = "BAAAAAAAAAAAAAAAAAAAAA";
    323     let content = serde_json::to_string(&sample_coop(d_tag)).expect("coop content");
    324     let tags = d_tag_tags(d_tag);
    325     let metadata: RadrootsParsedData<RadrootsCoop> = coop_metadata_from_event(
    326         "id2".to_string(),
    327         TEST_PUBKEY_HEX.to_string(),
    328         56,
    329         KIND_COOP,
    330         content.clone(),
    331         tags.clone(),
    332     )
    333     .expect("coop metadata");
    334     assert_eq!(metadata.id, "id2");
    335     assert_eq!(metadata.data.d_tag, d_tag);
    336 
    337     let index: RadrootsParsedEvent<RadrootsCoop> = coop_index_from_event(
    338         "id2".to_string(),
    339         TEST_PUBKEY_HEX.to_string(),
    340         56,
    341         KIND_COOP,
    342         content,
    343         tags,
    344         "sig2".to_string(),
    345     )
    346     .expect("coop index");
    347     assert_eq!(index.event.kind, KIND_COOP);
    348     assert_eq!(index.data.data.d_tag, d_tag);
    349 }
    350 
    351 #[test]
    352 fn plot_decode_handles_success_fill_and_tag_error_paths() {
    353     let d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
    354     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
    355     let tags = vec![
    356         vec![TAG_D.to_string(), d_tag.to_string()],
    357         vec![
    358             "a".to_string(),
    359             format!("30340:{TEST_PUBKEY_HEX}:{farm_d_tag}"),
    360         ],
    361         vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    362     ];
    363 
    364     let content = serde_json::to_string(&sample_plot(d_tag, TEST_PUBKEY_HEX, farm_d_tag))
    365         .expect("plot content");
    366     let parsed = plot_from_event(KIND_PLOT, &tags, &content).expect("plot parse");
    367     assert_eq!(parsed.farm.pubkey, TEST_PUBKEY_HEX);
    368 
    369     let filled_content =
    370         serde_json::to_string(&sample_plot("", "", "")).expect("plot missing content");
    371     let filled = plot_from_event(KIND_PLOT, &tags, &filled_content).expect("plot fill");
    372     assert_eq!(filled.d_tag, d_tag);
    373     assert_eq!(filled.farm.d_tag, farm_d_tag);
    374 
    375     let partial_farm_content =
    376         serde_json::to_string(&sample_plot(d_tag, TEST_PUBKEY_HEX, "")).expect("plot partial farm");
    377     let partial_farm =
    378         plot_from_event(KIND_PLOT, &tags, &partial_farm_content).expect("partial farm fill");
    379     assert_eq!(partial_farm.farm.d_tag, farm_d_tag);
    380 
    381     let bad_a = plot_from_event(
    382         KIND_PLOT,
    383         &[
    384             vec![TAG_D.to_string(), d_tag.to_string()],
    385             vec![
    386                 "a".to_string(),
    387                 format!("30361:{TEST_PUBKEY_HEX}:AAAAAAAAAAAAAAAAAAAAAA"),
    388             ],
    389             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    390         ],
    391         &content,
    392     )
    393     .expect_err("bad a");
    394     assert!(matches!(bad_a, EventParseError::InvalidTag("a")));
    395 
    396     let bad_p = plot_from_event(
    397         KIND_PLOT,
    398         &[
    399             vec![TAG_D.to_string(), d_tag.to_string()],
    400             vec![
    401                 "a".to_string(),
    402                 format!("30340:{TEST_PUBKEY_HEX}:{farm_d_tag}"),
    403             ],
    404             vec!["p".to_string(), TEST_NPUB.to_string()],
    405         ],
    406         &content,
    407     )
    408     .expect_err("bad p");
    409     assert!(matches!(bad_p, EventParseError::InvalidTag("p")));
    410 
    411     let missing_a = plot_from_event(
    412         KIND_PLOT,
    413         &[
    414             vec![TAG_D.to_string(), d_tag.to_string()],
    415             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    416         ],
    417         &content,
    418     )
    419     .expect_err("missing a");
    420     assert!(matches!(missing_a, EventParseError::MissingTag("a")));
    421 
    422     let invalid_kind = plot_from_event(KIND_FARM, &tags, &content).expect_err("invalid kind");
    423     assert!(matches!(
    424         invalid_kind,
    425         EventParseError::InvalidKind {
    426             expected: "30350",
    427             got: KIND_FARM
    428         }
    429     ));
    430 
    431     let blank_content = plot_from_event(KIND_PLOT, &tags, " ").expect_err("blank content");
    432     assert!(matches!(
    433         blank_content,
    434         EventParseError::InvalidJson("content")
    435     ));
    436 
    437     let empty_d = plot_from_event(
    438         KIND_PLOT,
    439         &[
    440             vec![TAG_D.to_string(), " ".to_string()],
    441             vec![
    442                 "a".to_string(),
    443                 format!("30340:{TEST_PUBKEY_HEX}:{farm_d_tag}"),
    444             ],
    445             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    446         ],
    447         &content,
    448     )
    449     .expect_err("empty d");
    450     assert!(matches!(empty_d, EventParseError::InvalidTag("d")));
    451 
    452     let empty_a_pubkey = plot_from_event(
    453         KIND_PLOT,
    454         &[
    455             vec![TAG_D.to_string(), d_tag.to_string()],
    456             vec!["a".to_string(), format!("30340::{farm_d_tag}")],
    457             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    458         ],
    459         &content,
    460     )
    461     .expect_err("empty a pubkey");
    462     assert!(matches!(empty_a_pubkey, EventParseError::InvalidTag("a")));
    463 
    464     let empty_a_d_tag = plot_from_event(
    465         KIND_PLOT,
    466         &[
    467             vec![TAG_D.to_string(), d_tag.to_string()],
    468             vec!["a".to_string(), format!("30340:{TEST_PUBKEY_HEX}:")],
    469             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    470         ],
    471         &content,
    472     )
    473     .expect_err("empty a d_tag");
    474     assert!(matches!(empty_a_d_tag, EventParseError::InvalidTag("a")));
    475 
    476     let empty_p = plot_from_event(
    477         KIND_PLOT,
    478         &[
    479             vec![TAG_D.to_string(), d_tag.to_string()],
    480             vec![
    481                 "a".to_string(),
    482                 format!("30340:{TEST_PUBKEY_HEX}:{farm_d_tag}"),
    483             ],
    484             vec!["p".to_string(), " ".to_string()],
    485         ],
    486         &content,
    487     )
    488     .expect_err("empty p");
    489     assert!(matches!(empty_p, EventParseError::InvalidTag("p")));
    490 
    491     let mismatched_d_content = serde_json::to_string(&sample_plot(
    492         "BAAAAAAAAAAAAAAAAAAAAA",
    493         TEST_PUBKEY_HEX,
    494         farm_d_tag,
    495     ))
    496     .expect("mismatched d content");
    497     let mismatched_d =
    498         plot_from_event(KIND_PLOT, &tags, &mismatched_d_content).expect_err("mismatched d");
    499     assert!(matches!(mismatched_d, EventParseError::InvalidTag("d")));
    500 
    501     let mismatched_farm_pubkey_content =
    502         serde_json::to_string(&sample_plot(d_tag, TEST_NPUB, farm_d_tag))
    503             .expect("mismatched farm pubkey content");
    504     let mismatched_farm_pubkey = plot_from_event(KIND_PLOT, &tags, &mismatched_farm_pubkey_content)
    505         .expect_err("mismatched farm pubkey");
    506     assert!(matches!(
    507         mismatched_farm_pubkey,
    508         EventParseError::InvalidTag("a")
    509     ));
    510 
    511     let mismatched_farm_d_tag_content = serde_json::to_string(&sample_plot(
    512         d_tag,
    513         TEST_PUBKEY_HEX,
    514         "BAAAAAAAAAAAAAAAAAAAAA",
    515     ))
    516     .expect("mismatched farm d_tag content");
    517     let mismatched_farm_d_tag = plot_from_event(KIND_PLOT, &tags, &mismatched_farm_d_tag_content)
    518         .expect_err("mismatched farm d_tag");
    519     assert!(matches!(
    520         mismatched_farm_d_tag,
    521         EventParseError::InvalidTag("a")
    522     ));
    523 
    524     let parsed_err = plot_index_from_event(
    525         "id".to_string(),
    526         TEST_PUBKEY_HEX.to_string(),
    527         57,
    528         KIND_FARM,
    529         content,
    530         tags,
    531         "sig".to_string(),
    532     )
    533     .expect_err("parsed error");
    534     assert!(matches!(
    535         parsed_err,
    536         EventParseError::InvalidKind {
    537             expected: "30350",
    538             got: KIND_FARM
    539         }
    540     ));
    541 }
    542 
    543 #[test]
    544 fn plot_metadata_and_index_decode_roundtrip() {
    545     let d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
    546     let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
    547     let tags = vec![
    548         vec![TAG_D.to_string(), d_tag.to_string()],
    549         vec![
    550             "a".to_string(),
    551             format!("30340:{TEST_PUBKEY_HEX}:{farm_d_tag}"),
    552         ],
    553         vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    554     ];
    555     let content = serde_json::to_string(&sample_plot(d_tag, TEST_PUBKEY_HEX, farm_d_tag))
    556         .expect("plot content");
    557 
    558     let metadata: RadrootsParsedData<RadrootsPlot> = plot_metadata_from_event(
    559         "id3".to_string(),
    560         TEST_PUBKEY_HEX.to_string(),
    561         57,
    562         KIND_PLOT,
    563         content.clone(),
    564         tags.clone(),
    565     )
    566     .expect("plot metadata");
    567     assert_eq!(metadata.data.d_tag, d_tag);
    568 
    569     let index: RadrootsParsedEvent<RadrootsPlot> = plot_index_from_event(
    570         "id3".to_string(),
    571         TEST_PUBKEY_HEX.to_string(),
    572         57,
    573         KIND_PLOT,
    574         content,
    575         tags,
    576         "sig3".to_string(),
    577     )
    578     .expect("plot index");
    579     assert_eq!(index.event.author, TEST_PUBKEY_HEX);
    580     assert_eq!(index.data.data.d_tag, d_tag);
    581 }
    582 
    583 #[test]
    584 fn document_decode_handles_subject_and_address_paths() {
    585     let d_tag = "EAAAAAAAAAAAAAAAAAAAAA";
    586     let tag_address = format!("30360:{TEST_PUBKEY_HEX}:BAAAAAAAAAAAAAAAAAAAAA");
    587     let tags = vec![
    588         vec![TAG_D.to_string(), d_tag.to_string()],
    589         vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    590         vec!["a".to_string(), tag_address.clone()],
    591     ];
    592     let content =
    593         serde_json::to_string(&sample_document(d_tag, TEST_PUBKEY_HEX, Some(&tag_address)))
    594             .expect("document content");
    595     let parsed = document_from_event(KIND_DOCUMENT, &tags, &content).expect("document parse");
    596     assert_eq!(parsed.subject.pubkey, TEST_PUBKEY_HEX);
    597     assert_eq!(
    598         parsed.subject.address.as_deref(),
    599         Some(tag_address.as_str())
    600     );
    601 
    602     let fill_content = serde_json::to_string(&sample_document("", "", None)).expect("fill");
    603     let filled = document_from_event(KIND_DOCUMENT, &tags, &fill_content).expect("document fill");
    604     assert_eq!(filled.d_tag, d_tag);
    605     assert_eq!(filled.subject.pubkey, TEST_PUBKEY_HEX);
    606     assert_eq!(
    607         filled.subject.address.as_deref(),
    608         Some(tag_address.as_str())
    609     );
    610 
    611     let missing_a_err = document_from_event(
    612         KIND_DOCUMENT,
    613         &[
    614             vec![TAG_D.to_string(), d_tag.to_string()],
    615             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    616         ],
    617         &content,
    618     )
    619     .expect_err("missing a");
    620     assert!(matches!(missing_a_err, EventParseError::MissingTag("a")));
    621 
    622     let mismatch_p_content =
    623         serde_json::to_string(&sample_document(d_tag, TEST_NPUB, Some(&tag_address)))
    624             .expect("mismatch p content");
    625     let mismatch_p =
    626         document_from_event(KIND_DOCUMENT, &tags, &mismatch_p_content).expect_err("mismatch p");
    627     assert!(matches!(mismatch_p, EventParseError::InvalidTag("p")));
    628 
    629     let empty_a_content =
    630         serde_json::to_string(&sample_document(d_tag, TEST_PUBKEY_HEX, Some(""))).expect("empty a");
    631     let empty_a =
    632         document_from_event(KIND_DOCUMENT, &tags, &empty_a_content).expect_err("empty address");
    633     assert!(matches!(empty_a, EventParseError::InvalidTag("a")));
    634 
    635     let invalid_kind = document_from_event(KIND_FARM, &tags, &content).expect_err("invalid kind");
    636     assert!(matches!(
    637         invalid_kind,
    638         EventParseError::InvalidKind {
    639             expected: "30361",
    640             got: KIND_FARM
    641         }
    642     ));
    643 
    644     let blank_content = document_from_event(KIND_DOCUMENT, &tags, " ").expect_err("blank content");
    645     assert!(matches!(
    646         blank_content,
    647         EventParseError::InvalidJson("content")
    648     ));
    649 
    650     let empty_d_tag = document_from_event(
    651         KIND_DOCUMENT,
    652         &[
    653             vec![TAG_D.to_string(), " ".to_string()],
    654             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    655             vec!["a".to_string(), tag_address.clone()],
    656         ],
    657         &content,
    658     )
    659     .expect_err("empty d");
    660     assert!(matches!(empty_d_tag, EventParseError::InvalidTag("d")));
    661 
    662     let empty_p_tag = document_from_event(
    663         KIND_DOCUMENT,
    664         &[
    665             vec![TAG_D.to_string(), d_tag.to_string()],
    666             vec!["p".to_string(), " ".to_string()],
    667             vec!["a".to_string(), tag_address.clone()],
    668         ],
    669         &content,
    670     )
    671     .expect_err("empty p");
    672     assert!(matches!(empty_p_tag, EventParseError::InvalidTag("p")));
    673 
    674     let empty_a_tag = document_from_event(
    675         KIND_DOCUMENT,
    676         &[
    677             vec![TAG_D.to_string(), d_tag.to_string()],
    678             vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    679             vec!["a".to_string(), " ".to_string()],
    680         ],
    681         &content,
    682     )
    683     .expect_err("empty a tag");
    684     assert!(matches!(empty_a_tag, EventParseError::InvalidTag("a")));
    685 
    686     let mismatched_d_content = serde_json::to_string(&sample_document(
    687         "FAAAAAAAAAAAAAAAAAAAAA",
    688         TEST_PUBKEY_HEX,
    689         None,
    690     ))
    691     .expect("mismatched d content");
    692     let mismatched_d =
    693         document_from_event(KIND_DOCUMENT, &tags, &mismatched_d_content).expect_err("mismatched d");
    694     assert!(matches!(mismatched_d, EventParseError::InvalidTag("d")));
    695 
    696     let mismatched_a_content = serde_json::to_string(&sample_document(
    697         d_tag,
    698         TEST_PUBKEY_HEX,
    699         Some("30360:author:other"),
    700     ))
    701     .expect("mismatched address content");
    702     let mismatched_a =
    703         document_from_event(KIND_DOCUMENT, &tags, &mismatched_a_content).expect_err("mismatched a");
    704     assert!(matches!(mismatched_a, EventParseError::InvalidTag("a")));
    705 
    706     let parsed_err = document_index_from_event(
    707         "id".to_string(),
    708         TEST_PUBKEY_HEX.to_string(),
    709         58,
    710         KIND_FARM,
    711         content,
    712         tags,
    713         "sig".to_string(),
    714     )
    715     .expect_err("parsed error");
    716     assert!(matches!(
    717         parsed_err,
    718         EventParseError::InvalidKind {
    719             expected: "30361",
    720             got: KIND_FARM
    721         }
    722     ));
    723 }
    724 
    725 #[test]
    726 fn document_metadata_and_index_decode_roundtrip() {
    727     let d_tag = "EAAAAAAAAAAAAAAAAAAAAA";
    728     let tag_address = format!("30360:{TEST_PUBKEY_HEX}:BAAAAAAAAAAAAAAAAAAAAA");
    729     let tags = vec![
    730         vec![TAG_D.to_string(), d_tag.to_string()],
    731         vec!["p".to_string(), TEST_PUBKEY_HEX.to_string()],
    732         vec!["a".to_string(), tag_address.clone()],
    733     ];
    734     let content =
    735         serde_json::to_string(&sample_document(d_tag, TEST_PUBKEY_HEX, Some(&tag_address)))
    736             .expect("document content");
    737 
    738     let metadata: RadrootsParsedData<RadrootsDocument> = document_metadata_from_event(
    739         "id4".to_string(),
    740         TEST_PUBKEY_HEX.to_string(),
    741         58,
    742         KIND_DOCUMENT,
    743         content.clone(),
    744         tags.clone(),
    745     )
    746     .expect("document metadata");
    747     assert_eq!(metadata.data.d_tag, d_tag);
    748 
    749     let index: RadrootsParsedEvent<RadrootsDocument> = document_index_from_event(
    750         "id4".to_string(),
    751         TEST_PUBKEY_HEX.to_string(),
    752         58,
    753         KIND_DOCUMENT,
    754         content,
    755         tags,
    756         "sig4".to_string(),
    757     )
    758     .expect("document index");
    759     assert_eq!(index.event.kind, KIND_DOCUMENT);
    760     assert_eq!(index.data.data.d_tag, d_tag);
    761 }
    762 
    763 #[test]
    764 fn resource_area_decode_handles_success_fill_and_errors() {
    765     let d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
    766     let tags = d_tag_tags(d_tag);
    767     let area = sample_resource_area(d_tag);
    768     let content = serde_json::to_string(&area).expect("area content");
    769     let parsed = resource_area_from_event(KIND_RESOURCE_AREA, &tags, &content).expect("area");
    770     assert_eq!(parsed.d_tag, d_tag);
    771 
    772     let fill_content = serde_json::to_string(&sample_resource_area("")).expect("area fill");
    773     let filled =
    774         resource_area_from_event(KIND_RESOURCE_AREA, &tags, &fill_content).expect("area fill");
    775     assert_eq!(filled.d_tag, d_tag);
    776 
    777     let mismatch_content =
    778         serde_json::to_string(&sample_resource_area("AAAAAAAAAAAAAAAAAAAAAQ")).expect("mismatch");
    779     let mismatch =
    780         resource_area_from_event(KIND_RESOURCE_AREA, &tags, &mismatch_content).expect_err("m");
    781     assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    782 
    783     let wrong_kind = resource_area_from_event(KIND_FARM, &tags, &content).expect_err("wrong kind");
    784     assert!(matches!(
    785         wrong_kind,
    786         EventParseError::InvalidKind {
    787             expected: "30370",
    788             got: KIND_FARM
    789         }
    790     ));
    791 
    792     let blank_content =
    793         resource_area_from_event(KIND_RESOURCE_AREA, &tags, " ").expect_err("blank content");
    794     assert!(matches!(
    795         blank_content,
    796         EventParseError::InvalidJson("content")
    797     ));
    798 
    799     let missing_d =
    800         resource_area_from_event(KIND_RESOURCE_AREA, &[], &content).expect_err("missing d");
    801     assert!(matches!(missing_d, EventParseError::MissingTag("d")));
    802 
    803     let invalid_d = resource_area_from_event(
    804         KIND_RESOURCE_AREA,
    805         &[vec![TAG_D.to_string(), " ".to_string()]],
    806         &content,
    807     )
    808     .expect_err("invalid d");
    809     assert!(matches!(invalid_d, EventParseError::InvalidTag("d")));
    810 }
    811 
    812 #[test]
    813 fn resource_area_metadata_and_index_decode_roundtrip() {
    814     let d_tag = "AAAAAAAAAAAAAAAAAAAAAw";
    815     let content = serde_json::to_string(&sample_resource_area(d_tag)).expect("area content");
    816     let tags = d_tag_tags(d_tag);
    817     let metadata: RadrootsParsedData<RadrootsResourceArea> = resource_area_metadata_from_event(
    818         "id5".to_string(),
    819         TEST_PUBKEY_HEX.to_string(),
    820         59,
    821         KIND_RESOURCE_AREA,
    822         content.clone(),
    823         tags.clone(),
    824     )
    825     .expect("area metadata");
    826     assert_eq!(metadata.data.d_tag, d_tag);
    827 
    828     let index: RadrootsParsedEvent<RadrootsResourceArea> = resource_area_index_from_event(
    829         "id5".to_string(),
    830         TEST_PUBKEY_HEX.to_string(),
    831         59,
    832         KIND_RESOURCE_AREA,
    833         content,
    834         tags,
    835         "sig5".to_string(),
    836     )
    837     .expect("area index");
    838     assert_eq!(index.event.id, "id5");
    839     assert_eq!(index.data.data.d_tag, d_tag);
    840 }
    841 
    842 #[test]
    843 fn resource_cap_decode_handles_success_fill_and_errors() {
    844     let d_tag = "DAAAAAAAAAAAAAAAAAAAAA";
    845     let tags = d_tag_tags(d_tag);
    846     let cap = sample_resource_cap(d_tag);
    847     let content = serde_json::to_string(&cap).expect("cap content");
    848     let parsed = resource_harvest_cap_from_event(KIND_RESOURCE_HARVEST_CAP, &tags, &content)
    849         .expect("cap parse");
    850     assert_eq!(parsed.d_tag, d_tag);
    851 
    852     let fill_content = serde_json::to_string(&sample_resource_cap("")).expect("cap fill");
    853     let filled = resource_harvest_cap_from_event(KIND_RESOURCE_HARVEST_CAP, &tags, &fill_content)
    854         .expect("cap fill parse");
    855     assert_eq!(filled.d_tag, d_tag);
    856 
    857     let mismatch_content =
    858         serde_json::to_string(&sample_resource_cap("AAAAAAAAAAAAAAAAAAAAAQ")).expect("mismatch");
    859     let mismatch =
    860         resource_harvest_cap_from_event(KIND_RESOURCE_HARVEST_CAP, &tags, &mismatch_content)
    861             .expect_err("cap mismatch");
    862     assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    863 
    864     let wrong_kind =
    865         resource_harvest_cap_from_event(KIND_FARM, &tags, &content).expect_err("wrong kind");
    866     assert!(matches!(
    867         wrong_kind,
    868         EventParseError::InvalidKind {
    869             expected: "30371",
    870             got: KIND_FARM
    871         }
    872     ));
    873 
    874     let blank_content = resource_harvest_cap_from_event(KIND_RESOURCE_HARVEST_CAP, &tags, " ")
    875         .expect_err("blank content");
    876     assert!(matches!(
    877         blank_content,
    878         EventParseError::InvalidJson("content")
    879     ));
    880 
    881     let missing_d = resource_harvest_cap_from_event(KIND_RESOURCE_HARVEST_CAP, &[], &content)
    882         .expect_err("missing d");
    883     assert!(matches!(missing_d, EventParseError::MissingTag("d")));
    884 
    885     let invalid_d = resource_harvest_cap_from_event(
    886         KIND_RESOURCE_HARVEST_CAP,
    887         &[vec![TAG_D.to_string(), " ".to_string()]],
    888         &content,
    889     )
    890     .expect_err("invalid d");
    891     assert!(matches!(invalid_d, EventParseError::InvalidTag("d")));
    892 }
    893 
    894 #[test]
    895 fn resource_cap_metadata_and_index_decode_roundtrip() {
    896     let d_tag = "DAAAAAAAAAAAAAAAAAAAAA";
    897     let content = serde_json::to_string(&sample_resource_cap(d_tag)).expect("cap content");
    898     let tags = d_tag_tags(d_tag);
    899     let metadata: RadrootsParsedData<RadrootsResourceHarvestCap> =
    900         resource_cap_metadata_from_event(
    901             "id6".to_string(),
    902             TEST_PUBKEY_HEX.to_string(),
    903             60,
    904             KIND_RESOURCE_HARVEST_CAP,
    905             content.clone(),
    906             tags.clone(),
    907         )
    908         .expect("cap metadata");
    909     assert_eq!(metadata.data.d_tag, d_tag);
    910 
    911     let index: RadrootsParsedEvent<RadrootsResourceHarvestCap> = resource_cap_index_from_event(
    912         "id6".to_string(),
    913         TEST_PUBKEY_HEX.to_string(),
    914         60,
    915         KIND_RESOURCE_HARVEST_CAP,
    916         content,
    917         tags,
    918         "sig6".to_string(),
    919     )
    920     .expect("cap index");
    921     assert_eq!(index.event.sig, "sig6");
    922     assert_eq!(index.data.data.d_tag, d_tag);
    923 }