1use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use systemprompt_identifiers::{
12 ContentHash, DocumentCommentId, DocumentEditSessionId, DocumentId, DocumentNodeId,
13 DocumentProposalId, DocumentRevisionId, DriveDeviceId, UserId,
14};
15
16use super::access::{AccessLevel, ActorKind, ProposalStatus, RevisionOriginKind};
17use super::drive::{PresenceSource, PresenceState};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
20pub enum DocumentEventName {
21 #[serde(rename = "document.revision.created")]
22 RevisionCreated,
23 #[serde(rename = "document.presence")]
24 Presence,
25 #[serde(rename = "document.lock")]
26 Lock,
27 #[serde(rename = "document.conflict")]
28 Conflict,
29 #[serde(rename = "document.collab.update")]
30 CollabUpdate,
31 #[serde(rename = "document.comment.created")]
32 CommentCreated,
33 #[serde(rename = "document.comment.resolved")]
34 CommentResolved,
35 #[serde(rename = "document.shared")]
36 Shared,
37 #[serde(rename = "document.ownership_transferred")]
38 OwnershipTransferred,
39 #[serde(rename = "document.proposal.opened")]
40 ProposalOpened,
41 #[serde(rename = "document.proposal.accepted")]
42 ProposalAccepted,
43 #[serde(rename = "document.proposal.rejected")]
44 ProposalRejected,
45 #[serde(rename = "document.proposal.superseded")]
46 ProposalSuperseded,
47}
48
49impl DocumentEventName {
50 pub const ALL: [Self; 13] = [
51 Self::RevisionCreated,
52 Self::Presence,
53 Self::Lock,
54 Self::Conflict,
55 Self::CollabUpdate,
56 Self::CommentCreated,
57 Self::CommentResolved,
58 Self::Shared,
59 Self::OwnershipTransferred,
60 Self::ProposalOpened,
61 Self::ProposalAccepted,
62 Self::ProposalRejected,
63 Self::ProposalSuperseded,
64 ];
65
66 #[must_use]
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::RevisionCreated => DocumentRevisionCreatedEvent::NAME,
70 Self::Presence => DocumentPresenceEvent::NAME,
71 Self::Lock => DocumentLockEvent::NAME,
72 Self::Conflict => DocumentConflictEvent::NAME,
73 Self::CollabUpdate => DocumentCollabUpdateEvent::NAME,
74 Self::CommentCreated => DocumentCommentCreatedEvent::NAME,
75 Self::CommentResolved => DocumentCommentResolvedEvent::NAME,
76 Self::Shared => DocumentSharedEvent::NAME,
77 Self::OwnershipTransferred => DocumentOwnershipTransferredEvent::NAME,
78 Self::ProposalOpened => DocumentProposalEvent::OPENED,
79 Self::ProposalAccepted => DocumentProposalEvent::ACCEPTED,
80 Self::ProposalRejected => DocumentProposalEvent::REJECTED,
81 Self::ProposalSuperseded => DocumentProposalEvent::SUPERSEDED,
82 }
83 }
84
85 #[must_use]
86 pub fn from_name(name: &str) -> Option<Self> {
87 Self::ALL.into_iter().find(|event| event.as_str() == name)
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92pub struct DocumentRevisionCreatedEvent {
93 pub document_id: DocumentId,
94 pub revision_id: DocumentRevisionId,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub parent_revision_id: Option<DocumentRevisionId>,
97 pub content_sha256: ContentHash,
98 pub author_id: UserId,
99 pub actor_kind: ActorKind,
100 pub origin_kind: RevisionOriginKind,
101}
102
103impl DocumentRevisionCreatedEvent {
104 pub const NAME: &'static str = "document.revision.created";
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108pub struct DocumentPresenceEvent {
109 pub document_id: DocumentId,
110 pub user_id: UserId,
111 pub state: PresenceState,
112 pub source_kind: PresenceSource,
113 pub source_ref: String,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub cursor: Option<serde_json::Value>,
117}
118
119impl DocumentPresenceEvent {
120 pub const NAME: &'static str = "document.presence";
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
124pub struct DocumentLockEvent {
125 pub document_id: DocumentId,
126 pub locked: bool,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub holder_id: Option<UserId>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub edit_session_id: Option<DocumentEditSessionId>,
131}
132
133impl DocumentLockEvent {
134 pub const NAME: &'static str = "document.lock";
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
138pub struct DocumentConflictEvent {
139 pub document_id: DocumentId,
140 pub path: String,
141 pub head_revision_id: DocumentRevisionId,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub base_revision_id: Option<DocumentRevisionId>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub device_id: Option<DriveDeviceId>,
146}
147
148impl DocumentConflictEvent {
149 pub const NAME: &'static str = "document.conflict";
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
153pub struct DocumentCollabUpdateEvent {
154 pub document_id: DocumentId,
155 pub edit_session_id: DocumentEditSessionId,
156 pub seq: i64,
157 pub update_b64: String,
158 pub author_id: UserId,
159}
160
161impl DocumentCollabUpdateEvent {
162 pub const NAME: &'static str = "document.collab.update";
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
166#[expect(
167 clippy::struct_field_names,
168 reason = "the document event wire vocabulary names every reference document_id, comment_id, \
169 node_id, parent_id and author_id, as the sibling events do"
170)]
171pub struct DocumentCommentCreatedEvent {
172 pub document_id: DocumentId,
173 pub comment_id: DocumentCommentId,
174 pub node_id: DocumentNodeId,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub parent_id: Option<DocumentCommentId>,
177 pub author_id: UserId,
178}
179
180impl DocumentCommentCreatedEvent {
181 pub const NAME: &'static str = "document.comment.created";
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
185pub struct DocumentCommentResolvedEvent {
186 pub document_id: DocumentId,
187 pub comment_id: DocumentCommentId,
188 pub resolved_by: UserId,
189}
190
191impl DocumentCommentResolvedEvent {
192 pub const NAME: &'static str = "document.comment.resolved";
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
196pub struct DocumentSharedEvent {
197 pub document_id: DocumentId,
198 pub subject_kind: String,
199 pub subject_value: String,
200 pub level: AccessLevel,
201 pub by: UserId,
202}
203
204impl DocumentSharedEvent {
205 pub const NAME: &'static str = "document.shared";
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct DocumentOwnershipTransferredEvent {
210 pub document_id: DocumentId,
211 pub from: UserId,
212 pub to: UserId,
213}
214
215impl DocumentOwnershipTransferredEvent {
216 pub const NAME: &'static str = "document.ownership_transferred";
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
222pub struct DocumentProposalEvent {
223 pub document_id: DocumentId,
224 pub proposal_id: DocumentProposalId,
225 pub status: ProposalStatus,
226 pub proposer_id: UserId,
227 pub base_revision_id: DocumentRevisionId,
228 pub proposed_revision_id: DocumentRevisionId,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub reviewer_id: Option<UserId>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub landed_revision_id: Option<DocumentRevisionId>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub message: Option<String>,
235}
236
237impl DocumentProposalEvent {
238 pub const OPENED: &'static str = "document.proposal.opened";
239 pub const ACCEPTED: &'static str = "document.proposal.accepted";
240 pub const REJECTED: &'static str = "document.proposal.rejected";
241 pub const SUPERSEDED: &'static str = "document.proposal.superseded";
242
243 #[must_use]
244 pub const fn name_of(status: ProposalStatus) -> &'static str {
245 match status {
246 ProposalStatus::Open => Self::OPENED,
247 ProposalStatus::Accepted => Self::ACCEPTED,
248 ProposalStatus::Rejected => Self::REJECTED,
249 ProposalStatus::Superseded => Self::SUPERSEDED,
250 }
251 }
252}