Skip to main content

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