1use serde::{Deserialize, Serialize};
2use serde_json::value::RawValue;
3
4#[derive(Debug, Hash, Deserialize, Serialize)]
5#[serde(rename_all = "lowercase")]
6pub enum RecordAction {
7 Create,
8 Update,
9 Delete,
10}
11
12#[derive(Debug, Deserialize, Serialize)]
13struct InnerRecordEvent {
14 did: String,
15 rev: String,
16 collection: String,
17 rkey: String,
18 action: RecordAction,
19 record: Option<Box<RawValue>>,
20 cid: Option<String>,
21 live: bool,
22}
23
24#[derive(Debug, Deserialize, Serialize)]
25pub struct RecordEvent {
26 pub id: u64,
27 pub did: String,
28 pub rev: String,
29 pub collection: String,
30 pub rkey: String,
31 pub action: RecordAction,
32 pub record: Option<Box<RawValue>>,
33 pub cid: Option<String>,
34 pub live: bool,
35}
36
37#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
38#[serde(rename_all = "lowercase")]
39#[sqlx(type_name = "identity_status", rename_all = "lowercase")]
40pub enum IdentityStatus {
41 Active,
42 Takendown,
43 Suspended,
44 Deactivated,
45 Deleted,
46}
47
48#[derive(Debug, Deserialize, Serialize)]
49struct InnerIdentityEvent {
50 did: String,
51 handle: String,
52 is_active: bool,
53 status: IdentityStatus,
54}
55
56#[derive(Debug, Deserialize, Serialize)]
57pub struct IdentityEvent {
58 pub id: u64,
59 pub did: String,
60 pub handle: String,
61 pub is_active: bool,
62 pub status: IdentityStatus,
63}
64
65#[derive(Debug, Deserialize, Serialize)]
66#[serde(rename_all = "snake_case")]
67pub enum EventType {
68 Record,
69 Identity,
70}
71
72#[derive(Debug, Deserialize, Serialize)]
73struct InnerEvent {
74 id: u64,
75 r#type: EventType,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 record: Option<InnerRecordEvent>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 identity: Option<InnerIdentityEvent>,
80}
81
82#[derive(Debug, Deserialize, Serialize)]
83#[serde(try_from = "InnerEvent")]
84pub enum TapEvent {
85 Record(RecordEvent),
86 Identity(IdentityEvent),
87}
88
89impl TapEvent {
90 pub const fn id(&self) -> u64 {
91 match self {
92 Self::Record(record) => record.id,
93 Self::Identity(identity) => identity.id,
94 }
95 }
96}
97
98#[derive(Debug, thiserror::Error)]
99enum EventError {
100 #[error("event with 'record' type should have 'record' property set")]
101 RecordEventMissingRecord,
102 #[error("event with 'record' type should not have 'identity' property set")]
103 RecordEventWithIdentity,
104 #[error("event with 'identity' type should have 'identity' property set")]
105 IdentityEventMissingIdentity,
106 #[error("event with 'identity' type should not have 'record' property set")]
107 IdentityEventWithRecord,
108}
109
110impl TryFrom<InnerEvent> for TapEvent {
111 type Error = EventError;
112
113 fn try_from(value: InnerEvent) -> Result<Self, Self::Error> {
114 match (value.r#type, value.record, value.identity) {
115 (EventType::Record, None, _) => Err(EventError::RecordEventMissingRecord),
116 (EventType::Record, _, Some(_)) => Err(EventError::RecordEventWithIdentity),
117 (EventType::Record, Some(record), None) => {
118 let InnerRecordEvent {
119 did,
120 rev,
121 collection,
122 rkey,
123 action,
124 record,
125 cid,
126 live,
127 } = record;
128 Ok(Self::Record(RecordEvent {
129 id: value.id,
130 did,
131 rev,
132 collection,
133 rkey,
134 action,
135 record,
136 cid,
137 live,
138 }))
139 }
140 (EventType::Identity, _, None) => Err(EventError::IdentityEventMissingIdentity),
141 (EventType::Identity, Some(_), _) => Err(EventError::IdentityEventWithRecord),
142 (EventType::Identity, None, Some(identity)) => {
143 let InnerIdentityEvent {
144 did,
145 handle,
146 is_active,
147 status,
148 } = identity;
149 Ok(Self::Identity(IdentityEvent {
150 id: value.id,
151 did,
152 handle,
153 is_active,
154 status,
155 }))
156 }
157 }
158 }
159}
160
161#[derive(Debug, Deserialize, Serialize)]
162pub struct RepoInfo {
163 pub did: String,
164 pub handle: String,
165 pub state: String,
166 pub rev: String,
167 pub records: u64,
168 pub error: Option<String>,
169 pub retries: Option<u64>,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn deserialized_record_event() {
178 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"}}"#;
179 let event: TapEvent = serde_json::from_str(SAMPLE).unwrap();
180 assert!(matches!(event, TapEvent::Record(_)));
181 }
182}