1use crate::{
7 AttributeRevisionNo, Attributes, ChangeSeq, CommitId, ContentRef, DisplayName, InodeId,
8 NameKey, NamespaceId, RevisionNo,
9};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20pub struct CommitResponse {
21 pub namespace_id: NamespaceId,
23 pub commit_id: CommitId,
26 pub committed_seq: ChangeSeq,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
33pub struct DeletedDirentry {
34 #[serde(with = "crate::public_inode_id")]
36 #[cfg_attr(
37 feature = "openapi",
38 schema(schema_with = crate::public_inode_id::schema)
39 )]
40 pub parent_inode_id: InodeId,
41 pub name_key: NameKey,
43 pub display_name: DisplayName,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
59#[serde(tag = "kind", rename_all = "snake_case")]
60pub enum FilesystemChange {
61 #[cfg_attr(
63 feature = "openapi",
64 schema(title = "FilesystemChangeDirectoryCreated")
65 )]
66 DirectoryCreated {
67 #[serde(with = "crate::public_inode_id")]
69 #[cfg_attr(
70 feature = "openapi",
71 schema(schema_with = crate::public_inode_id::schema)
72 )]
73 inode_id: InodeId,
74 #[serde(with = "crate::public_inode_id")]
76 #[cfg_attr(
77 feature = "openapi",
78 schema(schema_with = crate::public_inode_id::schema)
79 )]
80 parent_inode_id: InodeId,
81 display_name: DisplayName,
83 },
84 #[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeFileCreated"))]
86 FileCreated {
87 #[serde(with = "crate::public_inode_id")]
89 #[cfg_attr(
90 feature = "openapi",
91 schema(schema_with = crate::public_inode_id::schema)
92 )]
93 inode_id: InodeId,
94 #[serde(with = "crate::public_inode_id")]
96 #[cfg_attr(
97 feature = "openapi",
98 schema(schema_with = crate::public_inode_id::schema)
99 )]
100 parent_inode_id: InodeId,
101 display_name: DisplayName,
103 revision_no: RevisionNo,
105 content_ref: ContentRef,
107 },
108 #[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeContentChanged"))]
111 ContentChanged {
112 #[serde(with = "crate::public_inode_id")]
114 #[cfg_attr(
115 feature = "openapi",
116 schema(schema_with = crate::public_inode_id::schema)
117 )]
118 inode_id: InodeId,
119 revision_no: RevisionNo,
121 content_ref: ContentRef,
123 },
124 #[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeMoved"))]
126 Moved {
127 #[serde(with = "crate::public_inode_id")]
129 #[cfg_attr(
130 feature = "openapi",
131 schema(schema_with = crate::public_inode_id::schema)
132 )]
133 inode_id: InodeId,
134 #[serde(with = "crate::public_inode_id")]
136 #[cfg_attr(
137 feature = "openapi",
138 schema(schema_with = crate::public_inode_id::schema)
139 )]
140 from_parent_inode_id: InodeId,
141 from_display_name: DisplayName,
143 #[serde(with = "crate::public_inode_id")]
145 #[cfg_attr(
146 feature = "openapi",
147 schema(schema_with = crate::public_inode_id::schema)
148 )]
149 to_parent_inode_id: InodeId,
150 to_display_name: DisplayName,
152 },
153 #[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeDeleted"))]
156 Deleted {
157 #[serde(with = "crate::public_inode_id")]
159 #[cfg_attr(
160 feature = "openapi",
161 schema(schema_with = crate::public_inode_id::schema)
162 )]
163 inode_id: InodeId,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
167 deleted_direntry: Option<DeletedDirentry>,
168 },
169 #[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeUndeleted"))]
171 Undeleted {
172 #[serde(with = "crate::public_inode_id")]
174 #[cfg_attr(
175 feature = "openapi",
176 schema(schema_with = crate::public_inode_id::schema)
177 )]
178 inode_id: InodeId,
179 #[serde(with = "crate::public_inode_id")]
181 #[cfg_attr(
182 feature = "openapi",
183 schema(schema_with = crate::public_inode_id::schema)
184 )]
185 parent_inode_id: InodeId,
186 display_name: DisplayName,
188 },
189 #[cfg_attr(
191 feature = "openapi",
192 schema(title = "FilesystemChangeAttributesChanged")
193 )]
194 AttributesChanged {
195 #[serde(with = "crate::public_inode_id")]
197 #[cfg_attr(
198 feature = "openapi",
199 schema(schema_with = crate::public_inode_id::schema)
200 )]
201 inode_id: InodeId,
202 attributes_revision_no: AttributeRevisionNo,
204 attributes: Attributes,
208 },
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
214pub struct CommittedChange {
215 pub committed_seq: ChangeSeq,
217 pub commit_id: CommitId,
219 pub actor: crate::ActorRef,
221 pub committed_at_ms: u64,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub message: Option<String>,
227 pub events: Vec<FilesystemChange>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
236pub struct ChangesResponse {
237 pub namespace_id: NamespaceId,
239 pub after_seq: ChangeSeq,
241 pub through_seq: ChangeSeq,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub next_after_seq: Option<ChangeSeq>,
246 pub changes: Vec<CommittedChange>,
248}
249
250#[cfg(test)]
251mod tests {
252 use super::FilesystemChange;
253 use crate::InodeId;
254
255 #[test]
256 fn filesystem_change_events_use_snake_case_kind_tags() {
257 let sample_content_ref = crate::ContentRef::blob_v1(
258 crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
259 .expect("valid content id"),
260 b"hello",
261 );
262 let sample_content_ref_json = r#"{"kind":"blob_v1","content_id":"con_0123456789abcdef0123456789abcdef","size_bytes":5,"checksum":{"algorithm":"sha256","value":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}}"#;
263
264 let directory_created = FilesystemChange::DirectoryCreated {
265 inode_id: InodeId(2),
266 parent_inode_id: InodeId(1),
267 display_name: crate::DisplayName::parse("Docs").expect("valid display name"),
268 };
269 assert_eq!(
270 serde_json::to_string(&directory_created).expect("serialize directory-created event"),
271 r#"{"kind":"directory_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"Docs"}"#
272 );
273
274 let file_created = FilesystemChange::FileCreated {
275 inode_id: InodeId(2),
276 parent_inode_id: InodeId(1),
277 display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
278 revision_no: crate::RevisionNo(1),
279 content_ref: sample_content_ref.clone(),
280 };
281 assert_eq!(
282 serde_json::to_string(&file_created).expect("serialize file-created event"),
283 format!(
284 r#"{{"kind":"file_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt","revision_no":1,"content_ref":{sample_content_ref_json}}}"#
285 )
286 );
287
288 let missing_content_ref = r#"{"kind":"file_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt","revision_no":1}"#;
289 assert!(serde_json::from_str::<FilesystemChange>(missing_content_ref).is_err());
290
291 let retired_creation = serde_json::json!({
292 "kind": (["cre", "ated"].concat()),
293 "inode_id": "ino_2",
294 "inode_kind": "file",
295 "parent_inode_id": "ino_1",
296 "display_name": "a.txt",
297 "revision_no": 1,
298 });
299 assert!(serde_json::from_value::<FilesystemChange>(retired_creation).is_err());
300
301 let content_changed = FilesystemChange::ContentChanged {
302 inode_id: InodeId(2),
303 revision_no: crate::RevisionNo(3),
304 content_ref: sample_content_ref,
305 };
306 assert_eq!(
307 serde_json::to_string(&content_changed).expect("serialize content changed event"),
308 format!(
309 r#"{{"kind":"content_changed","inode_id":"ino_2","revision_no":3,"content_ref":{sample_content_ref_json}}}"#
310 )
311 );
312
313 let moved = FilesystemChange::Moved {
314 inode_id: InodeId(2),
315 from_parent_inode_id: InodeId(1),
316 from_display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
317 to_parent_inode_id: InodeId(3),
318 to_display_name: crate::DisplayName::parse("b.txt").expect("valid display name"),
319 };
320 assert_eq!(
321 serde_json::to_string(&moved).expect("serialize moved event"),
322 r#"{"kind":"moved","inode_id":"ino_2","from_parent_inode_id":"ino_1","from_display_name":"a.txt","to_parent_inode_id":"ino_3","to_display_name":"b.txt"}"#
323 );
324
325 let deleted = FilesystemChange::Deleted {
326 inode_id: InodeId(2),
327 deleted_direntry: Some(super::DeletedDirentry {
328 parent_inode_id: InodeId(1),
329 name_key: crate::NameKey::parse("a.txt").expect("valid name key"),
330 display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
331 }),
332 };
333 assert_eq!(
334 serde_json::to_string(&deleted).expect("serialize deleted event"),
335 r#"{"kind":"deleted","inode_id":"ino_2","deleted_direntry":{"parent_inode_id":"ino_1","name_key":"a.txt","display_name":"a.txt"}}"#
336 );
337
338 let undeleted = FilesystemChange::Undeleted {
339 inode_id: InodeId(2),
340 parent_inode_id: InodeId(1),
341 display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
342 };
343 assert_eq!(
344 serde_json::to_string(&undeleted).expect("serialize undeleted event"),
345 r#"{"kind":"undeleted","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt"}"#
346 );
347
348 let attributes_changed = FilesystemChange::AttributesChanged {
349 inode_id: InodeId(2),
350 attributes_revision_no: crate::AttributeRevisionNo(4),
351 attributes: crate::Attributes::new(std::collections::BTreeMap::from([(
352 crate::AttributeKey::parse("owner").expect("valid attribute key"),
353 crate::AttributeValue::parse("ada").expect("valid attribute value"),
354 )]))
355 .expect("valid attribute map"),
356 };
357 assert_eq!(
358 serde_json::to_string(&attributes_changed).expect("serialize attributes event"),
359 r#"{"kind":"attributes_changed","inode_id":"ino_2","attributes_revision_no":4,"attributes":{"owner":"ada"}}"#
360 );
361
362 let cleared = FilesystemChange::AttributesChanged {
364 inode_id: InodeId(2),
365 attributes_revision_no: crate::AttributeRevisionNo(5),
366 attributes: crate::Attributes::default(),
367 };
368 assert_eq!(
369 serde_json::to_string(&cleared).expect("serialize cleared attributes event"),
370 r#"{"kind":"attributes_changed","inode_id":"ino_2","attributes_revision_no":5,"attributes":{}}"#
371 );
372 }
373}