cdx_core/extensions/collaboration/
revision.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::DocumentId;
7
8use super::{Collaborator, CrdtFormat};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct RevisionHistory {
14 pub revisions: Vec<Revision>,
16}
17
18impl RevisionHistory {
19 #[must_use]
21 pub fn new() -> Self {
22 Self {
23 revisions: Vec::new(),
24 }
25 }
26
27 pub fn add(&mut self, revision: Revision) {
29 self.revisions.push(revision);
30 }
31
32 #[must_use]
34 pub fn latest(&self) -> Option<&Revision> {
35 self.revisions.last()
36 }
37
38 #[must_use]
40 pub fn get_version(&self, version: u32) -> Option<&Revision> {
41 self.revisions.iter().find(|r| r.version == version)
42 }
43
44 #[must_use]
46 pub fn next_version(&self) -> u32 {
47 self.revisions
48 .iter()
49 .map(|r| r.version)
50 .max()
51 .map_or(1, |v| v + 1)
52 }
53
54 #[must_use]
56 pub fn len(&self) -> usize {
57 self.revisions.len()
58 }
59
60 #[must_use]
62 pub fn is_empty(&self) -> bool {
63 self.revisions.is_empty()
64 }
65}
66
67impl Default for RevisionHistory {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct Revision {
77 pub version: u32,
79
80 pub document_id: DocumentId,
82
83 pub created: DateTime<Utc>,
85
86 pub author: Collaborator,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub note: Option<String>,
92
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub tags: Vec<String>,
96}
97
98impl Revision {
99 #[must_use]
101 pub fn new(version: u32, document_id: DocumentId, author: Collaborator) -> Self {
102 Self {
103 version,
104 document_id,
105 created: Utc::now(),
106 author,
107 note: None,
108 tags: Vec::new(),
109 }
110 }
111
112 #[must_use]
114 pub fn with_note(mut self, note: impl Into<String>) -> Self {
115 self.note = Some(note.into());
116 self
117 }
118
119 #[must_use]
121 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
122 self.tags.push(tag.into());
123 self
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct MaterializationEvent {
134 pub timestamp: DateTime<Utc>,
136
137 pub agent: String,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub from_crdt_format: Option<CrdtFormat>,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub to_crdt_format: Option<CrdtFormat>,
147
148 pub reason: MaterializationReason,
150}
151
152impl MaterializationEvent {
153 #[must_use]
155 pub fn new(agent: impl Into<String>, reason: MaterializationReason) -> Self {
156 Self {
157 timestamp: Utc::now(),
158 agent: agent.into(),
159 from_crdt_format: None,
160 to_crdt_format: None,
161 reason,
162 }
163 }
164
165 #[must_use]
167 pub fn with_source_format(mut self, format: CrdtFormat) -> Self {
168 self.from_crdt_format = Some(format);
169 self
170 }
171
172 #[must_use]
174 pub fn with_target_format(mut self, format: CrdtFormat) -> Self {
175 self.to_crdt_format = Some(format);
176 self
177 }
178
179 #[must_use]
181 pub fn cross_tool_exchange(agent: impl Into<String>, from: CrdtFormat, to: CrdtFormat) -> Self {
182 Self::new(agent, MaterializationReason::CrossToolExchange)
183 .with_source_format(from)
184 .with_target_format(to)
185 }
186
187 #[must_use]
189 pub fn export(agent: impl Into<String>, from: CrdtFormat) -> Self {
190 Self::new(agent, MaterializationReason::Export).with_source_format(from)
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
196#[serde(rename_all = "kebab-case")]
197#[strum(serialize_all = "kebab-case")]
198pub enum MaterializationReason {
199 CrossToolExchange,
201 Export,
203 Archive,
205 UserRequest,
207}