Skip to main content

heddle_object_model/object/collaboration/codec/
v2.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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)] // wire codec; boxing would change MessagePack layout
70enum 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}