use super::ValidatedContentToken;
use crate::{
AbsolutePath, ChangeSeq, CheckpointId, CommitId, ContentRef, InodeId, ManifestId, NamespaceId,
RevisionNo, WriterEpoch,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ApiError {
pub code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feature: Option<String>,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<Box<ErrorDetails>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ErrorDetails {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit_id: Option<CommitId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub committed_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub committed_fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fenced_epoch: Option<WriterEpoch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_writer_epoch: Option<WriterEpoch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_writer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_acquired_at_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inode_id: Option<InodeId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_revision: Option<RevisionNo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actual_revision: Option<RevisionNo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention_floor_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_deletion_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_deletion_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_head_seq: Option<ChangeSeq>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actual_head_seq: Option<ChangeSeq>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CreateNamespaceRequest {
pub namespace_id: NamespaceId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ForkNamespaceRequest {
pub new_namespace_id: NamespaceId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct NamespaceSummary {
pub namespace_id: NamespaceId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct NamespaceStatusResponse {
pub namespace_id: NamespaceId,
pub head_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_manifest_id: Option<ManifestId>,
pub wal_tail_segments: u64,
pub retention_floor_seq: ChangeSeq,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DeleteNamespaceResponse {
pub namespace_id: NamespaceId,
pub head_seq: ChangeSeq,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DestinationBehavior {
#[default]
NoReplace,
Replace,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DeleteDirectoryBehavior {
#[default]
NonRecursive,
Recursive,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FilesystemOperation {
#[cfg_attr(feature = "openapi", schema(title = "FsOpCreateDirectory"))]
CreateDirectory {
path: AbsolutePath,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
parents: bool,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpPutFile"))]
PutFile {
path: AbsolutePath,
content_ref: ContentRef,
#[serde(default)]
behavior: DestinationBehavior,
#[serde(default, skip_serializing_if = "Option::is_none")]
expected_revision_no: Option<RevisionNo>,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpDeletePath"))]
DeletePath {
path: AbsolutePath,
#[serde(default)]
behavior: DeleteDirectoryBehavior,
#[serde(default, skip_serializing_if = "Option::is_none")]
expected_inode_id: Option<InodeId>,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpMovePath"))]
MovePath {
from_path: AbsolutePath,
to_path: AbsolutePath,
#[serde(default)]
behavior: DestinationBehavior,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpCopyPath"))]
CopyPath {
from_path: AbsolutePath,
to_path: AbsolutePath,
#[serde(default)]
behavior: DestinationBehavior,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpUndelete"))]
Undelete {
inode_id: InodeId,
deleted_at_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "Option::is_none")]
path: Option<AbsolutePath>,
},
#[cfg_attr(feature = "openapi", schema(title = "FsOpRestoreRevision"))]
RestoreRevision {
path: AbsolutePath,
source_revision_no: RevisionNo,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CommitRequest {
pub commit_id: CommitId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content_tokens: Vec<ValidatedContentToken>,
pub operations: Vec<FilesystemOperation>,
}
impl CommitRequest {
pub fn single(
commit_id: CommitId,
message: Option<String>,
operation: FilesystemOperation,
) -> Self {
Self {
commit_id,
message,
content_tokens: Vec::new(),
operations: vec![operation],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct FileRevision {
pub inode_id: InodeId,
pub revision_no: RevisionNo,
pub committed_seq: ChangeSeq,
pub committed_at_ms: u64,
pub content_ref: ContentRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ListFileRevisionsResponse {
pub namespace_id: NamespaceId,
pub inode_id: InodeId,
pub head_seq: ChangeSeq,
pub revisions: Vec<FileRevision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CreateCheckpointRequest {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CreateCheckpointResponse {
pub namespace_id: NamespaceId,
pub checkpoint_id: CheckpointId,
pub checkpoint_seq: ChangeSeq,
pub manifest_id: ManifestId,
pub current_manifest_id: Option<ManifestId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ReleaseCheckpointResponse {
pub namespace_id: NamespaceId,
pub checkpoint_id: CheckpointId,
pub was_active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CheckpointOwnerSummary {
#[cfg_attr(feature = "openapi", schema(title = "CheckpointOwnerUser"))]
User {
name: String,
},
#[cfg_attr(feature = "openapi", schema(title = "CheckpointOwnerFork"))]
Fork {
target_namespace_id: NamespaceId,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CheckpointSummary {
pub checkpoint_id: CheckpointId,
pub owner: CheckpointOwnerSummary,
pub created_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_ms: Option<u64>,
pub checkpoint_seq: ChangeSeq,
pub manifest_id: ManifestId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ListCheckpointsResponse {
pub namespace_id: NamespaceId,
pub checkpoints: Vec<CheckpointSummary>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum FlushWalOutcome {
AlreadyCurrent,
Published,
Superseded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct FlushWalResponse {
pub namespace_id: NamespaceId,
pub target_head_seq: ChangeSeq,
pub manifest_id: ManifestId,
pub manifest_head_seq: ChangeSeq,
pub outcome: FlushWalOutcome,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GcRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grace_window_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_objects: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RetainedCandidates {
pub referenced: u64,
pub grace_window: u64,
pub no_provider_timestamp: u64,
pub degraded_roots: u64,
pub unrecognized_key: u64,
pub checkpoint_not_releasable: u64,
pub upload_session_window: u64,
pub upload_session_undecided: u64,
pub content_scan_deferred: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GcResponse {
pub namespace_id: NamespaceId,
pub deleted_wal_segments: u64,
pub deleted_metadata_tables: u64,
pub deleted_manifests: u64,
pub deleted_checkpoint_records: u64,
pub released_fork_checkpoints: u64,
#[serde(default)]
pub released_expired_checkpoints: u64,
#[serde(default)]
pub deleted_upload_sessions: u64,
#[serde(default)]
pub deleted_content_objects: u64,
#[serde(default)]
pub released_missing_basis_checkpoints: u64,
pub retained_candidates: u64,
#[serde(default)]
pub retained: RetainedCandidates,
pub degraded_retention: bool,
#[serde(default)]
pub content_reclamation_deferred: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(default)]
pub next_reclamation_at_ms: Option<u64>,
}
impl GcResponse {
pub fn empty(namespace_id: NamespaceId) -> Self {
Self {
namespace_id,
deleted_wal_segments: 0,
deleted_metadata_tables: 0,
deleted_manifests: 0,
deleted_checkpoint_records: 0,
released_fork_checkpoints: 0,
released_expired_checkpoints: 0,
deleted_upload_sessions: 0,
deleted_content_objects: 0,
released_missing_basis_checkpoints: 0,
retained_candidates: 0,
retained: RetainedCandidates::default(),
degraded_retention: false,
content_reclamation_deferred: false,
next_cursor: None,
next_reclamation_at_ms: None,
}
}
pub fn retain(&mut self, reason: RetainedReason) {
self.retained_candidates += 1;
*reason.counter(&mut self.retained) += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RetainedReason {
Referenced,
GraceWindow,
NoProviderTimestamp,
DegradedRoots,
UnrecognizedKey,
CheckpointNotReleasable,
UploadSessionWindow,
UploadSessionUndecided,
ContentScanDeferred,
}
impl RetainedReason {
fn counter(self, retained: &mut RetainedCandidates) -> &mut u64 {
match self {
Self::Referenced => &mut retained.referenced,
Self::GraceWindow => &mut retained.grace_window,
Self::NoProviderTimestamp => &mut retained.no_provider_timestamp,
Self::DegradedRoots => &mut retained.degraded_roots,
Self::UnrecognizedKey => &mut retained.unrecognized_key,
Self::CheckpointNotReleasable => &mut retained.checkpoint_not_releasable,
Self::UploadSessionWindow => &mut retained.upload_session_window,
Self::UploadSessionUndecided => &mut retained.upload_session_undecided,
Self::ContentScanDeferred => &mut retained.content_scan_deferred,
}
}
}
impl RetainedCandidates {
pub fn by_reason(&self) -> [(&'static str, u64); 9] {
[
("referenced", self.referenced),
("grace_window", self.grace_window),
("no_provider_timestamp", self.no_provider_timestamp),
("degraded_roots", self.degraded_roots),
("unrecognized_key", self.unrecognized_key),
("checkpoint_not_releasable", self.checkpoint_not_releasable),
("upload_session_window", self.upload_session_window),
("upload_session_undecided", self.upload_session_undecided),
("content_scan_deferred", self.content_scan_deferred),
]
}
pub fn add(&mut self, other: &Self) {
self.referenced += other.referenced;
self.grace_window += other.grace_window;
self.no_provider_timestamp += other.no_provider_timestamp;
self.degraded_roots += other.degraded_roots;
self.unrecognized_key += other.unrecognized_key;
self.checkpoint_not_releasable += other.checkpoint_not_releasable;
self.upload_session_window += other.upload_session_window;
self.upload_session_undecided += other.upload_session_undecided;
self.content_scan_deferred += other.content_scan_deferred;
}
pub fn top_reason(&self) -> Option<(&'static str, u64)> {
self.by_reason()
.into_iter()
.filter(|(_, count)| *count > 0)
.rev()
.max_by_key(|(_, count)| *count)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AdvanceRetentionResponse {
pub namespace_id: NamespaceId,
pub retention_floor_seq: ChangeSeq,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum MaintenanceStepKind {
WalFlush,
Reorganize,
Retention,
Gc,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct MaintenanceStepRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_wal_tail_segments: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gc: Option<GcRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub only: Option<MaintenanceStepKind>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WalFlushStepOutcome {
NotNeeded,
Flushed {
manifest_head_seq: ChangeSeq,
},
Superseded {
attempted_seq: ChangeSeq,
current_manifest_id: ManifestId,
},
RaceLost {
observed_head_seq: ChangeSeq,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReorganizeStepOutcome {
NotNeeded,
UnitPublished,
BudgetExhausted,
Superseded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct MaintenanceStepResponse {
pub namespace_id: NamespaceId,
pub status_before: NamespaceStatusResponse,
pub wal_flush: WalFlushStepOutcome,
pub reorganize: ReorganizeStepOutcome,
pub retention_floor_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gc: Option<GcResponse>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct StoreProbeRequest {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct StoreProbeResponse {
pub run_id: String,
pub checks: Vec<StoreProbeCheckResult>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct StoreProbeCheckResult {
pub name: String,
pub outcome: StoreProbeCheckOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum StoreProbeCheckOutcome {
Passed,
Unsupported,
Failed,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ContentId;
fn path(value: &str) -> AbsolutePath {
AbsolutePath::parse(value).expect("valid test path")
}
#[test]
fn behavior_enums_use_snake_case_wire_values() {
assert_eq!(
DestinationBehavior::default(),
DestinationBehavior::NoReplace
);
assert_eq!(
DeleteDirectoryBehavior::default(),
DeleteDirectoryBehavior::NonRecursive
);
assert_eq!(
serde_json::to_value(DestinationBehavior::NoReplace)
.expect("destination behavior json"),
serde_json::json!("no_replace")
);
assert_eq!(
serde_json::to_value(DestinationBehavior::Replace).expect("destination behavior json"),
serde_json::json!("replace")
);
assert_eq!(
serde_json::to_value(DeleteDirectoryBehavior::NonRecursive)
.expect("delete behavior json"),
serde_json::json!("non_recursive")
);
assert_eq!(
serde_json::to_value(DeleteDirectoryBehavior::Recursive).expect("delete behavior json"),
serde_json::json!("recursive")
);
}
#[test]
fn filesystem_delete_and_move_operations_use_behavior_field() {
let create_directory = FilesystemOperation::CreateDirectory {
path: path("/docs"),
parents: false,
};
assert_eq!(
serde_json::to_value(&create_directory).expect("create directory op json"),
serde_json::json!({
"kind": "create_directory",
"path": "/docs"
})
);
let create_directory_with_parents = FilesystemOperation::CreateDirectory {
path: path("/docs/notes"),
parents: true,
};
assert_eq!(
serde_json::to_value(&create_directory_with_parents)
.expect("create directory with parents op json"),
serde_json::json!({
"kind": "create_directory",
"path": "/docs/notes",
"parents": true
})
);
let delete = FilesystemOperation::DeletePath {
path: path("/docs"),
behavior: DeleteDirectoryBehavior::Recursive,
expected_inode_id: None,
};
assert_eq!(
serde_json::to_value(&delete).expect("delete op json"),
serde_json::json!({
"kind": "delete_path",
"path": "/docs",
"behavior": "recursive"
})
);
let move_path = FilesystemOperation::MovePath {
from_path: path("/docs/a.txt"),
to_path: path("/docs/b.txt"),
behavior: DestinationBehavior::Replace,
};
assert_eq!(
serde_json::to_value(&move_path).expect("move op json"),
serde_json::json!({
"kind": "move_path",
"from_path": "/docs/a.txt",
"to_path": "/docs/b.txt",
"behavior": "replace"
})
);
let copy_path = FilesystemOperation::CopyPath {
from_path: path("/docs/a.txt"),
to_path: path("/docs/b.txt"),
behavior: DestinationBehavior::Replace,
};
assert_eq!(
serde_json::to_value(©_path).expect("copy op json"),
serde_json::json!({
"kind": "copy_path",
"from_path": "/docs/a.txt",
"to_path": "/docs/b.txt",
"behavior": "replace"
})
);
}
#[test]
fn filesystem_operations_default_omitted_behavior_fields() {
let put: FilesystemOperation = serde_json::from_value(serde_json::json!({
"kind": "put_file",
"path": "/docs/a.txt",
"content_ref": {
"kind": "blob_v1",
"content_id": "con_0123456789abcdef0123456789abcdef",
"size_bytes": 1,
"storage_checksum": {
"algorithm": "sha256",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}
}))
.expect("put op defaults behavior");
assert!(matches!(
put,
FilesystemOperation::PutFile {
behavior: DestinationBehavior::NoReplace,
expected_revision_no: None,
..
}
));
let delete: FilesystemOperation = serde_json::from_value(serde_json::json!({
"kind": "delete_path",
"path": "/docs"
}))
.expect("delete op defaults behavior");
assert_eq!(
delete,
FilesystemOperation::DeletePath {
path: path("/docs"),
behavior: DeleteDirectoryBehavior::NonRecursive,
expected_inode_id: None,
}
);
let move_path: FilesystemOperation = serde_json::from_value(serde_json::json!({
"kind": "move_path",
"from_path": "/docs/a.txt",
"to_path": "/docs/b.txt"
}))
.expect("move op defaults behavior");
assert_eq!(
move_path,
FilesystemOperation::MovePath {
from_path: path("/docs/a.txt"),
to_path: path("/docs/b.txt"),
behavior: DestinationBehavior::NoReplace,
}
);
let copy_path: FilesystemOperation = serde_json::from_value(serde_json::json!({
"kind": "copy_path",
"from_path": "/docs/a.txt",
"to_path": "/docs/b.txt"
}))
.expect("copy op defaults behavior");
assert_eq!(
copy_path,
FilesystemOperation::CopyPath {
from_path: path("/docs/a.txt"),
to_path: path("/docs/b.txt"),
behavior: DestinationBehavior::NoReplace,
}
);
}
#[test]
fn filesystem_operation_paths_keep_the_plain_string_wire_shape() {
let content_ref = ContentRef::blob_v1(ContentId::generate(), b"hello");
let cases = [
(
FilesystemOperation::PutFile {
path: path("/docs/a.txt"),
content_ref: content_ref.clone(),
behavior: DestinationBehavior::NoReplace,
expected_revision_no: None,
},
serde_json::json!({
"kind": "put_file",
"path": "/docs/a.txt",
"content_ref": content_ref,
"behavior": "no_replace"
}),
),
(
FilesystemOperation::Undelete {
inode_id: InodeId(7),
deleted_at_seq: ChangeSeq(8),
path: Some(path("/docs/restored")),
},
serde_json::json!({
"kind": "undelete",
"inode_id": 7,
"deleted_at_seq": 8,
"path": "/docs/restored"
}),
),
(
FilesystemOperation::RestoreRevision {
path: path("/docs/a.txt"),
source_revision_no: RevisionNo(2),
},
serde_json::json!({
"kind": "restore_revision",
"path": "/docs/a.txt",
"source_revision_no": 2
}),
),
];
for (operation, string_shaped_json) in cases {
assert_eq!(
serde_json::to_value(operation).expect("serialize filesystem operation"),
string_shaped_json
);
}
}
#[test]
fn filesystem_operation_paths_validate_during_deserialization() {
for encoded in [
serde_json::json!({"kind": "create_directory", "path": "relative", "parents": false}),
serde_json::json!({
"kind": "put_file",
"path": "relative",
"content_ref": ContentRef::blob_v1(ContentId::generate(), b"hello")
}),
serde_json::json!({"kind": "delete_path", "path": "relative"}),
serde_json::json!({
"kind": "move_path",
"from_path": "relative",
"to_path": "/target"
}),
serde_json::json!({
"kind": "copy_path",
"from_path": "/source",
"to_path": "relative"
}),
serde_json::json!({
"kind": "undelete",
"inode_id": 7,
"deleted_at_seq": 8,
"path": "relative"
}),
serde_json::json!({
"kind": "restore_revision",
"path": "relative",
"source_revision_no": 2
}),
] {
assert!(serde_json::from_value::<FilesystemOperation>(encoded).is_err());
}
}
}