lib

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

verified_post_conformance.rs (29880B)


      1 #![cfg(feature = "json")]
      2 
      3 use std::{borrow::Cow, collections::BTreeMap, fs, path::Path};
      4 
      5 use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256};
      6 use radroots_event::{
      7     contract::identify_event_contract,
      8     envelope::EventEnvelope,
      9     media::AuthoredImage,
     10     post::reply::{AuthoredNip10Reply, Nip10ReplyError, Nip10ReplyReference},
     11     post::{
     12         AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostError, AuthoredPostImage, AuthoredUpdate,
     13         PostImageDimensions,
     14     },
     15     wire::Nip01EventWire,
     16     wire::Nip01EventWireParts,
     17 };
     18 use radroots_event_codec::admission::post::{
     19     RadrootsPostAdmissionOutcome, verify_and_admit_post_event,
     20 };
     21 use radroots_event_codec::admission::reply::verify_and_admit_nip10_reply_event;
     22 use radroots_event_codec::decode::post::{
     23     RadrootsInboundPostProjection, RadrootsPostClassification, RadrootsPostDiagnostic,
     24     project_verified_post_event,
     25 };
     26 use radroots_event_codec::decode::reply::{
     27     RadrootsInboundNip10EventReference, RadrootsInboundNip10Participant,
     28     RadrootsInboundNip10ReplyProjection, RadrootsNip10ReplyDiagnostic, RadrootsNip10ReplyStyle,
     29     project_verified_nip10_reply_event,
     30 };
     31 use radroots_event_codec::encode::post::{
     32     authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts,
     33 };
     34 use radroots_event_codec::encode::reply::authored_nip10_reply_to_wire_parts;
     35 use radroots_event_codec::verify::verify_nip01_event;
     36 use serde::Deserialize;
     37 use serde_json::{Value, json};
     38 
     39 const PACKAGED_VECTORS: &str = include_str!("fixtures/post_verified_profiles.v1.json");
     40 const WORKSPACE_VECTOR_PATH: &str =
     41     "../../contracts/conformance/vectors/post/verified_profiles.v1.json";
     42 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
     43 const EXPECTED_VECTOR_CASES: [(&str, &str); 64] = [
     44     (
     45         "admit_signed_duplicate_normalized_ask_marker",
     46         "social.post.verify_and_admit_event.invalid",
     47     ),
     48     (
     49         "admit_signed_empty_e_thread_candidate",
     50         "social.post.verify_and_admit_event.valid",
     51     ),
     52     (
     53         "admit_signed_empty_e_value_thread_candidate",
     54         "social.post.verify_and_admit_event.valid",
     55     ),
     56     (
     57         "admit_signed_invalid_signature",
     58         "social.post.verify_and_admit_event.invalid",
     59     ),
     60     (
     61         "admit_signed_kind_20_is_not_photo_update",
     62         "social.post.verify_and_admit_event.invalid",
     63     ),
     64     (
     65         "admit_signed_nip10_invalid_signature",
     66         "social.reply.verify_and_admit_event.invalid",
     67     ),
     68     (
     69         "admit_signed_nip10_marked_direct",
     70         "social.reply.verify_and_admit_event.valid",
     71     ),
     72     (
     73         "admit_signed_nip10_positional_direct",
     74         "social.reply.verify_and_admit_event.valid",
     75     ),
     76     (
     77         "admit_signed_normalized_ask_precedes_malformed_media",
     78         "social.post.verify_and_admit_event.valid",
     79     ),
     80     (
     81         "admit_signed_structural_photo",
     82         "social.post.verify_and_admit_event.valid",
     83     ),
     84     (
     85         "admit_signed_thread_candidate_precedes_ask_and_media",
     86         "social.post.verify_and_admit_event.valid",
     87     ),
     88     (
     89         "admit_signed_update",
     90         "social.post.verify_and_admit_event.valid",
     91     ),
     92     (
     93         "authored_ask_blank",
     94         "social.ask.build_authored_draft.invalid",
     95     ),
     96     ("authored_ask_wire", "social.ask.build_authored_draft.valid"),
     97     (
     98         "authored_nip10_ambiguous_parent",
     99         "social.reply.build_authored_draft.invalid",
    100     ),
    101     (
    102         "authored_nip10_canonical_ipv6_relay",
    103         "social.reply.build_authored_draft.valid",
    104     ),
    105     (
    106         "authored_nip10_direct_wire",
    107         "social.reply.build_authored_draft.valid",
    108     ),
    109     (
    110         "authored_nip10_invalid_relay_bad_percent_host",
    111         "social.reply.build_authored_draft.invalid",
    112     ),
    113     (
    114         "authored_nip10_invalid_relay_empty_port",
    115         "social.reply.build_authored_draft.invalid",
    116     ),
    117     (
    118         "authored_nip10_invalid_relay_ipv4_overflow",
    119         "social.reply.build_authored_draft.invalid",
    120     ),
    121     (
    122         "authored_nip10_invalid_relay_ipvfuture",
    123         "social.reply.build_authored_draft.invalid",
    124     ),
    125     (
    126         "authored_nip10_invalid_relay_percent_host",
    127         "social.reply.build_authored_draft.invalid",
    128     ),
    129     (
    130         "authored_nip10_invalid_relay_port_overflow",
    131         "social.reply.build_authored_draft.invalid",
    132     ),
    133     (
    134         "authored_nip10_invalid_relay_zero_port",
    135         "social.reply.build_authored_draft.invalid",
    136     ),
    137     (
    138         "authored_nip10_invalid_event_id",
    139         "social.reply.build_authored_draft.invalid",
    140     ),
    141     (
    142         "authored_nip10_nested_wire",
    143         "social.reply.build_authored_draft.valid",
    144     ),
    145     (
    146         "authored_photo_update_mime_underscore",
    147         "social.photo_update.build_authored_draft.invalid",
    148     ),
    149     (
    150         "authored_photo_update_wire",
    151         "social.photo_update.build_authored_draft.valid",
    152     ),
    153     (
    154         "authored_update_blank",
    155         "social.update.build_authored_draft.invalid",
    156     ),
    157     (
    158         "authored_update_wire",
    159         "social.update.build_authored_draft.valid",
    160     ),
    161     (
    162         "project_signed_duplicate_normalized_ask_marker",
    163         "social.post.project_verified_event.invalid",
    164     ),
    165     (
    166         "project_signed_duplicate_singleton_is_update",
    167         "social.post.project_verified_event.valid",
    168     ),
    169     (
    170         "project_signed_empty_inbound_update",
    171         "social.post.project_verified_event.valid",
    172     ),
    173     (
    174         "project_signed_kind_20_is_not_photo_update",
    175         "social.post.project_verified_event.invalid",
    176     ),
    177     (
    178         "project_signed_malformed_ask_marker_is_update",
    179         "social.post.project_verified_event.valid",
    180     ),
    181     (
    182         "project_signed_malformed_imeta_is_update",
    183         "social.post.project_verified_event.valid",
    184     ),
    185     (
    186         "project_signed_mixed_imeta_is_update",
    187         "social.post.project_verified_event.valid",
    188     ),
    189     (
    190         "project_signed_nip10_ambiguous_same_reference",
    191         "social.reply.project_verified_event.invalid",
    192     ),
    193     (
    194         "project_signed_nip10_author_hint_mismatch",
    195         "social.reply.project_verified_event.valid",
    196     ),
    197     (
    198         "project_signed_nip10_blank_content",
    199         "social.reply.project_verified_event.valid",
    200     ),
    201     (
    202         "project_signed_nip10_invalid_event_id",
    203         "social.reply.project_verified_event.invalid",
    204     ),
    205     (
    206         "project_signed_nip10_invalid_relay",
    207         "social.reply.project_verified_event.valid",
    208     ),
    209     (
    210         "project_signed_nip10_canonical_relay_authorities",
    211         "social.reply.project_verified_event.valid",
    212     ),
    213     (
    214         "project_signed_nip10_malformed_relay_authorities",
    215         "social.reply.project_verified_event.valid",
    216     ),
    217     (
    218         "project_signed_nip10_invalid_author_hint_tolerated",
    219         "social.reply.project_verified_event.valid",
    220     ),
    221     (
    222         "project_signed_nip10_invalid_participant_tolerated",
    223         "social.reply.project_verified_event.valid",
    224     ),
    225     (
    226         "project_signed_nip10_lone_reply_marker",
    227         "social.reply.project_verified_event.invalid",
    228     ),
    229     (
    230         "project_signed_nip10_marked_direct",
    231         "social.reply.project_verified_event.valid",
    232     ),
    233     (
    234         "project_signed_nip10_marked_with_citation",
    235         "social.reply.project_verified_event.valid",
    236     ),
    237     (
    238         "project_signed_nip10_marked_with_malformed_citation",
    239         "social.reply.project_verified_event.valid",
    240     ),
    241     (
    242         "project_signed_nip10_marked_nested_reordered",
    243         "social.reply.project_verified_event.valid",
    244     ),
    245     (
    246         "project_signed_nip10_missing_author",
    247         "social.reply.project_verified_event.valid",
    248     ),
    249     (
    250         "project_signed_nip10_positional_direct",
    251         "social.reply.project_verified_event.valid",
    252     ),
    253     (
    254         "project_signed_nip10_positional_direct_with_author_hint",
    255         "social.reply.project_verified_event.valid",
    256     ),
    257     (
    258         "project_signed_nip10_positional_many",
    259         "social.reply.project_verified_event.valid",
    260     ),
    261     (
    262         "project_signed_nip10_positional_many_with_author_hints",
    263         "social.reply.project_verified_event.valid",
    264     ),
    265     (
    266         "project_signed_nip10_positional_malformed_middle_citation",
    267         "social.reply.project_verified_event.valid",
    268     ),
    269     (
    270         "project_signed_nip10_precedes_ask_and_media",
    271         "social.reply.project_verified_event.valid",
    272     ),
    273     (
    274         "project_signed_nip10_unknown_marker",
    275         "social.reply.project_verified_event.invalid",
    276     ),
    277     (
    278         "project_signed_normalized_ask_precedes_malformed_media",
    279         "social.post.project_verified_event.valid",
    280     ),
    281     (
    282         "project_signed_photo_preserves_fallbacks_and_unknown_fields",
    283         "social.post.project_verified_event.valid",
    284     ),
    285     (
    286         "project_signed_structural_photo",
    287         "social.post.project_verified_event.valid",
    288     ),
    289     (
    290         "project_signed_thread_candidate_precedes_ask_and_media",
    291         "social.post.project_verified_event.valid",
    292     ),
    293     (
    294         "project_signed_update",
    295         "social.post.project_verified_event.valid",
    296     ),
    297 ];
    298 
    299 #[derive(Debug, Deserialize)]
    300 struct Suite {
    301     suite: String,
    302     contract_version: String,
    303     vectors: Vec<Vector>,
    304 }
    305 
    306 #[derive(Debug, Deserialize)]
    307 struct Vector {
    308     id: String,
    309     kind: String,
    310     input: Value,
    311     expected: Value,
    312 }
    313 
    314 #[test]
    315 fn post_operation_vectors_execute_the_declared_public_functions() {
    316     let vectors = conformance_vectors();
    317     let suite: Suite = serde_json::from_str(&vectors).expect("verified post vectors must parse");
    318     assert_eq!(suite.suite, "post_profiles");
    319     assert_eq!(suite.contract_version, "1.0.0");
    320     assert_eq!(vector_cases(&suite.vectors), expected_vector_cases());
    321 
    322     for vector in &suite.vectors {
    323         execute(vector);
    324     }
    325 }
    326 
    327 fn conformance_vectors() -> Cow<'static, str> {
    328     let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH);
    329     match fs::read_to_string(&workspace_path) {
    330         Ok(canonical) => {
    331             assert_eq!(
    332                 canonical,
    333                 PACKAGED_VECTORS,
    334                 "packaged verified post vectors must match {}",
    335                 workspace_path.display()
    336             );
    337             Cow::Owned(canonical)
    338         }
    339         Err(error)
    340             if error.kind() == std::io::ErrorKind::NotFound
    341                 && !Path::new(env!("CARGO_MANIFEST_DIR"))
    342                     .join(WORKSPACE_CONTRACT_MARKER_PATH)
    343                     .is_file() =>
    344         {
    345             Cow::Borrowed(PACKAGED_VECTORS)
    346         }
    347         Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
    348     }
    349 }
    350 
    351 fn execute(vector: &Vector) {
    352     match vector.kind.as_str() {
    353         "social.update.build_authored_draft.valid" => {
    354             let update = AuthoredUpdate::new(input_str(vector, "content"))
    355                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    356             let first = authored_update_to_wire_parts(&update);
    357             let second = authored_update_to_wire_parts(&update);
    358             assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
    359             assert_eq!(wire_parts_value(&first), vector.expected, "{}", vector.id);
    360         }
    361         "social.update.build_authored_draft.invalid" => {
    362             let error = AuthoredUpdate::new(input_str(vector, "content"))
    363                 .expect_err("invalid authored Update must fail");
    364             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    365         }
    366         "social.photo_update.build_authored_draft.valid" => {
    367             let images = authored_images(vector)
    368                 .unwrap_or_else(|error| panic!("{} image construction failed: {error}", vector.id));
    369             let photo = AuthoredPhotoUpdate::new(input_str(vector, "content"), images)
    370                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    371             let first = authored_photo_update_to_wire_parts(&photo);
    372             let second = authored_photo_update_to_wire_parts(&photo);
    373             assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
    374             assert_eq!(wire_parts_value(&first), vector.expected, "{}", vector.id);
    375         }
    376         "social.photo_update.build_authored_draft.invalid" => {
    377             let error = authored_images(vector).expect_err("invalid authored image must fail");
    378             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    379         }
    380         "social.ask.build_authored_draft.valid" => {
    381             let images = authored_images(vector)
    382                 .unwrap_or_else(|error| panic!("{} image construction failed: {error}", vector.id));
    383             let ask = AuthoredAsk::new(input_str(vector, "content"), images)
    384                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    385             let first = authored_ask_to_wire_parts(&ask);
    386             let second = authored_ask_to_wire_parts(&ask);
    387             assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
    388             assert_eq!(wire_parts_value(&first), vector.expected, "{}", vector.id);
    389         }
    390         "social.ask.build_authored_draft.invalid" => {
    391             let error = AuthoredAsk::new(input_str(vector, "content"), Vec::new())
    392                 .expect_err("invalid authored Ask must fail");
    393             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    394         }
    395         "social.reply.build_authored_draft.valid" => {
    396             let reply = authored_reply(vector)
    397                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    398             let first = authored_nip10_reply_to_wire_parts(&reply);
    399             let second = authored_nip10_reply_to_wire_parts(&reply);
    400             assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
    401             assert_eq!(wire_parts_value(&first), vector.expected, "{}", vector.id);
    402         }
    403         "social.reply.build_authored_draft.invalid" => {
    404             let error = authored_reply(vector).expect_err("invalid authored Reply must fail");
    405             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    406         }
    407         "social.reply.project_verified_event.valid" => {
    408             let envelope = canonical_envelope(input_str(vector, "event_json"));
    409             let verified = verify_nip01_event(envelope)
    410                 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id));
    411             let projection = project_verified_nip10_reply_event(&verified)
    412                 .unwrap_or_else(|error| panic!("{} projection failed: {error}", vector.id));
    413             assert_eq!(
    414                 reply_projection_value(&projection),
    415                 vector.expected,
    416                 "{}",
    417                 vector.id
    418             );
    419         }
    420         "social.reply.project_verified_event.invalid" => {
    421             let envelope = canonical_envelope(input_str(vector, "event_json"));
    422             let verified = verify_nip01_event(envelope)
    423                 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id));
    424             let error = project_verified_nip10_reply_event(&verified)
    425                 .expect_err("invalid verified Reply projection must fail");
    426             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    427         }
    428         "social.reply.verify_and_admit_event.valid" => {
    429             let envelope = canonical_envelope(input_str(vector, "event_json"));
    430             let expected_envelope = envelope.clone();
    431             let admitted = verify_and_admit_nip10_reply_event(envelope)
    432                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    433             assert_eq!(admitted.event(), &expected_envelope, "{}", vector.id);
    434             let actual = json!({
    435                 "contract_id": admitted.contract().id,
    436                 "style": reply_style_label(admitted.projection().style()),
    437                 "direct": admitted.projection().is_direct(),
    438             });
    439             let (verified, projection) = admitted.into_parts();
    440             assert_eq!(verified.event(), &expected_envelope, "{}", vector.id);
    441             assert_eq!(projection.contract_id(), "radroots.social.reply.v1");
    442             assert_eq!(actual, vector.expected, "{}", vector.id);
    443         }
    444         "social.reply.verify_and_admit_event.invalid" => {
    445             let envelope = canonical_envelope(input_str(vector, "event_json"));
    446             let error = verify_and_admit_nip10_reply_event(envelope)
    447                 .expect_err("invalid signed Reply vector must fail");
    448             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    449         }
    450         "social.post.project_verified_event.valid" => {
    451             let envelope = canonical_envelope(input_str(vector, "event_json"));
    452             let verified = verify_nip01_event(envelope)
    453                 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id));
    454             let projection = project_verified_post_event(&verified)
    455                 .unwrap_or_else(|error| panic!("{} projection failed: {error}", vector.id));
    456             assert_eq!(
    457                 projection_value(&projection),
    458                 vector.expected,
    459                 "{}",
    460                 vector.id
    461             );
    462         }
    463         "social.post.project_verified_event.invalid" => {
    464             let envelope = canonical_envelope(input_str(vector, "event_json"));
    465             let verified = verify_nip01_event(envelope)
    466                 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id));
    467             let error = project_verified_post_event(&verified)
    468                 .expect_err("invalid verified projection must fail");
    469             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    470         }
    471         "social.post.verify_and_admit_event.valid" => {
    472             let envelope = canonical_envelope(input_str(vector, "event_json"));
    473             let expected_envelope = envelope.clone();
    474             let generic = identify_event_contract(
    475                 envelope.kind_u32(),
    476                 &envelope.tags_as_vec(),
    477                 envelope.content(),
    478             )
    479             .expect("unsigned post identification remains available");
    480             assert_eq!(generic.id, "radroots.social.post.v1", "{}", vector.id);
    481 
    482             let outcome = verify_and_admit_post_event(envelope)
    483                 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    484             let actual = match outcome {
    485                 RadrootsPostAdmissionOutcome::Root(admitted) => {
    486                     assert_eq!(admitted.event(), &expected_envelope, "{}", vector.id);
    487                     let actual = json!({
    488                         "outcome": "root",
    489                         "contract_id": admitted.contract().id,
    490                         "projection": projection_value(admitted.projection()),
    491                     });
    492                     let (verified, projection) = admitted.into_parts();
    493                     assert_eq!(verified.event(), &expected_envelope, "{}", vector.id);
    494                     assert!(projection.classification().is_root_card(), "{}", vector.id);
    495                     actual
    496                 }
    497                 RadrootsPostAdmissionOutcome::ThreadExcluded(candidate) => {
    498                     assert_eq!(candidate.event(), &expected_envelope, "{}", vector.id);
    499                     let actual = json!({
    500                         "outcome": "thread_excluded",
    501                         "projection": projection_value(candidate.projection()),
    502                     });
    503                     let (verified, projection) = candidate.into_parts();
    504                     assert_eq!(verified.event(), &expected_envelope, "{}", vector.id);
    505                     assert_eq!(
    506                         projection.classification(),
    507                         RadrootsPostClassification::ThreadExcluded,
    508                         "{}",
    509                         vector.id
    510                     );
    511                     actual
    512                 }
    513                 _ => panic!("{} returned an unsupported admission outcome", vector.id),
    514             };
    515             assert_eq!(actual, vector.expected, "{}", vector.id);
    516         }
    517         "social.post.verify_and_admit_event.invalid" => {
    518             let envelope = canonical_envelope(input_str(vector, "event_json"));
    519             let error = verify_and_admit_post_event(envelope)
    520                 .expect_err("invalid signed post vector must fail");
    521             assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
    522         }
    523         kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
    524     }
    525 }
    526 
    527 fn projection_value(projection: &RadrootsInboundPostProjection) -> Value {
    528     json!({
    529         "classification": classification_label(projection.classification()),
    530         "contract_id": projection.classification().contract_id(),
    531         "ask_marker": projection.ask_marker(),
    532         "diagnostics": diagnostic_codes(projection.diagnostics()),
    533         "imeta": projection.imeta().iter().map(|media| json!({
    534             "raw_fields": media.raw_fields(),
    535             "url": media.url(),
    536             "sha256": media.sha256(),
    537             "media_type": media.media_type(),
    538             "dimensions": media.dimensions().map(|dimensions| json!({
    539                 "width": dimensions.width(),
    540                 "height": dimensions.height(),
    541             })),
    542             "size": media.size(),
    543             "alt": media.alt(),
    544             "fallbacks": media.fallbacks(),
    545             "unknown_fields": media.unknown_fields(),
    546             "diagnostics": diagnostic_codes(media.diagnostics()),
    547             "qualifies_photo": media.qualifies_photo(),
    548         })).collect::<Vec<_>>(),
    549     })
    550 }
    551 
    552 fn reply_projection_value(projection: &RadrootsInboundNip10ReplyProjection) -> Value {
    553     json!({
    554         "style": reply_style_label(projection.style()),
    555         "contract_id": projection.contract_id(),
    556         "direct": projection.is_direct(),
    557         "root": reply_event_reference_value(projection.root()),
    558         "parent": projection.reply_reference().map(reply_event_reference_value),
    559         "citations": projection
    560             .citations()
    561             .iter()
    562             .map(reply_event_reference_value)
    563             .collect::<Vec<_>>(),
    564         "participants": projection
    565             .participants()
    566             .iter()
    567             .map(reply_participant_value)
    568             .collect::<Vec<_>>(),
    569         "diagnostics": projection
    570             .diagnostics()
    571             .iter()
    572             .map(reply_diagnostic_value)
    573             .collect::<Vec<_>>(),
    574     })
    575 }
    576 
    577 fn reply_event_reference_value(reference: &RadrootsInboundNip10EventReference) -> Value {
    578     json!({
    579         "tag_index": reference.tag_index(),
    580         "raw_tag": reference.raw_tag(),
    581         "event_id": reference.event_id().to_hex(),
    582         "relay": reference.relay().map(|relay| relay.as_str()),
    583         "author_hint": reference.author_hint().map(|author| author.to_hex()),
    584     })
    585 }
    586 
    587 fn reply_participant_value(participant: &RadrootsInboundNip10Participant) -> Value {
    588     json!({
    589         "tag_index": participant.tag_index(),
    590         "raw_tag": participant.raw_tag(),
    591         "pubkey": participant.pubkey().to_hex(),
    592         "relay": participant.relay().map(|relay| relay.as_str()),
    593     })
    594 }
    595 
    596 fn reply_diagnostic_value(diagnostic: &RadrootsNip10ReplyDiagnostic) -> Value {
    597     json!({
    598         "code": diagnostic.code(),
    599         "tag_index": diagnostic.tag_index(),
    600         "raw_tag": diagnostic.raw_tag(),
    601     })
    602 }
    603 
    604 fn wire_parts_value(parts: &Nip01EventWireParts) -> Value {
    605     json!({
    606         "kind": parts.kind,
    607         "content": parts.content,
    608         "tags": parts.tags,
    609     })
    610 }
    611 
    612 fn authored_reply(vector: &Vector) -> Result<AuthoredNip10Reply, Nip10ReplyError> {
    613     let root = authored_reply_reference(vector, &vector.input["root"])?;
    614     match vector.input.get("parent") {
    615         None | Some(Value::Null) => AuthoredNip10Reply::direct(input_str(vector, "content"), root),
    616         Some(parent) => AuthoredNip10Reply::nested(
    617             input_str(vector, "content"),
    618             root,
    619             authored_reply_reference(vector, parent)?,
    620         ),
    621     }
    622 }
    623 
    624 fn authored_reply_reference(
    625     vector: &Vector,
    626     input: &Value,
    627 ) -> Result<Nip10ReplyReference, Nip10ReplyError> {
    628     let relay = match input.get("relay") {
    629         None | Some(Value::Null) => None,
    630         Some(relay) => Some(
    631             relay
    632                 .as_str()
    633                 .unwrap_or_else(|| panic!("{} Reply relay must be a string or null", vector.id)),
    634         ),
    635     };
    636     Nip10ReplyReference::parse(
    637         value_str(vector, input, "event_id"),
    638         value_str(vector, input, "author"),
    639         relay,
    640     )
    641 }
    642 
    643 fn authored_images(vector: &Vector) -> Result<Vec<AuthoredPostImage>, AuthoredPostError> {
    644     vector
    645         .input
    646         .get("images")
    647         .and_then(Value::as_array)
    648         .map_or_else(
    649             || Ok(Vec::new()),
    650             |images| {
    651                 images
    652                     .iter()
    653                     .map(|image| authored_image(vector, image))
    654                     .collect()
    655             },
    656         )
    657 }
    658 
    659 fn authored_image(vector: &Vector, input: &Value) -> Result<AuthoredPostImage, AuthoredPostError> {
    660     let bytes = value_str(vector, input, "bytes_utf8").as_bytes();
    661     let media_type = MediaType::parse(value_str(vector, input, "media_type"))
    662         .unwrap_or_else(|error| panic!("{} media type setup failed: {error}", vector.id));
    663     let hash = Sha256::digest(bytes);
    664     let descriptor = BlobDescriptor::new(
    665         BlobUrl::parse(value_str(vector, input, "url"))
    666             .unwrap_or_else(|error| panic!("{} URL setup failed: {error}", vector.id)),
    667         hash,
    668         u64::try_from(bytes.len()).expect("authored image byte length must fit u64"),
    669         media_type.clone(),
    670         value_u64(vector, input, "uploaded"),
    671     )
    672     .unwrap_or_else(|error| panic!("{} descriptor setup failed: {error}", vector.id))
    673     .approve_reference()
    674     .unwrap_or_else(|error| panic!("{} URL approval failed: {error}", vector.id))
    675     .verify_bytes(bytes, &media_type)
    676     .unwrap_or_else(|error| panic!("{} byte verification failed: {error}", vector.id));
    677     let image = AuthoredImage::try_from(descriptor)
    678         .unwrap_or_else(|error| panic!("{} image typestate setup failed: {error}", vector.id));
    679     let dimensions = PostImageDimensions::new(
    680         u32::try_from(value_u64(vector, input, "width"))
    681             .unwrap_or_else(|_| panic!("{} image.width must fit u32", vector.id)),
    682         u32::try_from(value_u64(vector, input, "height"))
    683             .unwrap_or_else(|_| panic!("{} image.height must fit u32", vector.id)),
    684     )?;
    685     let mut image = AuthoredPostImage::new(image, dimensions, value_str(vector, input, "alt"))?;
    686     if let Some(fallbacks) = input.get("fallbacks").and_then(Value::as_array) {
    687         for fallback in fallbacks {
    688             image = image.try_with_fallback(
    689                 BlobUrl::parse(
    690                     fallback
    691                         .as_str()
    692                         .unwrap_or_else(|| panic!("{} image fallback must be a string", vector.id)),
    693                 )
    694                 .unwrap_or_else(|error| panic!("{} fallback setup failed: {error}", vector.id))
    695                 .approve()
    696                 .unwrap_or_else(|error| panic!("{} fallback approval failed: {error}", vector.id)),
    697             )?;
    698         }
    699     }
    700     Ok(image)
    701 }
    702 
    703 fn classification_label(classification: RadrootsPostClassification) -> &'static str {
    704     match classification {
    705         RadrootsPostClassification::ThreadExcluded => "thread_excluded",
    706         RadrootsPostClassification::Update => "update",
    707         RadrootsPostClassification::PhotoUpdate => "photo_update",
    708         RadrootsPostClassification::Ask => "ask",
    709         _ => "future",
    710     }
    711 }
    712 
    713 fn reply_style_label(style: RadrootsNip10ReplyStyle) -> &'static str {
    714     match style {
    715         RadrootsNip10ReplyStyle::Marked => "marked",
    716         RadrootsNip10ReplyStyle::LegacyPositional => "legacy_positional",
    717         _ => "future",
    718     }
    719 }
    720 
    721 fn diagnostic_codes(diagnostics: &[RadrootsPostDiagnostic]) -> Vec<&'static str> {
    722     diagnostics
    723         .iter()
    724         .map(|diagnostic| diagnostic.code())
    725         .collect()
    726 }
    727 
    728 fn canonical_envelope(raw_json: &str) -> EventEnvelope {
    729     Nip01EventWire::parse_json(raw_json)
    730         .expect("canonical raw event")
    731         .into_envelope()
    732         .expect("event envelope")
    733 }
    734 
    735 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    736     vector.input[field]
    737         .as_str()
    738         .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id))
    739 }
    740 
    741 fn value_str<'a>(vector: &Vector, value: &'a Value, field: &str) -> &'a str {
    742     value[field]
    743         .as_str()
    744         .unwrap_or_else(|| panic!("{} image.{field} must be a string", vector.id))
    745 }
    746 
    747 fn value_u64(vector: &Vector, value: &Value, field: &str) -> u64 {
    748     value[field]
    749         .as_u64()
    750         .unwrap_or_else(|| panic!("{} image.{field} must be a u64", vector.id))
    751 }
    752 
    753 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    754     vector.expected[field]
    755         .as_str()
    756         .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id))
    757 }
    758 
    759 fn vector_cases(vectors: &[Vector]) -> BTreeMap<&str, &str> {
    760     let mut cases = BTreeMap::new();
    761     for vector in vectors {
    762         assert!(
    763             cases
    764                 .insert(vector.id.as_str(), vector.kind.as_str())
    765                 .is_none(),
    766             "duplicate post vector id {}",
    767             vector.id
    768         );
    769     }
    770     cases
    771 }
    772 
    773 fn expected_vector_cases() -> BTreeMap<&'static str, &'static str> {
    774     EXPECTED_VECTOR_CASES.into_iter().collect()
    775 }