use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; #[derive(Debug, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum RecordAction { Create, Update, Delete, } #[derive(Debug, Deserialize, Serialize)] struct InnerRecordEvent { did: String, rev: String, collection: String, rkey: String, action: RecordAction, record: Option>, cid: Option, live: bool, } #[derive(Debug, Deserialize, Serialize)] pub struct RecordEvent { pub id: u64, pub did: String, pub rev: String, pub collection: String, pub rkey: String, pub action: RecordAction, pub record: Option>, pub cid: Option, pub live: bool, } #[derive(Debug, Deserialize, Serialize, sqlx::Type)] #[serde(rename_all = "lowercase")] #[sqlx(type_name = "identity_status", rename_all = "lowercase")] pub enum IdentityStatus { Active, Takendown, Suspended, Deactivated, Deleted, } #[derive(Debug, Deserialize, Serialize)] struct InnerIdentityEvent { did: String, handle: String, is_active: bool, status: IdentityStatus, } #[derive(Debug, Deserialize, Serialize)] pub struct IdentityEvent { pub id: u64, pub did: String, pub handle: String, pub is_active: bool, pub status: IdentityStatus, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum EventType { Record, Identity, } #[derive(Debug, Deserialize, Serialize)] struct InnerEvent { id: u64, r#type: EventType, #[serde(skip_serializing_if = "Option::is_none")] record: Option, #[serde(skip_serializing_if = "Option::is_none")] identity: Option, } #[derive(Debug, Deserialize, Serialize)] #[serde(try_from = "InnerEvent")] pub enum TapEvent { Record(RecordEvent), Identity(IdentityEvent), } impl TapEvent { pub const fn id(&self) -> u64 { match self { Self::Record(record) => record.id, Self::Identity(identity) => identity.id, } } } #[derive(Debug, thiserror::Error)] enum EventError { #[error("event with 'record' type should have 'record' property set")] RecordEventMissingRecord, #[error("event with 'record' type should not have 'identity' property set")] RecordEventWithIdentity, #[error("event with 'identity' type should have 'identity' property set")] IdentityEventMissingIdentity, #[error("event with 'identity' type should not have 'record' property set")] IdentityEventWithRecord, } impl TryFrom for TapEvent { type Error = EventError; fn try_from(value: InnerEvent) -> Result { match (value.r#type, value.record, value.identity) { (EventType::Record, None, _) => Err(EventError::RecordEventMissingRecord), (EventType::Record, _, Some(_)) => Err(EventError::RecordEventWithIdentity), (EventType::Record, Some(record), None) => { let InnerRecordEvent { did, rev, collection, rkey, action, record, cid, live, } = record; Ok(Self::Record(RecordEvent { id: value.id, did, rev, collection, rkey, action, record, cid, live, })) } (EventType::Identity, _, None) => Err(EventError::IdentityEventMissingIdentity), (EventType::Identity, Some(_), _) => Err(EventError::IdentityEventWithRecord), (EventType::Identity, None, Some(identity)) => { let InnerIdentityEvent { did, handle, is_active, status, } = identity; Ok(Self::Identity(IdentityEvent { id: value.id, did, handle, is_active, status, })) } } } } #[derive(Debug, Deserialize, Serialize)] pub struct RepoInfo { pub did: String, pub handle: String, pub state: String, pub rev: String, pub records: u64, pub error: Option, pub retries: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn deserialized_record_event() { const SAMPLE: &str = r#"{"id":24431,"type":"record","record":{"live":false,"did":"did:plc:jlplwn5pi4dqrls7i6dx2me7","rev":"3m7xjcvqmch2k","collection":"sh.tangled.repo.issue.comment","rkey":"3lxy6urnngt22","action":"create","record":{"$type":"sh.tangled.repo.issue.comment","body":"x","commentId":66128,"createdAt":"2025-09-04T03:20:53Z","issue":"at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.issue/3ljnffq4axj22","owner":"did:plc:jlplwn5pi4dqrls7i6dx2me7","repo":"at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.repo/3liuighjy2h22"},"cid":"bafyreielwpdea6mnhgqz2r4yfyr5ne6v6h2ucncm46enukpqlclrned5ia"}}"#; let event: TapEvent = serde_json::from_str(SAMPLE).unwrap(); assert!(matches!(event, TapEvent::Record(_))); } }