model.rs (19237B)
1 use crate::RadrootsEventStoreError; 2 use radroots_events::RadrootsNostrEvent; 3 use radroots_events::contract::{ 4 RadrootsContractMatchError, RadrootsEventClass, RadrootsTagSemantic, RadrootsTagValueType, 5 }; 6 use radroots_events::event_head::RadrootsEventHeadDecision; 7 8 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 9 pub enum RadrootsEventVerificationStatus { 10 NotChecked, 11 IdVerified, 12 Verified, 13 IdMismatch, 14 SignatureInvalid, 15 MalformedEnvelope, 16 } 17 18 impl RadrootsEventVerificationStatus { 19 pub fn as_str(self) -> &'static str { 20 match self { 21 Self::NotChecked => "not_checked", 22 Self::IdVerified => "id_verified", 23 Self::Verified => "verified", 24 Self::IdMismatch => "id_mismatch", 25 Self::SignatureInvalid => "signature_invalid", 26 Self::MalformedEnvelope => "malformed_envelope", 27 } 28 } 29 30 pub fn parse(value: &str) -> Result<Self, RadrootsEventStoreError> { 31 match value { 32 "not_checked" => Ok(Self::NotChecked), 33 "id_verified" => Ok(Self::IdVerified), 34 "verified" => Ok(Self::Verified), 35 "id_mismatch" => Ok(Self::IdMismatch), 36 "signature_invalid" => Ok(Self::SignatureInvalid), 37 "malformed_envelope" => Ok(Self::MalformedEnvelope), 38 _ => Err(RadrootsEventStoreError::InvalidStoredEnum { 39 field: "verification_status", 40 value: value.to_owned(), 41 }), 42 } 43 } 44 } 45 46 #[derive(Clone, Debug, PartialEq, Eq)] 47 pub enum RadrootsEventContractStatus { 48 Supported, 49 UnsupportedKind(u32), 50 UnsupportedShape(u32), 51 AmbiguousShape(u32), 52 } 53 54 impl RadrootsEventContractStatus { 55 pub fn as_str(&self) -> &'static str { 56 match self { 57 Self::Supported => "supported", 58 Self::UnsupportedKind(_) => "unsupported_kind", 59 Self::UnsupportedShape(_) => "unsupported_shape", 60 Self::AmbiguousShape(_) => "ambiguous_shape", 61 } 62 } 63 64 pub fn from_match_error(error: RadrootsContractMatchError) -> Self { 65 match error { 66 RadrootsContractMatchError::UnsupportedKind(kind) => Self::UnsupportedKind(kind), 67 RadrootsContractMatchError::UnsupportedShape(kind) => Self::UnsupportedShape(kind), 68 RadrootsContractMatchError::AmbiguousShape(kind) => Self::AmbiguousShape(kind), 69 } 70 } 71 72 pub fn parse(value: &str, kind: u32) -> Result<Self, RadrootsEventStoreError> { 73 match value { 74 "supported" => Ok(Self::Supported), 75 "unsupported_kind" => Ok(Self::UnsupportedKind(kind)), 76 "unsupported_shape" => Ok(Self::UnsupportedShape(kind)), 77 "ambiguous_shape" => Ok(Self::AmbiguousShape(kind)), 78 _ => Err(RadrootsEventStoreError::InvalidStoredEnum { 79 field: "contract_status", 80 value: value.to_owned(), 81 }), 82 } 83 } 84 } 85 86 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 87 pub enum StoredEventClass { 88 Regular, 89 Replaceable, 90 Addressable, 91 Ephemeral, 92 } 93 94 impl StoredEventClass { 95 pub fn as_str(self) -> &'static str { 96 match self { 97 Self::Regular => "regular", 98 Self::Replaceable => "replaceable", 99 Self::Addressable => "addressable", 100 Self::Ephemeral => "ephemeral", 101 } 102 } 103 104 pub fn from_event_class(value: RadrootsEventClass) -> Self { 105 match value { 106 RadrootsEventClass::Regular => Self::Regular, 107 RadrootsEventClass::Replaceable => Self::Replaceable, 108 RadrootsEventClass::Addressable => Self::Addressable, 109 RadrootsEventClass::Ephemeral => Self::Ephemeral, 110 } 111 } 112 113 pub fn parse(value: &str) -> Result<Self, RadrootsEventStoreError> { 114 match value { 115 "regular" => Ok(Self::Regular), 116 "replaceable" => Ok(Self::Replaceable), 117 "addressable" => Ok(Self::Addressable), 118 "ephemeral" => Ok(Self::Ephemeral), 119 _ => Err(RadrootsEventStoreError::InvalidStoredEnum { 120 field: "event_class", 121 value: value.to_owned(), 122 }), 123 } 124 } 125 } 126 127 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 128 pub enum RadrootsRelayObservationType { 129 Fetch, 130 Subscription, 131 PublishAck, 132 Import, 133 } 134 135 impl RadrootsRelayObservationType { 136 pub fn as_str(self) -> &'static str { 137 match self { 138 Self::Fetch => "fetch", 139 Self::Subscription => "subscription", 140 Self::PublishAck => "publish_ack", 141 Self::Import => "import", 142 } 143 } 144 } 145 146 #[derive(Clone, Debug, PartialEq, Eq)] 147 pub struct RadrootsRelayObservation { 148 pub relay_url: String, 149 pub observation_type: RadrootsRelayObservationType, 150 pub observed_at_ms: i64, 151 pub message: Option<String>, 152 } 153 154 impl RadrootsRelayObservation { 155 pub fn new( 156 relay_url: impl Into<String>, 157 observation_type: RadrootsRelayObservationType, 158 observed_at_ms: i64, 159 ) -> Self { 160 Self { 161 relay_url: relay_url.into(), 162 observation_type, 163 observed_at_ms, 164 message: None, 165 } 166 } 167 168 pub fn with_message(mut self, message: impl Into<String>) -> Self { 169 self.message = Some(message.into()); 170 self 171 } 172 } 173 174 #[derive(Clone, Debug, PartialEq, Eq)] 175 pub struct RadrootsEventIngest { 176 pub event: RadrootsNostrEvent, 177 pub raw_json: Option<String>, 178 pub observed_at_ms: i64, 179 pub relay_observation: Option<RadrootsRelayObservation>, 180 } 181 182 impl RadrootsEventIngest { 183 pub fn new(event: RadrootsNostrEvent, observed_at_ms: i64) -> Self { 184 Self { 185 event, 186 raw_json: None, 187 observed_at_ms, 188 relay_observation: None, 189 } 190 } 191 192 pub fn with_raw_json(mut self, raw_json: impl Into<String>) -> Self { 193 self.raw_json = Some(raw_json.into()); 194 self 195 } 196 197 pub fn with_observation(mut self, observation: RadrootsRelayObservation) -> Self { 198 self.relay_observation = Some(observation); 199 self 200 } 201 } 202 203 #[derive(Clone, Debug, PartialEq, Eq)] 204 pub enum RadrootsEventHeadStoreDecision { 205 Applied, 206 NotHeadSelected, 207 NotPersisted, 208 NotProjectionEligible, 209 SkippedDuplicate, 210 SkippedOlder, 211 SkippedSameTimestampHigherEventId, 212 Malformed, 213 Unsupported, 214 } 215 216 impl RadrootsEventHeadStoreDecision { 217 pub fn from_protocol(value: &RadrootsEventHeadDecision) -> Self { 218 match value { 219 RadrootsEventHeadDecision::Applied(_) => Self::Applied, 220 RadrootsEventHeadDecision::SkippedDuplicate => Self::SkippedDuplicate, 221 RadrootsEventHeadDecision::SkippedOlder => Self::SkippedOlder, 222 RadrootsEventHeadDecision::SkippedSameTimestampHigherEventId => { 223 Self::SkippedSameTimestampHigherEventId 224 } 225 RadrootsEventHeadDecision::CoordinateMismatch => Self::Malformed, 226 } 227 } 228 } 229 230 #[derive(Clone, Debug, PartialEq, Eq)] 231 pub struct RadrootsEventIngestReceipt { 232 pub seq: i64, 233 pub event_id: String, 234 pub inserted: bool, 235 pub verification_status: RadrootsEventVerificationStatus, 236 pub contract_status: RadrootsEventContractStatus, 237 pub contract_id: Option<String>, 238 pub projection_eligible: bool, 239 pub head_decision: RadrootsEventHeadStoreDecision, 240 } 241 242 #[derive(Clone, Debug, PartialEq, Eq)] 243 pub struct RadrootsEventStoreStatusSummary { 244 pub total_events: i64, 245 pub projection_eligible_events: i64, 246 pub relay_observations: i64, 247 pub last_event_seq: Option<i64>, 248 pub last_event_updated_at_ms: Option<i64>, 249 } 250 251 #[derive(Clone, Debug, PartialEq, Eq)] 252 pub struct RadrootsStoredEvent { 253 pub seq: i64, 254 pub event_id: String, 255 pub pubkey: String, 256 pub created_at: u32, 257 pub kind: u32, 258 pub tags_json: String, 259 pub content: String, 260 pub sig: String, 261 pub raw_json: String, 262 pub verification_status: RadrootsEventVerificationStatus, 263 pub contract_status: RadrootsEventContractStatus, 264 pub contract_id: Option<String>, 265 pub event_class: Option<StoredEventClass>, 266 pub projection_eligible: bool, 267 pub inserted_at_ms: i64, 268 pub updated_at_ms: i64, 269 } 270 271 #[derive(Clone, Debug, PartialEq, Eq)] 272 pub struct RadrootsStoredEventTag { 273 pub event_id: String, 274 pub tag_index: u32, 275 pub tag_name: String, 276 pub tag_value: Option<String>, 277 pub tag_json: String, 278 pub contract_semantic: Option<String>, 279 pub contract_value_type: Option<String>, 280 pub relay_indexed: bool, 281 } 282 283 #[derive(Clone, Debug, PartialEq, Eq)] 284 pub struct RadrootsStoredEventHead { 285 pub coordinate_type: StoredEventClass, 286 pub kind: u32, 287 pub pubkey: String, 288 pub d_tag: Option<String>, 289 pub event_id: String, 290 pub created_at: u32, 291 pub updated_at_ms: i64, 292 } 293 294 #[derive(Clone, Debug, PartialEq, Eq)] 295 pub struct RadrootsProjectionCursor { 296 pub projection_id: String, 297 pub projection_version: u32, 298 pub last_event_seq: i64, 299 pub updated_at_ms: i64, 300 } 301 302 pub fn tag_semantic_name(value: RadrootsTagSemantic) -> &'static str { 303 match value { 304 RadrootsTagSemantic::AddressableCoordinate => "addressable_coordinate", 305 RadrootsTagSemantic::Category => "category", 306 RadrootsTagSemantic::Counterparty => "counterparty", 307 RadrootsTagSemantic::EventPointer => "event_pointer", 308 RadrootsTagSemantic::GroupId => "group_id", 309 RadrootsTagSemantic::Identifier => "identifier", 310 RadrootsTagSemantic::Image => "image", 311 RadrootsTagSemantic::Kind => "kind", 312 RadrootsTagSemantic::ListingAddress => "listing_address", 313 RadrootsTagSemantic::ListingSnapshot => "listing_snapshot", 314 RadrootsTagSemantic::Location => "location", 315 RadrootsTagSemantic::PreviousEvent => "previous_event", 316 RadrootsTagSemantic::Price => "price", 317 RadrootsTagSemantic::PublishedAt => "published_at", 318 RadrootsTagSemantic::Relay => "relay", 319 RadrootsTagSemantic::RootEvent => "root_event", 320 RadrootsTagSemantic::ServiceInput => "service_input", 321 RadrootsTagSemantic::ServiceOutput => "service_output", 322 RadrootsTagSemantic::Status => "status", 323 RadrootsTagSemantic::Summary => "summary", 324 RadrootsTagSemantic::Title => "title", 325 RadrootsTagSemantic::Url => "url", 326 } 327 } 328 329 pub fn tag_value_type_name(value: RadrootsTagValueType) -> &'static str { 330 match value { 331 RadrootsTagValueType::AddressableCoordinate => "addressable_coordinate", 332 RadrootsTagValueType::DTag => "d_tag", 333 RadrootsTagValueType::EventId => "event_id", 334 RadrootsTagValueType::EventPointer => "event_pointer", 335 RadrootsTagValueType::Kind => "kind", 336 RadrootsTagValueType::PublicKey => "public_key", 337 RadrootsTagValueType::RelayUrl => "relay_url", 338 RadrootsTagValueType::Text => "text", 339 RadrootsTagValueType::UnixTimestamp => "unix_timestamp", 340 RadrootsTagValueType::Url => "url", 341 } 342 } 343 344 #[cfg(test)] 345 mod tests { 346 use super::*; 347 use radroots_events::event_head::{ 348 RadrootsCurrentEventHead, RadrootsEventHeadCoordinate, RadrootsEventHeadDecision, 349 }; 350 use radroots_events::ids::{RadrootsDTag, RadrootsEventId, RadrootsPublicKey}; 351 352 #[test] 353 fn contract_status_event_class_and_observation_values_roundtrip() { 354 assert_eq!( 355 RadrootsEventContractStatus::from_match_error( 356 RadrootsContractMatchError::UnsupportedKind(7) 357 ), 358 RadrootsEventContractStatus::UnsupportedKind(7) 359 ); 360 assert_eq!( 361 RadrootsEventContractStatus::from_match_error( 362 RadrootsContractMatchError::UnsupportedShape(8) 363 ), 364 RadrootsEventContractStatus::UnsupportedShape(8) 365 ); 366 assert_eq!( 367 RadrootsEventContractStatus::from_match_error( 368 RadrootsContractMatchError::AmbiguousShape(9) 369 ), 370 RadrootsEventContractStatus::AmbiguousShape(9) 371 ); 372 373 for (status, expected) in [ 374 (RadrootsEventContractStatus::Supported, "supported"), 375 ( 376 RadrootsEventContractStatus::UnsupportedKind(1), 377 "unsupported_kind", 378 ), 379 ( 380 RadrootsEventContractStatus::UnsupportedShape(2), 381 "unsupported_shape", 382 ), 383 ( 384 RadrootsEventContractStatus::AmbiguousShape(3), 385 "ambiguous_shape", 386 ), 387 ] { 388 assert_eq!(status.as_str(), expected); 389 assert_eq!( 390 RadrootsEventContractStatus::parse(expected, 99).expect("status"), 391 match status { 392 RadrootsEventContractStatus::Supported => 393 RadrootsEventContractStatus::Supported, 394 RadrootsEventContractStatus::UnsupportedKind(_) => { 395 RadrootsEventContractStatus::UnsupportedKind(99) 396 } 397 RadrootsEventContractStatus::UnsupportedShape(_) => { 398 RadrootsEventContractStatus::UnsupportedShape(99) 399 } 400 RadrootsEventContractStatus::AmbiguousShape(_) => { 401 RadrootsEventContractStatus::AmbiguousShape(99) 402 } 403 } 404 ); 405 } 406 assert!(RadrootsEventContractStatus::parse("bad", 1).is_err()); 407 408 for class in [ 409 StoredEventClass::Regular, 410 StoredEventClass::Replaceable, 411 StoredEventClass::Addressable, 412 StoredEventClass::Ephemeral, 413 ] { 414 assert_eq!( 415 StoredEventClass::parse(class.as_str()).expect("class"), 416 class 417 ); 418 } 419 assert_eq!( 420 StoredEventClass::from_event_class(RadrootsEventClass::Regular), 421 StoredEventClass::Regular 422 ); 423 assert_eq!( 424 StoredEventClass::from_event_class(RadrootsEventClass::Replaceable), 425 StoredEventClass::Replaceable 426 ); 427 assert_eq!( 428 StoredEventClass::from_event_class(RadrootsEventClass::Addressable), 429 StoredEventClass::Addressable 430 ); 431 assert_eq!( 432 StoredEventClass::from_event_class(RadrootsEventClass::Ephemeral), 433 StoredEventClass::Ephemeral 434 ); 435 assert!(StoredEventClass::parse("bad").is_err()); 436 437 for observation_type in [ 438 RadrootsRelayObservationType::Fetch, 439 RadrootsRelayObservationType::Subscription, 440 RadrootsRelayObservationType::PublishAck, 441 RadrootsRelayObservationType::Import, 442 ] { 443 assert!(!observation_type.as_str().is_empty()); 444 } 445 let observation = RadrootsRelayObservation::new( 446 "wss://relay.example.test", 447 RadrootsRelayObservationType::Fetch, 448 1, 449 ) 450 .with_message("seen"); 451 assert_eq!(observation.message.as_deref(), Some("seen")); 452 } 453 454 #[test] 455 fn head_decisions_and_tag_metadata_names_cover_all_variants() { 456 let coordinate = RadrootsEventHeadCoordinate::Addressable { 457 kind: 30_023, 458 pubkey: RadrootsPublicKey::parse( 459 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 460 ) 461 .expect("pubkey"), 462 d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAA").expect("d tag"), 463 }; 464 let current = RadrootsCurrentEventHead { 465 coordinate, 466 event_id: RadrootsEventId::parse( 467 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 468 ) 469 .expect("event id"), 470 created_at: 10, 471 }; 472 473 assert_eq!( 474 RadrootsEventHeadStoreDecision::from_protocol(&RadrootsEventHeadDecision::Applied( 475 current 476 )), 477 RadrootsEventHeadStoreDecision::Applied 478 ); 479 assert_eq!( 480 RadrootsEventHeadStoreDecision::from_protocol( 481 &RadrootsEventHeadDecision::SkippedDuplicate 482 ), 483 RadrootsEventHeadStoreDecision::SkippedDuplicate 484 ); 485 assert_eq!( 486 RadrootsEventHeadStoreDecision::from_protocol(&RadrootsEventHeadDecision::SkippedOlder), 487 RadrootsEventHeadStoreDecision::SkippedOlder 488 ); 489 assert_eq!( 490 RadrootsEventHeadStoreDecision::from_protocol( 491 &RadrootsEventHeadDecision::SkippedSameTimestampHigherEventId 492 ), 493 RadrootsEventHeadStoreDecision::SkippedSameTimestampHigherEventId 494 ); 495 assert_eq!( 496 RadrootsEventHeadStoreDecision::from_protocol( 497 &RadrootsEventHeadDecision::CoordinateMismatch 498 ), 499 RadrootsEventHeadStoreDecision::Malformed 500 ); 501 502 for (semantic, expected) in [ 503 ( 504 RadrootsTagSemantic::AddressableCoordinate, 505 "addressable_coordinate", 506 ), 507 (RadrootsTagSemantic::Category, "category"), 508 (RadrootsTagSemantic::Counterparty, "counterparty"), 509 (RadrootsTagSemantic::EventPointer, "event_pointer"), 510 (RadrootsTagSemantic::GroupId, "group_id"), 511 (RadrootsTagSemantic::Identifier, "identifier"), 512 (RadrootsTagSemantic::Image, "image"), 513 (RadrootsTagSemantic::Kind, "kind"), 514 (RadrootsTagSemantic::ListingAddress, "listing_address"), 515 (RadrootsTagSemantic::ListingSnapshot, "listing_snapshot"), 516 (RadrootsTagSemantic::Location, "location"), 517 (RadrootsTagSemantic::PreviousEvent, "previous_event"), 518 (RadrootsTagSemantic::Price, "price"), 519 (RadrootsTagSemantic::PublishedAt, "published_at"), 520 (RadrootsTagSemantic::Relay, "relay"), 521 (RadrootsTagSemantic::RootEvent, "root_event"), 522 (RadrootsTagSemantic::ServiceInput, "service_input"), 523 (RadrootsTagSemantic::ServiceOutput, "service_output"), 524 (RadrootsTagSemantic::Status, "status"), 525 (RadrootsTagSemantic::Summary, "summary"), 526 (RadrootsTagSemantic::Title, "title"), 527 (RadrootsTagSemantic::Url, "url"), 528 ] { 529 assert_eq!(tag_semantic_name(semantic), expected); 530 } 531 532 for (value_type, expected) in [ 533 ( 534 RadrootsTagValueType::AddressableCoordinate, 535 "addressable_coordinate", 536 ), 537 (RadrootsTagValueType::DTag, "d_tag"), 538 (RadrootsTagValueType::EventId, "event_id"), 539 (RadrootsTagValueType::EventPointer, "event_pointer"), 540 (RadrootsTagValueType::Kind, "kind"), 541 (RadrootsTagValueType::PublicKey, "public_key"), 542 (RadrootsTagValueType::RelayUrl, "relay_url"), 543 (RadrootsTagValueType::Text, "text"), 544 (RadrootsTagValueType::UnixTimestamp, "unix_timestamp"), 545 (RadrootsTagValueType::Url, "url"), 546 ] { 547 assert_eq!(tag_value_type_name(value_type), expected); 548 } 549 } 550 }