lib

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

structured_decode.rs (30411B)


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