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, 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    })
175}
176
177impl From<CollaborationAnchor> for WireAnchorV1 {
178    fn from(value: CollaborationAnchor) -> Self {
179        match value {
180            CollaborationAnchor::Source { source } => Self::Source { source },
181            CollaborationAnchor::Repository => Self::Repository,
182            CollaborationAnchor::State { state_id } => Self::State { state_id },
183            CollaborationAnchor::Change { change_id } => Self::Change { change_id },
184            CollaborationAnchor::Path { state_id, path } => Self::Path { state_id, path },
185            CollaborationAnchor::Symbol {
186                state_id,
187                path,
188                symbol,
189            } => Self::Symbol {
190                state_id,
191                path,
192                symbol,
193            },
194        }
195    }
196}
197
198impl From<WireAnchorV1> for CollaborationAnchor {
199    fn from(value: WireAnchorV1) -> Self {
200        match value {
201            WireAnchorV1::Source { source } => Self::Source { source },
202            WireAnchorV1::Repository => Self::Repository,
203            WireAnchorV1::State { state_id } => Self::State { state_id },
204            WireAnchorV1::Change { change_id } => Self::Change { change_id },
205            WireAnchorV1::Path { state_id, path } => Self::Path { state_id, path },
206            WireAnchorV1::Symbol {
207                state_id,
208                path,
209                symbol,
210            } => Self::Symbol {
211                state_id,
212                path,
213                symbol,
214            },
215        }
216    }
217}
218
219impl From<CollaborationAnchorStatus> for WireAnchorStatusV1 {
220    fn from(value: CollaborationAnchorStatus) -> Self {
221        match value {
222            CollaborationAnchorStatus::Current => Self::Current,
223            CollaborationAnchorStatus::Moved => Self::Moved,
224            CollaborationAnchorStatus::Ambiguous => Self::Ambiguous,
225            CollaborationAnchorStatus::Orphaned => Self::Orphaned,
226        }
227    }
228}
229
230impl From<WireAnchorStatusV1> for CollaborationAnchorStatus {
231    fn from(value: WireAnchorStatusV1) -> Self {
232        match value {
233            WireAnchorStatusV1::Current => Self::Current,
234            WireAnchorStatusV1::Moved => Self::Moved,
235            WireAnchorStatusV1::Ambiguous => Self::Ambiguous,
236            WireAnchorStatusV1::Orphaned => Self::Orphaned,
237        }
238    }
239}
240
241impl From<DiscussionTurnV1> for WireTurnV1 {
242    fn from(value: DiscussionTurnV1) -> Self {
243        Self {
244            body: value.body,
245            content_hash: value.content_hash,
246        }
247    }
248}
249
250impl From<WireTurnV1> for DiscussionTurnV1 {
251    fn from(value: WireTurnV1) -> Self {
252        Self {
253            body: value.body,
254            content_hash: value.content_hash,
255        }
256    }
257}
258
259impl From<CollaborationResolution> for WireResolutionV1 {
260    fn from(value: CollaborationResolution) -> Self {
261        match value {
262            CollaborationResolution::AddressedByState { state_id } => {
263                Self::AddressedByState { state_id }
264            }
265            CollaborationResolution::AddressedByChange { change_id } => {
266                Self::AddressedByChange { change_id }
267            }
268            CollaborationResolution::Dismissed { reason } => Self::Dismissed { reason },
269            CollaborationResolution::IntoContext { context } => Self::IntoContext { context },
270            CollaborationResolution::IntoAnnotation {
271                annotation_kind,
272                content,
273                tags,
274            } => Self::IntoAnnotation {
275                annotation_kind,
276                content,
277                tags,
278            },
279            CollaborationResolution::Annotation { annotation_id } => {
280                Self::Annotation { annotation_id }
281            }
282        }
283    }
284}
285
286impl From<WireResolutionV1> for CollaborationResolution {
287    fn from(value: WireResolutionV1) -> Self {
288        match value {
289            WireResolutionV1::AddressedByState { state_id } => Self::AddressedByState { state_id },
290            WireResolutionV1::AddressedByChange { change_id } => {
291                Self::AddressedByChange { change_id }
292            }
293            WireResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
294            WireResolutionV1::IntoContext { context } => Self::IntoContext { context },
295            WireResolutionV1::IntoAnnotation {
296                annotation_kind,
297                content,
298                tags,
299            } => Self::IntoAnnotation {
300                annotation_kind,
301                content,
302                tags,
303            },
304            WireResolutionV1::Annotation { annotation_id } => Self::Annotation { annotation_id },
305        }
306    }
307}
308
309impl From<LegacyDiscussionResolutionV1> for WireLegacyResolutionV1 {
310    fn from(value: LegacyDiscussionResolutionV1) -> Self {
311        match value {
312            LegacyDiscussionResolutionV1::Open => Self::Open,
313            LegacyDiscussionResolutionV1::AddressedByState { state_id } => {
314                Self::AddressedByState { state_id }
315            }
316            LegacyDiscussionResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
317            LegacyDiscussionResolutionV1::Annotation { annotation_id } => {
318                Self::Annotation { annotation_id }
319            }
320        }
321    }
322}
323
324impl From<WireLegacyResolutionV1> for LegacyDiscussionResolutionV1 {
325    fn from(value: WireLegacyResolutionV1) -> Self {
326        match value {
327            WireLegacyResolutionV1::Open => Self::Open,
328            WireLegacyResolutionV1::AddressedByState { state_id } => {
329                Self::AddressedByState { state_id }
330            }
331            WireLegacyResolutionV1::Dismissed { reason } => Self::Dismissed { reason },
332            WireLegacyResolutionV1::Annotation { annotation_id } => {
333                Self::Annotation { annotation_id }
334            }
335        }
336    }
337}
338
339impl From<CollaborationOperationBodyV1> for WireBodyV1 {
340    fn from(value: CollaborationOperationBodyV1) -> Self {
341        match value {
342            CollaborationOperationBodyV1::Open {
343                blocking,
344                title,
345                anchor,
346                visibility,
347                turn,
348                thread_ref,
349            } => Self::Open {
350                blocking,
351                title,
352                anchor: anchor.into(),
353                visibility,
354                turn: turn.into(),
355                thread_ref,
356            },
357            CollaborationOperationBodyV1::AppendTurn { turn } => {
358                Self::AppendTurn { turn: turn.into() }
359            }
360            CollaborationOperationBodyV1::RebindAnchor {
361                anchor,
362                status,
363                body_changed_since_open,
364            } => Self::RebindAnchor {
365                anchor: anchor.into(),
366                status: status.into(),
367                body_changed_since_open,
368            },
369            CollaborationOperationBodyV1::Resolve { resolution } => Self::Resolve {
370                resolution: resolution.into(),
371            },
372            CollaborationOperationBodyV1::Reopen { reason } => Self::Reopen { reason },
373            CollaborationOperationBodyV1::ResolveConflict {
374                competing,
375                selected,
376            } => Self::ResolveConflict {
377                competing,
378                selected,
379            },
380            CollaborationOperationBodyV1::LegacyImported {
381                source,
382                legacy_discussion_id,
383                aliases,
384                title,
385                anchor,
386                visibility,
387                turns,
388                resolution,
389            } => Self::LegacyImported {
390                source,
391                legacy_discussion_id,
392                aliases,
393                title,
394                anchor: anchor.into(),
395                visibility,
396                turns: turns.into_iter().map(Into::into).collect(),
397                resolution: resolution.into(),
398            },
399        }
400    }
401}
402
403impl From<WireBodyV1> for CollaborationOperationBodyV1 {
404    fn from(value: WireBodyV1) -> Self {
405        match value {
406            WireBodyV1::Open {
407                blocking,
408                title,
409                anchor,
410                visibility,
411                turn,
412                thread_ref,
413            } => Self::Open {
414                blocking,
415                title,
416                anchor: anchor.into(),
417                visibility,
418                turn: turn.into(),
419                thread_ref,
420            },
421            WireBodyV1::AppendTurn { turn } => Self::AppendTurn { turn: turn.into() },
422            WireBodyV1::RebindAnchor {
423                anchor,
424                status,
425                body_changed_since_open,
426            } => Self::RebindAnchor {
427                anchor: anchor.into(),
428                status: status.into(),
429                body_changed_since_open,
430            },
431            WireBodyV1::Resolve { resolution } => Self::Resolve {
432                resolution: resolution.into(),
433            },
434            WireBodyV1::Reopen { reason } => Self::Reopen { reason },
435            WireBodyV1::ResolveConflict {
436                competing,
437                selected,
438            } => Self::ResolveConflict {
439                competing,
440                selected,
441            },
442            WireBodyV1::LegacyImported {
443                source,
444                legacy_discussion_id,
445                aliases,
446                title,
447                anchor,
448                visibility,
449                turns,
450                resolution,
451            } => Self::LegacyImported {
452                source,
453                legacy_discussion_id,
454                aliases,
455                title,
456                anchor: anchor.into(),
457                visibility,
458                turns: turns.into_iter().map(Into::into).collect(),
459                resolution: resolution.into(),
460            },
461        }
462    }
463}