1use serde::{Deserialize, Serialize};
4
5use super::CollaborationCodecError;
6use crate::object::{
7 AnnotationKind, Attribution, ChangeId, ContentHash, StateId, VisibilityTier,
8 collaboration::{
9 COLLABORATION_OPERATION_SCHEMA_VERSION, CollabOpId, CollaborationAnchor,
10 CollaborationIdempotencyKey, CollaborationOperationBodyV1, CollaborationOperationEnvelope,
11 CollaborationResolution, DiscussionRecordId, DiscussionTurnV1, LegacyDiscussionId,
12 LegacyDiscussionResolutionV1, LegacySourceLocator,
13 },
14};
15
16#[derive(Serialize, Deserialize)]
17struct WireOperationV1 {
18 schema_version: u16,
19 discussion_id: DiscussionRecordId,
20 parents: Vec<CollabOpId>,
21 idempotency_key: CollaborationIdempotencyKey,
22 author: Attribution,
23 occurred_at_ms: i64,
24 body: WireBodyV1,
25}
26
27#[derive(Serialize, Deserialize)]
28#[serde(rename_all = "snake_case", tag = "kind")]
29enum WireAnchorV1 {
30 Repository,
31 State {
32 state_id: StateId,
33 },
34 Change {
35 change_id: ChangeId,
36 },
37 Path {
38 state_id: StateId,
39 path: String,
40 },
41 Symbol {
42 state_id: StateId,
43 path: String,
44 symbol: String,
45 },
46}
47
48#[derive(Serialize, Deserialize)]
49struct WireTurnV1 {
50 body: String,
51 content_hash: ContentHash,
52}
53
54#[derive(Serialize, Deserialize)]
55#[serde(rename_all = "snake_case", tag = "kind")]
56enum WireResolutionV1 {
57 AddressedByState {
58 state_id: StateId,
59 },
60 AddressedByChange {
61 change_id: ChangeId,
62 },
63 Dismissed {
64 reason: String,
65 },
66 IntoAnnotation {
67 annotation_kind: AnnotationKind,
68 content: String,
69 tags: Vec<String>,
70 },
71 Annotation {
72 annotation_id: String,
73 },
74}
75
76#[derive(Serialize, Deserialize)]
77#[serde(rename_all = "snake_case", tag = "kind")]
78enum WireLegacyResolutionV1 {
79 Open,
80 AddressedByState { state_id: StateId },
81 Dismissed { reason: String },
82 Annotation { annotation_id: String },
83}
84
85#[derive(Serialize, Deserialize)]
86#[serde(rename_all = "snake_case", tag = "kind")]
87enum WireBodyV1 {
88 Open {
89 title: String,
90 anchor: WireAnchorV1,
91 visibility: VisibilityTier,
92 turn: WireTurnV1,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 thread_ref: Option<String>,
95 },
96 AppendTurn {
97 turn: WireTurnV1,
98 },
99 Resolve {
100 resolution: WireResolutionV1,
101 },
102 Reopen {
103 reason: String,
104 },
105 ResolveConflict {
106 competing: Vec<CollabOpId>,
107 selected: CollabOpId,
108 },
109 LegacyImported {
110 source: LegacySourceLocator,
111 legacy_discussion_id: LegacyDiscussionId,
112 aliases: Vec<LegacySourceLocator>,
113 title: String,
114 anchor: WireAnchorV1,
115 visibility: VisibilityTier,
116 turns: Vec<WireTurnV1>,
117 resolution: WireLegacyResolutionV1,
118 },
119}
120
121pub(super) fn encode(
122 operation: &CollaborationOperationEnvelope,
123) -> Result<Vec<u8>, CollaborationCodecError> {
124 let wire = WireOperationV1 {
125 schema_version: COLLABORATION_OPERATION_SCHEMA_VERSION,
126 discussion_id: operation.discussion_id,
127 parents: operation.parents.clone(),
128 idempotency_key: operation.idempotency_key.clone(),
129 author: operation.author.clone(),
130 occurred_at_ms: operation.occurred_at_ms,
131 body: operation.body.clone().into(),
132 };
133 rmp_serde::to_vec_named(&wire)
134 .map_err(|error| CollaborationCodecError::Encoding(error.to_string()))
135}
136
137pub(super) fn decode(
138 bytes: &[u8],
139) -> Result<CollaborationOperationEnvelope, CollaborationCodecError> {
140 let wire: WireOperationV1 = rmp_serde::from_slice(bytes)
141 .map_err(|error| CollaborationCodecError::Decoding(error.to_string()))?;
142 Ok(CollaborationOperationEnvelope {
143 discussion_id: wire.discussion_id,
144 parents: wire.parents,
145 idempotency_key: wire.idempotency_key,
146 author: wire.author,
147 occurred_at_ms: wire.occurred_at_ms,
148 body: wire.body.into(),
149 })
150}
151
152impl From<CollaborationAnchor> for WireAnchorV1 {
153 fn from(value: CollaborationAnchor) -> Self {
154 match value {
155 CollaborationAnchor::Repository => Self::Repository,
156 CollaborationAnchor::State { state_id } => Self::State { state_id },
157 CollaborationAnchor::Change { change_id } => Self::Change { change_id },
158 CollaborationAnchor::Path { state_id, path } => Self::Path { state_id, path },
159 CollaborationAnchor::Symbol {
160 state_id,
161 path,
162 symbol,
163 } => Self::Symbol {
164 state_id,
165 path,
166 symbol,
167 },
168 }
169 }
170}
171
172impl From<WireAnchorV1> for CollaborationAnchor {
173 fn from(value: WireAnchorV1) -> Self {
174 match value {
175 WireAnchorV1::Repository => Self::Repository,
176 WireAnchorV1::State { state_id } => Self::State { state_id },
177 WireAnchorV1::Change { change_id } => Self::Change { change_id },
178 WireAnchorV1::Path { state_id, path } => Self::Path { state_id, path },
179 WireAnchorV1::Symbol {
180 state_id,
181 path,
182 symbol,
183 } => Self::Symbol {
184 state_id,
185 path,
186 symbol,
187 },
188 }
189 }
190}
191
192impl From<DiscussionTurnV1> for WireTurnV1 {
193 fn from(value: DiscussionTurnV1) -> Self {
194 Self {
195 body: value.body,
196 content_hash: value.content_hash,
197 }
198 }
199}
200
201impl From<WireTurnV1> for DiscussionTurnV1 {
202 fn from(value: WireTurnV1) -> Self {
203 Self {
204 body: value.body,
205 content_hash: value.content_hash,
206 }
207 }
208}
209
210impl From<CollaborationResolution> for WireResolutionV1 {
211 fn from(value: CollaborationResolution) -> Self {
212 match value {
213 CollaborationResolution::AddressedByState { state_id } => {
214 Self::AddressedByState { state_id }
215 }
216 CollaborationResolution::AddressedByChange { change_id } => {
217 Self::AddressedByChange { change_id }
218 }
219 CollaborationResolution::Dismissed { reason } => Self::Dismissed { reason },
220 CollaborationResolution::IntoAnnotation {
221 annotation_kind,
222 content,
223 tags,
224 } => Self::IntoAnnotation {
225 annotation_kind,
226 content,
227 tags,
228 },
229 CollaborationResolution::Annotation { annotation_id } => {
230 Self::Annotation { annotation_id }
231 }
232 }
233 }
234}
235
236impl From<WireResolutionV1> for CollaborationResolution {
237 fn from(value: WireResolutionV1) -> Self {
238 match value {
239 WireResolutionV1::AddressedByState { state_id } => Self::AddressedByState { state_id },
240 WireResolutionV1::AddressedByChange { change_id } => {
241 Self::AddressedByChange { change_id }
242 }
243 WireResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
244 WireResolutionV1::IntoAnnotation {
245 annotation_kind,
246 content,
247 tags,
248 } => Self::IntoAnnotation {
249 annotation_kind,
250 content,
251 tags,
252 },
253 WireResolutionV1::Annotation { annotation_id } => Self::Annotation { annotation_id },
254 }
255 }
256}
257
258impl From<LegacyDiscussionResolutionV1> for WireLegacyResolutionV1 {
259 fn from(value: LegacyDiscussionResolutionV1) -> Self {
260 match value {
261 LegacyDiscussionResolutionV1::Open => Self::Open,
262 LegacyDiscussionResolutionV1::AddressedByState { state_id } => {
263 Self::AddressedByState { state_id }
264 }
265 LegacyDiscussionResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
266 LegacyDiscussionResolutionV1::Annotation { annotation_id } => {
267 Self::Annotation { annotation_id }
268 }
269 }
270 }
271}
272
273impl From<WireLegacyResolutionV1> for LegacyDiscussionResolutionV1 {
274 fn from(value: WireLegacyResolutionV1) -> Self {
275 match value {
276 WireLegacyResolutionV1::Open => Self::Open,
277 WireLegacyResolutionV1::AddressedByState { state_id } => {
278 Self::AddressedByState { state_id }
279 }
280 WireLegacyResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
281 WireLegacyResolutionV1::Annotation { annotation_id } => {
282 Self::Annotation { annotation_id }
283 }
284 }
285 }
286}
287
288impl From<CollaborationOperationBodyV1> for WireBodyV1 {
289 fn from(value: CollaborationOperationBodyV1) -> Self {
290 match value {
291 CollaborationOperationBodyV1::Open {
292 title,
293 anchor,
294 visibility,
295 turn,
296 thread_ref,
297 } => Self::Open {
298 title,
299 anchor: anchor.into(),
300 visibility,
301 turn: turn.into(),
302 thread_ref,
303 },
304 CollaborationOperationBodyV1::AppendTurn { turn } => {
305 Self::AppendTurn { turn: turn.into() }
306 }
307 CollaborationOperationBodyV1::Resolve { resolution } => Self::Resolve {
308 resolution: resolution.into(),
309 },
310 CollaborationOperationBodyV1::Reopen { reason } => Self::Reopen { reason },
311 CollaborationOperationBodyV1::ResolveConflict {
312 competing,
313 selected,
314 } => Self::ResolveConflict {
315 competing,
316 selected,
317 },
318 CollaborationOperationBodyV1::LegacyImported {
319 source,
320 legacy_discussion_id,
321 aliases,
322 title,
323 anchor,
324 visibility,
325 turns,
326 resolution,
327 } => Self::LegacyImported {
328 source,
329 legacy_discussion_id,
330 aliases,
331 title,
332 anchor: anchor.into(),
333 visibility,
334 turns: turns.into_iter().map(Into::into).collect(),
335 resolution: resolution.into(),
336 },
337 }
338 }
339}
340
341impl From<WireBodyV1> for CollaborationOperationBodyV1 {
342 fn from(value: WireBodyV1) -> Self {
343 match value {
344 WireBodyV1::Open {
345 title,
346 anchor,
347 visibility,
348 turn,
349 thread_ref,
350 } => Self::Open {
351 title,
352 anchor: anchor.into(),
353 visibility,
354 turn: turn.into(),
355 thread_ref,
356 },
357 WireBodyV1::AppendTurn { turn } => Self::AppendTurn { turn: turn.into() },
358 WireBodyV1::Resolve { resolution } => Self::Resolve {
359 resolution: resolution.into(),
360 },
361 WireBodyV1::Reopen { reason } => Self::Reopen { reason },
362 WireBodyV1::ResolveConflict {
363 competing,
364 selected,
365 } => Self::ResolveConflict {
366 competing,
367 selected,
368 },
369 WireBodyV1::LegacyImported {
370 source,
371 legacy_discussion_id,
372 aliases,
373 title,
374 anchor,
375 visibility,
376 turns,
377 resolution,
378 } => Self::LegacyImported {
379 source,
380 legacy_discussion_id,
381 aliases,
382 title,
383 anchor: anchor.into(),
384 visibility,
385 turns: turns.into_iter().map(Into::into).collect(),
386 resolution: resolution.into(),
387 },
388 }
389 }
390}