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 },
114 AppendTurn {
115 turn: WireTurnV1,
116 },
117 RebindAnchor {
118 anchor: WireAnchorV1,
119 status: WireAnchorStatusV1,
120 body_changed_since_open: bool,
121 },
122 Resolve {
123 resolution: WireResolutionV1,
124 },
125 Reopen {
126 reason: String,
127 },
128 ResolveConflict {
129 competing: Vec<CollabOpId>,
130 selected: CollabOpId,
131 },
132 LegacyImported {
133 source: LegacySourceLocator,
134 legacy_discussion_id: LegacyDiscussionId,
135 aliases: Vec<LegacySourceLocator>,
136 title: String,
137 anchor: WireAnchorV1,
138 visibility: VisibilityTier,
139 turns: Vec<WireTurnV1>,
140 resolution: WireLegacyResolutionV1,
141 },
142}
143
144pub(super) fn encode(
145 operation: &CollaborationOperationEnvelope,
146) -> Result<Vec<u8>, CollaborationCodecError> {
147 let wire = WireOperationV2 {
148 schema_version: COLLABORATION_OPERATION_SCHEMA_VERSION,
149 metadata: operation.metadata.clone(),
150 discussion_id: operation.discussion_id,
151 parents: operation.parents.clone(),
152 idempotency_key: operation.idempotency_key.clone(),
153 author: operation.author.clone(),
154 occurred_at_ms: operation.occurred_at_ms,
155 body: operation.body.clone().into(),
156 };
157 rmp_serde::to_vec_named(&wire)
158 .map_err(|error| CollaborationCodecError::Encoding(error.to_string()))
159}
160
161pub(super) fn decode(
162 bytes: &[u8],
163) -> Result<CollaborationOperationEnvelope, CollaborationCodecError> {
164 let wire: WireOperationV2 = rmp_serde::from_slice(bytes)
165 .map_err(|error| CollaborationCodecError::Decoding(error.to_string()))?;
166 Ok(CollaborationOperationEnvelope {
167 metadata: wire.metadata,
168 discussion_id: wire.discussion_id,
169 parents: wire.parents,
170 idempotency_key: wire.idempotency_key,
171 author: wire.author,
172 occurred_at_ms: wire.occurred_at_ms,
173 body: wire.body.into(),
174 canonical_body: CanonicalBody::default(),
175 })
176}
177
178impl From<CollaborationAnchor> for WireAnchorV1 {
179 fn from(value: CollaborationAnchor) -> Self {
180 match value {
181 CollaborationAnchor::Source { source } => Self::Source { source },
182 CollaborationAnchor::Repository => Self::Repository,
183 CollaborationAnchor::State { state_id } => Self::State { state_id },
184 CollaborationAnchor::Change { change_id } => Self::Change { change_id },
185 CollaborationAnchor::Path { state_id, path } => Self::Path { state_id, path },
186 CollaborationAnchor::Symbol {
187 state_id,
188 path,
189 symbol,
190 } => Self::Symbol {
191 state_id,
192 path,
193 symbol,
194 },
195 }
196 }
197}
198
199impl From<WireAnchorV1> for CollaborationAnchor {
200 fn from(value: WireAnchorV1) -> Self {
201 match value {
202 WireAnchorV1::Source { source } => Self::Source { source },
203 WireAnchorV1::Repository => Self::Repository,
204 WireAnchorV1::State { state_id } => Self::State { state_id },
205 WireAnchorV1::Change { change_id } => Self::Change { change_id },
206 WireAnchorV1::Path { state_id, path } => Self::Path { state_id, path },
207 WireAnchorV1::Symbol {
208 state_id,
209 path,
210 symbol,
211 } => Self::Symbol {
212 state_id,
213 path,
214 symbol,
215 },
216 }
217 }
218}
219
220impl From<CollaborationAnchorStatus> for WireAnchorStatusV1 {
221 fn from(value: CollaborationAnchorStatus) -> Self {
222 match value {
223 CollaborationAnchorStatus::Current => Self::Current,
224 CollaborationAnchorStatus::Moved => Self::Moved,
225 CollaborationAnchorStatus::Ambiguous => Self::Ambiguous,
226 CollaborationAnchorStatus::Orphaned => Self::Orphaned,
227 }
228 }
229}
230
231impl From<WireAnchorStatusV1> for CollaborationAnchorStatus {
232 fn from(value: WireAnchorStatusV1) -> Self {
233 match value {
234 WireAnchorStatusV1::Current => Self::Current,
235 WireAnchorStatusV1::Moved => Self::Moved,
236 WireAnchorStatusV1::Ambiguous => Self::Ambiguous,
237 WireAnchorStatusV1::Orphaned => Self::Orphaned,
238 }
239 }
240}
241
242impl From<DiscussionTurnV1> for WireTurnV1 {
243 fn from(value: DiscussionTurnV1) -> Self {
244 Self {
245 body: value.body,
246 content_hash: value.content_hash,
247 }
248 }
249}
250
251impl From<WireTurnV1> for DiscussionTurnV1 {
252 fn from(value: WireTurnV1) -> Self {
253 Self {
254 body: value.body,
255 content_hash: value.content_hash,
256 }
257 }
258}
259
260impl From<CollaborationResolution> for WireResolutionV1 {
261 fn from(value: CollaborationResolution) -> Self {
262 match value {
263 CollaborationResolution::AddressedByState { state_id } => {
264 Self::AddressedByState { state_id }
265 }
266 CollaborationResolution::AddressedByChange { change_id } => {
267 Self::AddressedByChange { change_id }
268 }
269 CollaborationResolution::Dismissed { reason } => Self::Dismissed { reason },
270 CollaborationResolution::IntoContext { context } => Self::IntoContext { context },
271 CollaborationResolution::IntoAnnotation {
272 annotation_kind,
273 content,
274 tags,
275 } => Self::IntoAnnotation {
276 annotation_kind,
277 content,
278 tags,
279 },
280 CollaborationResolution::Annotation { annotation_id } => {
281 Self::Annotation { annotation_id }
282 }
283 }
284 }
285}
286
287impl From<WireResolutionV1> for CollaborationResolution {
288 fn from(value: WireResolutionV1) -> Self {
289 match value {
290 WireResolutionV1::AddressedByState { state_id } => Self::AddressedByState { state_id },
291 WireResolutionV1::AddressedByChange { change_id } => {
292 Self::AddressedByChange { change_id }
293 }
294 WireResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
295 WireResolutionV1::IntoContext { context } => Self::IntoContext { context },
296 WireResolutionV1::IntoAnnotation {
297 annotation_kind,
298 content,
299 tags,
300 } => Self::IntoAnnotation {
301 annotation_kind,
302 content,
303 tags,
304 },
305 WireResolutionV1::Annotation { annotation_id } => Self::Annotation { annotation_id },
306 }
307 }
308}
309
310impl From<LegacyDiscussionResolutionV1> for WireLegacyResolutionV1 {
311 fn from(value: LegacyDiscussionResolutionV1) -> Self {
312 match value {
313 LegacyDiscussionResolutionV1::Open => Self::Open,
314 LegacyDiscussionResolutionV1::AddressedByState { state_id } => {
315 Self::AddressedByState { state_id }
316 }
317 LegacyDiscussionResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
318 LegacyDiscussionResolutionV1::Annotation { annotation_id } => {
319 Self::Annotation { annotation_id }
320 }
321 }
322 }
323}
324
325impl From<WireLegacyResolutionV1> for LegacyDiscussionResolutionV1 {
326 fn from(value: WireLegacyResolutionV1) -> Self {
327 match value {
328 WireLegacyResolutionV1::Open => Self::Open,
329 WireLegacyResolutionV1::AddressedByState { state_id } => {
330 Self::AddressedByState { state_id }
331 }
332 WireLegacyResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
333 WireLegacyResolutionV1::Annotation { annotation_id } => {
334 Self::Annotation { annotation_id }
335 }
336 }
337 }
338}
339
340impl From<CollaborationOperationBodyV1> for WireBodyV1 {
341 fn from(value: CollaborationOperationBodyV1) -> Self {
342 match value {
343 CollaborationOperationBodyV1::Open {
344 blocking,
345 title,
346 anchor,
347 visibility,
348 turn,
349 thread_ref,
350 } => Self::Open {
351 blocking,
352 title,
353 anchor: anchor.into(),
354 visibility,
355 turn: turn.into(),
356 thread_ref,
357 },
358 CollaborationOperationBodyV1::AppendTurn { turn } => {
359 Self::AppendTurn { turn: turn.into() }
360 }
361 CollaborationOperationBodyV1::RebindAnchor {
362 anchor,
363 status,
364 body_changed_since_open,
365 } => Self::RebindAnchor {
366 anchor: anchor.into(),
367 status: status.into(),
368 body_changed_since_open,
369 },
370 CollaborationOperationBodyV1::Resolve { resolution } => Self::Resolve {
371 resolution: resolution.into(),
372 },
373 CollaborationOperationBodyV1::Reopen { reason } => Self::Reopen { reason },
374 CollaborationOperationBodyV1::ResolveConflict {
375 competing,
376 selected,
377 } => Self::ResolveConflict {
378 competing,
379 selected,
380 },
381 CollaborationOperationBodyV1::LegacyImported {
382 source,
383 legacy_discussion_id,
384 aliases,
385 title,
386 anchor,
387 visibility,
388 turns,
389 resolution,
390 } => Self::LegacyImported {
391 source,
392 legacy_discussion_id,
393 aliases,
394 title,
395 anchor: anchor.into(),
396 visibility,
397 turns: turns.into_iter().map(Into::into).collect(),
398 resolution: resolution.into(),
399 },
400 }
401 }
402}
403
404impl From<WireBodyV1> for CollaborationOperationBodyV1 {
405 fn from(value: WireBodyV1) -> Self {
406 match value {
407 WireBodyV1::Open {
408 blocking,
409 title,
410 anchor,
411 visibility,
412 turn,
413 thread_ref,
414 } => Self::Open {
415 blocking,
416 title,
417 anchor: anchor.into(),
418 visibility,
419 turn: turn.into(),
420 thread_ref,
421 },
422 WireBodyV1::AppendTurn { turn } => Self::AppendTurn { turn: turn.into() },
423 WireBodyV1::RebindAnchor {
424 anchor,
425 status,
426 body_changed_since_open,
427 } => Self::RebindAnchor {
428 anchor: anchor.into(),
429 status: status.into(),
430 body_changed_since_open,
431 },
432 WireBodyV1::Resolve { resolution } => Self::Resolve {
433 resolution: resolution.into(),
434 },
435 WireBodyV1::Reopen { reason } => Self::Reopen { reason },
436 WireBodyV1::ResolveConflict {
437 competing,
438 selected,
439 } => Self::ResolveConflict {
440 competing,
441 selected,
442 },
443 WireBodyV1::LegacyImported {
444 source,
445 legacy_discussion_id,
446 aliases,
447 title,
448 anchor,
449 visibility,
450 turns,
451 resolution,
452 } => Self::LegacyImported {
453 source,
454 legacy_discussion_id,
455 aliases,
456 title,
457 anchor: anchor.into(),
458 visibility,
459 turns: turns.into_iter().map(Into::into).collect(),
460 resolution: resolution.into(),
461 },
462 }
463 }
464}