lib

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

models.rs (16678B)


      1 #![forbid(unsafe_code)]
      2 
      3 use serde::{Deserialize, Serialize};
      4 use serde_json::Value;
      5 
      6 use crate::LocalEventsError;
      7 
      8 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
      9 #[serde(rename_all = "snake_case")]
     10 pub enum LocalRecordFamily {
     11     LocalWork,
     12     SignedEvent,
     13 }
     14 
     15 impl LocalRecordFamily {
     16     pub fn as_str(self) -> &'static str {
     17         match self {
     18             Self::LocalWork => "local_work",
     19             Self::SignedEvent => "signed_event",
     20         }
     21     }
     22 
     23     pub fn parse(value: &str) -> Result<Self, LocalEventsError> {
     24         match value {
     25             "local_work" => Ok(Self::LocalWork),
     26             "signed_event" => Ok(Self::SignedEvent),
     27             other => Err(LocalEventsError::InvalidRecord(format!(
     28                 "unknown record family `{other}`"
     29             ))),
     30         }
     31     }
     32 }
     33 
     34 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
     35 #[serde(rename_all = "snake_case")]
     36 pub enum LocalRecordStatus {
     37     LocalDraft,
     38     LocalSaved,
     39     PendingPublish,
     40     Published,
     41     Failed,
     42     Conflict,
     43 }
     44 
     45 impl LocalRecordStatus {
     46     pub fn as_str(self) -> &'static str {
     47         match self {
     48             Self::LocalDraft => "local_draft",
     49             Self::LocalSaved => "local_saved",
     50             Self::PendingPublish => "pending_publish",
     51             Self::Published => "published",
     52             Self::Failed => "failed",
     53             Self::Conflict => "conflict",
     54         }
     55     }
     56 
     57     pub fn parse(value: &str) -> Result<Self, LocalEventsError> {
     58         match value {
     59             "local_draft" => Ok(Self::LocalDraft),
     60             "local_saved" => Ok(Self::LocalSaved),
     61             "pending_publish" => Ok(Self::PendingPublish),
     62             "published" => Ok(Self::Published),
     63             "failed" => Ok(Self::Failed),
     64             "conflict" => Ok(Self::Conflict),
     65             other => Err(LocalEventsError::InvalidRecord(format!(
     66                 "unknown record status `{other}`"
     67             ))),
     68         }
     69     }
     70 }
     71 
     72 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
     73 #[serde(rename_all = "snake_case")]
     74 pub enum PublishOutboxStatus {
     75     None,
     76     Pending,
     77     Acknowledged,
     78     Failed,
     79 }
     80 
     81 impl PublishOutboxStatus {
     82     pub fn as_str(self) -> &'static str {
     83         match self {
     84             Self::None => "none",
     85             Self::Pending => "pending",
     86             Self::Acknowledged => "acknowledged",
     87             Self::Failed => "failed",
     88         }
     89     }
     90 
     91     pub fn parse(value: &str) -> Result<Self, LocalEventsError> {
     92         match value {
     93             "none" => Ok(Self::None),
     94             "pending" => Ok(Self::Pending),
     95             "acknowledged" => Ok(Self::Acknowledged),
     96             "failed" => Ok(Self::Failed),
     97             other => Err(LocalEventsError::InvalidRecord(format!(
     98                 "unknown outbox status `{other}`"
     99             ))),
    100         }
    101     }
    102 }
    103 
    104 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
    105 #[serde(rename_all = "snake_case")]
    106 pub enum SourceRuntime {
    107     Cli,
    108     App,
    109     Network,
    110     Service,
    111     Worker,
    112     Test,
    113 }
    114 
    115 impl SourceRuntime {
    116     pub fn as_str(self) -> &'static str {
    117         match self {
    118             Self::Cli => "cli",
    119             Self::App => "app",
    120             Self::Network => "network",
    121             Self::Service => "service",
    122             Self::Worker => "worker",
    123             Self::Test => "test",
    124         }
    125     }
    126 
    127     pub fn parse(value: &str) -> Result<Self, LocalEventsError> {
    128         match value {
    129             "cli" => Ok(Self::Cli),
    130             "app" => Ok(Self::App),
    131             "network" => Ok(Self::Network),
    132             "service" => Ok(Self::Service),
    133             "worker" => Ok(Self::Worker),
    134             "test" => Ok(Self::Test),
    135             other => Err(LocalEventsError::InvalidRecord(format!(
    136                 "unknown source runtime `{other}`"
    137             ))),
    138         }
    139     }
    140 }
    141 
    142 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
    143 pub struct LocalEventRecordInput {
    144     pub record_id: String,
    145     pub family: LocalRecordFamily,
    146     pub status: LocalRecordStatus,
    147     pub source_runtime: SourceRuntime,
    148     pub created_at_ms: i64,
    149     pub inserted_at_ms: i64,
    150     pub owner_account_id: Option<String>,
    151     pub owner_pubkey: Option<String>,
    152     pub farm_id: Option<String>,
    153     pub listing_addr: Option<String>,
    154     pub local_work_json: Option<Value>,
    155     pub event_id: Option<String>,
    156     pub event_kind: Option<i64>,
    157     pub event_pubkey: Option<String>,
    158     pub event_created_at: Option<i64>,
    159     pub event_tags_json: Option<Value>,
    160     pub event_content: Option<String>,
    161     pub event_sig: Option<String>,
    162     pub raw_event_json: Option<Value>,
    163     pub outbox_status: PublishOutboxStatus,
    164     pub relay_set_fingerprint: Option<String>,
    165     pub relay_delivery_json: Option<Value>,
    166 }
    167 
    168 impl LocalEventRecordInput {
    169     pub fn validate(&self) -> Result<(), LocalEventsError> {
    170         validate_non_empty("record_id", &self.record_id)?;
    171         if let Some(value) = self.owner_account_id.as_deref() {
    172             validate_non_empty("owner_account_id", value)?;
    173         }
    174         if let Some(value) = self.owner_pubkey.as_deref() {
    175             validate_non_empty("owner_pubkey", value)?;
    176         }
    177         if let Some(value) = self.farm_id.as_deref() {
    178             validate_non_empty("farm_id", value)?;
    179         }
    180         if let Some(value) = self.listing_addr.as_deref() {
    181             validate_non_empty("listing_addr", value)?;
    182         }
    183         match self.family {
    184             LocalRecordFamily::LocalWork => {
    185                 if self.local_work_json.is_none() {
    186                     return Err(LocalEventsError::InvalidRecord(
    187                         "local work records require local_work_json".to_owned(),
    188                     ));
    189                 }
    190                 if self.outbox_status != PublishOutboxStatus::None {
    191                     return Err(LocalEventsError::InvalidRecord(
    192                         "local work records must use outbox status none".to_owned(),
    193                     ));
    194                 }
    195             }
    196             LocalRecordFamily::SignedEvent => {
    197                 validate_required("event_id", self.event_id.as_deref())?;
    198                 validate_required("event_pubkey", self.event_pubkey.as_deref())?;
    199                 validate_required("event_sig", self.event_sig.as_deref())?;
    200                 if self.event_kind.is_none() {
    201                     return Err(LocalEventsError::InvalidRecord(
    202                         "signed event records require event_kind".to_owned(),
    203                     ));
    204                 }
    205                 if self.raw_event_json.is_none() {
    206                     return Err(LocalEventsError::InvalidRecord(
    207                         "signed event records require raw_event_json".to_owned(),
    208                     ));
    209                 }
    210             }
    211         }
    212         Ok(())
    213     }
    214 }
    215 
    216 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
    217 pub struct LocalEventRecord {
    218     pub seq: i64,
    219     pub change_seq: i64,
    220     pub record_id: String,
    221     pub family: LocalRecordFamily,
    222     pub status: LocalRecordStatus,
    223     pub source_runtime: SourceRuntime,
    224     pub created_at_ms: i64,
    225     pub inserted_at_ms: i64,
    226     pub updated_at_ms: i64,
    227     pub owner_account_id: Option<String>,
    228     pub owner_pubkey: Option<String>,
    229     pub farm_id: Option<String>,
    230     pub listing_addr: Option<String>,
    231     pub local_work_json: Option<Value>,
    232     pub event_id: Option<String>,
    233     pub event_kind: Option<i64>,
    234     pub event_pubkey: Option<String>,
    235     pub event_created_at: Option<i64>,
    236     pub event_tags_json: Option<Value>,
    237     pub event_content: Option<String>,
    238     pub event_sig: Option<String>,
    239     pub raw_event_json: Option<Value>,
    240     pub outbox_status: PublishOutboxStatus,
    241     pub relay_set_fingerprint: Option<String>,
    242     pub relay_delivery_json: Option<Value>,
    243 }
    244 
    245 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
    246 pub struct LocalEventRecordUpdate {
    247     pub record_id: String,
    248     pub status: LocalRecordStatus,
    249     pub outbox_status: PublishOutboxStatus,
    250     pub relay_set_fingerprint: Option<String>,
    251     pub relay_delivery_json: Option<Value>,
    252     pub updated_at_ms: i64,
    253 }
    254 
    255 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    256 pub struct LocalEventsCursor {
    257     pub consumer_id: String,
    258     pub last_change_seq: i64,
    259     pub updated_at_ms: i64,
    260 }
    261 
    262 pub(crate) fn validate_non_empty(field: &str, value: &str) -> Result<(), LocalEventsError> {
    263     if value.trim().is_empty() {
    264         return Err(LocalEventsError::InvalidRecord(format!(
    265             "{field} must not be empty"
    266         )));
    267     }
    268     Ok(())
    269 }
    270 
    271 fn validate_required(field: &str, value: Option<&str>) -> Result<(), LocalEventsError> {
    272     match value {
    273         Some(value) => validate_non_empty(field, value),
    274         None => Err(LocalEventsError::InvalidRecord(format!(
    275             "{field} is required"
    276         ))),
    277     }
    278 }
    279 
    280 #[cfg(test)]
    281 mod tests {
    282     use serde_json::json;
    283 
    284     use super::*;
    285 
    286     #[test]
    287     fn enum_strings_and_parse_errors_cover_all_model_variants() {
    288         for (variant, value) in [
    289             (LocalRecordFamily::LocalWork, "local_work"),
    290             (LocalRecordFamily::SignedEvent, "signed_event"),
    291         ] {
    292             assert_eq!(variant.as_str(), value);
    293             assert_eq!(
    294                 LocalRecordFamily::parse(value).expect("record family"),
    295                 variant
    296             );
    297         }
    298 
    299         for (variant, value) in [
    300             (LocalRecordStatus::LocalDraft, "local_draft"),
    301             (LocalRecordStatus::LocalSaved, "local_saved"),
    302             (LocalRecordStatus::PendingPublish, "pending_publish"),
    303             (LocalRecordStatus::Published, "published"),
    304             (LocalRecordStatus::Failed, "failed"),
    305             (LocalRecordStatus::Conflict, "conflict"),
    306         ] {
    307             assert_eq!(variant.as_str(), value);
    308             assert_eq!(
    309                 LocalRecordStatus::parse(value).expect("record status"),
    310                 variant
    311             );
    312         }
    313 
    314         for (variant, value) in [
    315             (PublishOutboxStatus::None, "none"),
    316             (PublishOutboxStatus::Pending, "pending"),
    317             (PublishOutboxStatus::Acknowledged, "acknowledged"),
    318             (PublishOutboxStatus::Failed, "failed"),
    319         ] {
    320             assert_eq!(variant.as_str(), value);
    321             assert_eq!(
    322                 PublishOutboxStatus::parse(value).expect("outbox status"),
    323                 variant
    324             );
    325         }
    326 
    327         for (variant, value) in [
    328             (SourceRuntime::Cli, "cli"),
    329             (SourceRuntime::App, "app"),
    330             (SourceRuntime::Network, "network"),
    331             (SourceRuntime::Service, "service"),
    332             (SourceRuntime::Worker, "worker"),
    333             (SourceRuntime::Test, "test"),
    334         ] {
    335             assert_eq!(variant.as_str(), value);
    336             assert_eq!(
    337                 SourceRuntime::parse(value).expect("source runtime"),
    338                 variant
    339             );
    340         }
    341 
    342         assert!(LocalRecordFamily::parse("other").is_err());
    343         assert!(LocalRecordStatus::parse("other").is_err());
    344         assert!(PublishOutboxStatus::parse("other").is_err());
    345         assert!(SourceRuntime::parse("other").is_err());
    346     }
    347 
    348     #[test]
    349     fn local_record_input_validation_covers_success_and_error_paths() {
    350         let mut local_work = local_work_input();
    351         local_work.validate().expect("valid local work");
    352 
    353         for (field, update) in [
    354             (
    355                 "owner_account_id",
    356                 Box::new(|input: &mut LocalEventRecordInput| {
    357                     input.owner_account_id = Some(" ".to_owned());
    358                 }) as Box<dyn Fn(&mut LocalEventRecordInput)>,
    359             ),
    360             (
    361                 "owner_pubkey",
    362                 Box::new(|input: &mut LocalEventRecordInput| {
    363                     input.owner_pubkey = Some(" ".to_owned());
    364                 }),
    365             ),
    366             (
    367                 "farm_id",
    368                 Box::new(|input: &mut LocalEventRecordInput| {
    369                     input.farm_id = Some(" ".to_owned());
    370                 }),
    371             ),
    372             (
    373                 "listing_addr",
    374                 Box::new(|input: &mut LocalEventRecordInput| {
    375                     input.listing_addr = Some(" ".to_owned());
    376                 }),
    377             ),
    378         ] {
    379             let mut input = local_work_input();
    380             update(&mut input);
    381             assert_error_contains(input.validate(), field);
    382         }
    383 
    384         local_work.record_id = " ".to_owned();
    385         assert_error_contains(local_work.validate(), "record_id");
    386 
    387         let mut missing_work = local_work_input();
    388         missing_work.local_work_json = None;
    389         assert_error_contains(missing_work.validate(), "local_work_json");
    390 
    391         let mut queued_work = local_work_input();
    392         queued_work.outbox_status = PublishOutboxStatus::Pending;
    393         assert_error_contains(queued_work.validate(), "outbox status none");
    394 
    395         let signed_event = signed_event_input();
    396         signed_event.validate().expect("valid signed event");
    397 
    398         for (field, update) in [
    399             (
    400                 "event_id",
    401                 Box::new(|input: &mut LocalEventRecordInput| {
    402                     input.event_id = Some(" ".to_owned());
    403                 }) as Box<dyn Fn(&mut LocalEventRecordInput)>,
    404             ),
    405             (
    406                 "event_pubkey",
    407                 Box::new(|input: &mut LocalEventRecordInput| {
    408                     input.event_pubkey = None;
    409                 }),
    410             ),
    411             (
    412                 "event_sig",
    413                 Box::new(|input: &mut LocalEventRecordInput| {
    414                     input.event_sig = None;
    415                 }),
    416             ),
    417             (
    418                 "event_kind",
    419                 Box::new(|input: &mut LocalEventRecordInput| {
    420                     input.event_kind = None;
    421                 }),
    422             ),
    423             (
    424                 "raw_event_json",
    425                 Box::new(|input: &mut LocalEventRecordInput| {
    426                     input.raw_event_json = None;
    427                 }),
    428             ),
    429         ] {
    430             let mut input = signed_event_input();
    431             update(&mut input);
    432             assert_error_contains(input.validate(), field);
    433         }
    434     }
    435 
    436     fn local_work_input() -> LocalEventRecordInput {
    437         LocalEventRecordInput {
    438             record_id: "local-work-a".to_owned(),
    439             family: LocalRecordFamily::LocalWork,
    440             status: LocalRecordStatus::LocalSaved,
    441             source_runtime: SourceRuntime::App,
    442             created_at_ms: 10,
    443             inserted_at_ms: 11,
    444             owner_account_id: Some("account-a".to_owned()),
    445             owner_pubkey: Some("pubkey-a".to_owned()),
    446             farm_id: Some("farm-a".to_owned()),
    447             listing_addr: Some("listing-a".to_owned()),
    448             local_work_json: Some(json!({"kind":"buyer_order_request_v1"})),
    449             event_id: None,
    450             event_kind: None,
    451             event_pubkey: None,
    452             event_created_at: None,
    453             event_tags_json: None,
    454             event_content: None,
    455             event_sig: None,
    456             raw_event_json: None,
    457             outbox_status: PublishOutboxStatus::None,
    458             relay_set_fingerprint: None,
    459             relay_delivery_json: None,
    460         }
    461     }
    462 
    463     fn signed_event_input() -> LocalEventRecordInput {
    464         LocalEventRecordInput {
    465             record_id: "signed-event-a".to_owned(),
    466             family: LocalRecordFamily::SignedEvent,
    467             status: LocalRecordStatus::PendingPublish,
    468             source_runtime: SourceRuntime::Service,
    469             created_at_ms: 20,
    470             inserted_at_ms: 21,
    471             owner_account_id: None,
    472             owner_pubkey: None,
    473             farm_id: None,
    474             listing_addr: None,
    475             local_work_json: None,
    476             event_id: Some("event-a".to_owned()),
    477             event_kind: Some(30402),
    478             event_pubkey: Some("pubkey-a".to_owned()),
    479             event_created_at: Some(20),
    480             event_tags_json: Some(json!([["d", "listing-a"]])),
    481             event_content: Some("{}".to_owned()),
    482             event_sig: Some("sig-a".to_owned()),
    483             raw_event_json: Some(json!({"id":"event-a"})),
    484             outbox_status: PublishOutboxStatus::Pending,
    485             relay_set_fingerprint: Some("relay-set-a".to_owned()),
    486             relay_delivery_json: Some(json!({"state":"pending"})),
    487         }
    488     }
    489 
    490     fn assert_error_contains(result: Result<(), LocalEventsError>, expected: &str) {
    491         let err = result.expect_err("validation error");
    492         assert!(
    493             err.to_string().contains(expected),
    494             "expected error to contain {expected}, got {err}"
    495         );
    496     }
    497 }