use crate::copy::CopyConflictPolicy;
use crate::copy::CopyMethod;
use crate::copy::CopyMode;
use crate::copy::CopyOptions;
use crate::copy::CopyStats;
use crate::copy::MetadataPreservePolicy;
use crate::copy::ServerSidePreference;
use crate::metadata::AchievedAtomicity;
use crate::metadata::NonSensitiveMetadata;
use crate::metadata::ResourceVersion;
use crate::metadata::UserMetadata;
#[derive(Clone, Debug, PartialEq)]
pub struct CopyOutcome {
stats: CopyStats,
method: CopyMethod,
atomicity: AchievedAtomicity,
durable: bool,
metadata: MetadataPreservePolicy,
target_version: Option<ResourceVersion>,
used_fallback: bool,
diagnostics: NonSensitiveMetadata,
}
impl CopyOutcome {
#[inline]
#[must_use]
pub fn new(stats: CopyStats, method: CopyMethod, atomicity: AchievedAtomicity) -> Self {
Self {
stats,
method,
atomicity,
durable: false,
metadata: MetadataPreservePolicy::None,
target_version: None,
used_fallback: false,
diagnostics: NonSensitiveMetadata::new(),
}
}
#[inline]
#[must_use]
pub fn with_diagnostics(mut self, diagnostics: UserMetadata) -> Self {
self.diagnostics = NonSensitiveMetadata::from(diagnostics);
self
}
#[inline]
#[must_use]
pub const fn stats(&self) -> &CopyStats {
&self.stats
}
#[inline]
#[must_use]
pub const fn method(&self) -> CopyMethod {
self.method
}
#[inline]
#[must_use]
pub const fn atomicity(&self) -> AchievedAtomicity {
self.atomicity
}
#[inline]
#[must_use]
pub const fn durable(&self) -> bool {
self.durable
}
#[inline]
#[must_use]
pub fn with_durable(mut self, durable: bool) -> Self {
self.durable = durable;
self
}
#[inline]
#[must_use]
pub fn with_metadata(mut self, metadata: MetadataPreservePolicy) -> Self {
self.metadata = metadata;
self
}
#[inline]
#[must_use]
pub fn with_target_version(mut self, target_version: ResourceVersion) -> Self {
self.target_version = Some(target_version);
self
}
#[inline]
#[must_use]
pub const fn metadata(&self) -> MetadataPreservePolicy {
self.metadata
}
#[inline]
#[must_use]
pub const fn target_version(&self) -> Option<&ResourceVersion> {
self.target_version.as_ref()
}
#[inline]
#[must_use]
pub const fn used_fallback(&self) -> bool {
self.used_fallback
}
#[inline]
#[must_use]
pub const fn diagnostics(&self) -> &NonSensitiveMetadata {
&self.diagnostics
}
pub(crate) fn streamed_fallback(stats: CopyStats, atomicity: AchievedAtomicity, durable: bool) -> Self {
Self {
stats,
method: CopyMethod::Streamed,
atomicity,
durable,
metadata: MetadataPreservePolicy::None,
target_version: None,
used_fallback: true,
diagnostics: NonSensitiveMetadata::new(),
}
}
pub(crate) fn contract_violation(&self, options: &CopyOptions) -> Option<&'static str> {
if self.used_fallback || self.method == CopyMethod::Streamed {
return Some("provider returned a facade streamed-fallback outcome as native success");
}
if options.atomicity() == crate::metadata::AtomicityRequirement::Required
&& self.atomicity != AchievedAtomicity::Atomic
{
return Some("provider reported non-atomic success for an atomic-required copy");
}
if options.durability() == crate::metadata::DurabilityRequirement::Required && !self.durable {
return Some("provider reported non-durable success for a durability-required copy");
}
if options.server_side() == ServerSidePreference::Require && self.method != CopyMethod::ServerSide {
return Some("provider reported a non-server-side success for a server-side-required copy");
}
if options.server_side() == ServerSidePreference::Disable && self.method == CopyMethod::ServerSide {
return Some("provider reported a server-side success for a server-side-disabled copy");
}
if self.metadata != options.preserve_metadata() {
return Some("provider reported metadata preservation different from the copy request");
}
if !options.continue_on_error() && self.stats.failed != 0 {
return Some("provider reported failed copy entries without continue-on-error");
}
if options.conflict() != CopyConflictPolicy::Skip && self.stats.skipped != 0 {
return Some("provider reported skipped copy entries without a skip conflict policy");
}
if options.conflict() != CopyConflictPolicy::Overwrite && self.stats.overwritten != 0 {
return Some("provider reported overwritten copy entries without an overwrite conflict policy");
}
if options.max_bytes().is_some_and(|maximum| self.stats.bytes > maximum) {
return Some("provider reported copy bytes beyond the requested limit");
}
let entries = self
.stats
.files
.checked_add(self.stats.directories)
.and_then(|value| value.checked_add(self.stats.symlinks))
.and_then(|value| value.checked_add(self.stats.objects))
.and_then(|value| value.checked_add(self.stats.prefixes));
let valid_skipped_file = options.mode() == CopyMode::File
&& options.conflict() == CopyConflictPolicy::Skip
&& self.stats.skipped == 1
&& entries == Some(0);
let valid_copied_file = entries == Some(1) && self.stats.directories == 0 && self.stats.prefixes == 0;
if options.mode() == CopyMode::File && !valid_copied_file && !valid_skipped_file {
return Some("provider reported a file-mode copy without exactly one resource");
}
if entries.is_none()
|| options.max_entries().is_some_and(|maximum| {
u64::try_from(maximum).is_ok_and(|maximum| entries.is_some_and(|entries| entries > maximum))
})
{
return Some("provider reported copy entries beyond the requested limit");
}
None
}
}
#[cfg(test)]
mod tests {
use std::hint::black_box;
use super::CopyOutcome;
use crate::copy::CopyMethod;
use crate::copy::CopyOptions;
use crate::copy::CopyStats;
use crate::copy::MetadataPreservePolicy;
use crate::metadata::AchievedAtomicity;
use crate::metadata::NonSensitiveMetadata;
use crate::metadata::ResourceVersion;
#[test]
fn outcome_accessors_are_executed_at_runtime() {
let constructor: fn(CopyStats, CopyMethod, AchievedAtomicity) -> CopyOutcome = black_box(CopyOutcome::new);
let metadata: fn(&CopyOutcome) -> MetadataPreservePolicy = black_box(CopyOutcome::metadata);
let target_version: for<'a> fn(&'a CopyOutcome) -> Option<&'a ResourceVersion> =
black_box(CopyOutcome::target_version);
let diagnostics: fn(&CopyOutcome) -> &NonSensitiveMetadata = black_box(CopyOutcome::diagnostics);
let with_target_version: fn(CopyOutcome, ResourceVersion) -> CopyOutcome =
black_box(CopyOutcome::with_target_version);
let with_diagnostics: fn(CopyOutcome, crate::metadata::UserMetadata) -> CopyOutcome =
black_box(CopyOutcome::with_diagnostics);
let outcome = with_diagnostics(
with_target_version(
constructor(CopyStats::default(), CopyMethod::Native, AchievedAtomicity::Atomic),
ResourceVersion::new("generation-7"),
),
crate::metadata::UserMetadata::new(),
);
assert_eq!(MetadataPreservePolicy::None, metadata(&outcome));
assert_eq!(
Some("generation-7"),
target_version(&outcome).map(ResourceVersion::as_str)
);
assert!(diagnostics(&outcome).is_empty());
let bytes_exceeded = CopyOutcome::new(
CopyStats {
bytes: 11,
..CopyStats::default()
},
CopyMethod::Native,
AchievedAtomicity::Atomic,
);
assert!(
bytes_exceeded
.contract_violation(&CopyOptions::default().with_max_bytes(Some(10)))
.is_some()
);
let entries_exceeded = CopyOutcome::new(
CopyStats {
files: 2,
..CopyStats::default()
},
CopyMethod::Native,
AchievedAtomicity::Atomic,
);
assert!(
entries_exceeded
.contract_violation(&CopyOptions::tree().with_max_entries(Some(1)))
.is_some()
);
}
}