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    },
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}