lib

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

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 }