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 }