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