use crate::{
AttributeRevisionNo, Attributes, ChangeSeq, CommitId, ContentRef, DisplayName, InodeId,
NameKey, NamespaceId, RevisionNo,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CommitResponse {
pub namespace_id: NamespaceId,
pub commit_id: CommitId,
pub committed_seq: ChangeSeq,
pub committed_by: crate::ActorRef,
pub committed_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
pub events: Option<Vec<FilesystemChange>>,
}
impl CommitResponse {
pub fn from_committed_change(namespace_id: NamespaceId, change: CommittedChange) -> Self {
Self {
namespace_id,
commit_id: change.commit_id,
committed_seq: change.committed_seq,
committed_by: change.committed_by,
committed_at_ms: change.committed_at_ms,
message: change.message,
events: Some(change.events),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DirectoryBinding {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
pub parent_inode_id: InodeId,
pub name_key: NameKey,
pub display_name: DisplayName,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FilesystemChange {
#[cfg_attr(
feature = "openapi",
schema(title = "FilesystemChangeDirectoryCreated")
)]
DirectoryCreated {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
parent_inode_id: InodeId,
display_name: DisplayName,
binding_generation: String,
},
#[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeFileCreated"))]
FileCreated {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
parent_inode_id: InodeId,
display_name: DisplayName,
binding_generation: String,
revision_no: RevisionNo,
content_ref: ContentRef,
},
#[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeContentChanged"))]
ContentChanged {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
revision_no: RevisionNo,
content_ref: ContentRef,
},
#[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeMoved"))]
Moved {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
from_parent_inode_id: InodeId,
from_display_name: DisplayName,
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
to_parent_inode_id: InodeId,
to_display_name: DisplayName,
binding_generation: String,
},
#[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeDeleted"))]
Deleted {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
deleted_binding: Option<DirectoryBinding>,
},
#[cfg_attr(feature = "openapi", schema(title = "FilesystemChangeUndeleted"))]
Undeleted {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
parent_inode_id: InodeId,
display_name: DisplayName,
binding_generation: String,
},
#[cfg_attr(
feature = "openapi",
schema(title = "FilesystemChangeAttributesChanged")
)]
AttributesChanged {
#[serde(with = "crate::public_inode_id")]
#[cfg_attr(
feature = "openapi",
schema(schema_with = crate::public_inode_id::schema)
)]
inode_id: InodeId,
attributes_revision_no: AttributeRevisionNo,
attributes: Attributes,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CommittedChange {
pub committed_seq: ChangeSeq,
pub commit_id: CommitId,
pub committed_by: crate::ActorRef,
pub committed_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub events: Vec<FilesystemChange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ListChangesResponse {
pub namespace_id: NamespaceId,
pub after_seq: ChangeSeq,
pub through_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = false))]
pub next_after_seq: Option<ChangeSeq>,
pub changes: Vec<CommittedChange>,
}
#[cfg(test)]
mod tests {
use super::{CommitResponse, CommittedChange, FilesystemChange};
use crate::InodeId;
fn binding_generation() -> String {
"generation".to_owned()
}
#[test]
fn committed_change_uses_committed_by_on_the_wire() {
let change = CommittedChange {
committed_seq: crate::ChangeSeq(7),
commit_id: crate::CommitId::parse("example-commit").expect("valid commit id"),
committed_by: crate::ActorRef::loonfs_system(),
committed_at_ms: 1_752_624_000_000,
message: None,
events: Vec::new(),
};
assert_eq!(
serde_json::to_value(change).expect("serialize committed change"),
serde_json::json!({
"committed_seq": 7,
"commit_id": "example-commit",
"committed_by": { "kind": "system", "id": "loonfs" },
"committed_at_ms": 1_752_624_000_000_u64,
"events": [],
})
);
}
#[test]
fn a_commit_response_carries_the_committed_change_at_the_top_level() {
let response = CommitResponse::from_committed_change(
crate::NamespaceId::parse("demo").expect("valid namespace id"),
CommittedChange {
committed_seq: crate::ChangeSeq(419),
commit_id: crate::CommitId::parse("example-commit").expect("valid commit id"),
committed_by: crate::ActorRef::loonfs_system(),
committed_at_ms: 1_752_624_000_000,
message: Some("import the reports".to_owned()),
events: vec![FilesystemChange::DirectoryCreated {
inode_id: InodeId(43),
parent_inode_id: InodeId(1),
display_name: crate::DisplayName::parse("docs").expect("valid display name"),
binding_generation: binding_generation(),
}],
},
);
assert_eq!(
serde_json::to_value(response).expect("serialize commit response"),
serde_json::json!({
"namespace_id": "demo",
"commit_id": "example-commit",
"committed_seq": 419,
"committed_by": { "kind": "system", "id": "loonfs" },
"committed_at_ms": 1_752_624_000_000_u64,
"message": "import the reports",
"events": [{
"kind": "directory_created",
"inode_id": "ino_43",
"parent_inode_id": "ino_1",
"display_name": "docs",
"binding_generation": binding_generation(),
}],
})
);
}
#[test]
fn a_commit_response_omits_absent_events_and_message() {
let response = CommitResponse {
namespace_id: crate::NamespaceId::parse("demo").expect("valid namespace id"),
commit_id: crate::CommitId::parse("example-commit").expect("valid commit id"),
committed_seq: crate::ChangeSeq(419),
committed_by: crate::ActorRef::loonfs_system(),
committed_at_ms: 1_752_624_000_000,
message: None,
events: None,
};
assert_eq!(
serde_json::to_value(response).expect("serialize commit response"),
serde_json::json!({
"namespace_id": "demo",
"commit_id": "example-commit",
"committed_seq": 419,
"committed_by": { "kind": "system", "id": "loonfs" },
"committed_at_ms": 1_752_624_000_000_u64,
})
);
}
#[test]
fn filesystem_change_events_use_snake_case_kind_tags() {
let sample_content_ref = crate::ContentRef::blob_v1(
crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
.expect("valid content id"),
b"hello",
);
let sample_content_ref_json = r#"{"kind":"blob_v1","content_id":"con_0123456789abcdef0123456789abcdef","size_bytes":5,"checksum":{"algorithm":"sha256","value":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}}"#;
let generation = binding_generation();
let directory_created = FilesystemChange::DirectoryCreated {
inode_id: InodeId(2),
parent_inode_id: InodeId(1),
display_name: crate::DisplayName::parse("Docs").expect("valid display name"),
binding_generation: generation.clone(),
};
assert_eq!(
serde_json::to_string(&directory_created).expect("serialize directory-created event"),
format!(
r#"{{"kind":"directory_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"Docs","binding_generation":"{generation}"}}"#
)
);
let file_created = FilesystemChange::FileCreated {
inode_id: InodeId(2),
parent_inode_id: InodeId(1),
display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
binding_generation: generation.clone(),
revision_no: crate::RevisionNo(1),
content_ref: sample_content_ref.clone(),
};
assert_eq!(
serde_json::to_string(&file_created).expect("serialize file-created event"),
format!(
r#"{{"kind":"file_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt","binding_generation":"{generation}","revision_no":1,"content_ref":{sample_content_ref_json}}}"#
)
);
let missing_content_ref = r#"{"kind":"file_created","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt","revision_no":1}"#;
assert!(serde_json::from_str::<FilesystemChange>(missing_content_ref).is_err());
let retired_creation = serde_json::json!({
"kind": (["cre", "ated"].concat()),
"inode_id": "ino_2",
"inode_kind": "file",
"parent_inode_id": "ino_1",
"display_name": "a.txt",
"revision_no": 1,
});
assert!(serde_json::from_value::<FilesystemChange>(retired_creation).is_err());
let content_changed = FilesystemChange::ContentChanged {
inode_id: InodeId(2),
revision_no: crate::RevisionNo(3),
content_ref: sample_content_ref,
};
assert_eq!(
serde_json::to_string(&content_changed).expect("serialize content changed event"),
format!(
r#"{{"kind":"content_changed","inode_id":"ino_2","revision_no":3,"content_ref":{sample_content_ref_json}}}"#
)
);
let moved = FilesystemChange::Moved {
inode_id: InodeId(2),
from_parent_inode_id: InodeId(1),
from_display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
to_parent_inode_id: InodeId(3),
to_display_name: crate::DisplayName::parse("b.txt").expect("valid display name"),
binding_generation: generation.clone(),
};
assert_eq!(
serde_json::to_string(&moved).expect("serialize moved event"),
format!(
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","binding_generation":"{generation}"}}"#
)
);
let deleted = FilesystemChange::Deleted {
inode_id: InodeId(2),
deleted_binding: Some(super::DirectoryBinding {
parent_inode_id: InodeId(1),
name_key: crate::NameKey::parse("a.txt").expect("valid name key"),
display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
}),
};
assert_eq!(
serde_json::to_string(&deleted).expect("serialize deleted event"),
r#"{"kind":"deleted","inode_id":"ino_2","deleted_binding":{"parent_inode_id":"ino_1","name_key":"a.txt","display_name":"a.txt"}}"#
);
let undeleted = FilesystemChange::Undeleted {
inode_id: InodeId(2),
parent_inode_id: InodeId(1),
display_name: crate::DisplayName::parse("a.txt").expect("valid display name"),
binding_generation: generation.clone(),
};
assert_eq!(
serde_json::to_string(&undeleted).expect("serialize undeleted event"),
format!(
r#"{{"kind":"undeleted","inode_id":"ino_2","parent_inode_id":"ino_1","display_name":"a.txt","binding_generation":"{generation}"}}"#
)
);
let attributes_changed = FilesystemChange::AttributesChanged {
inode_id: InodeId(2),
attributes_revision_no: crate::AttributeRevisionNo(4),
attributes: crate::Attributes::new(std::collections::BTreeMap::from([(
crate::AttributeKey::parse("owner").expect("valid attribute key"),
crate::AttributeValue::parse("ada").expect("valid attribute value"),
)]))
.expect("valid attribute map"),
};
assert_eq!(
serde_json::to_string(&attributes_changed).expect("serialize attributes event"),
r#"{"kind":"attributes_changed","inode_id":"ino_2","attributes_revision_no":4,"attributes":{"owner":"ada"}}"#
);
let cleared = FilesystemChange::AttributesChanged {
inode_id: InodeId(2),
attributes_revision_no: crate::AttributeRevisionNo(5),
attributes: crate::Attributes::default(),
};
assert_eq!(
serde_json::to_string(&cleared).expect("serialize cleared attributes event"),
r#"{"kind":"attributes_changed","inode_id":"ino_2","attributes_revision_no":5,"attributes":{}}"#
);
}
}