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