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