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 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)] enum 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}