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 }