1use 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}