Skip to main content

rc_core/
object_lock.rs

1//! Typed Object Lock, retention, and legal-hold models.
2
3use std::fmt;
4
5use jiff::Timestamp;
6use serde::{Deserialize, Serialize};
7
8use crate::{Error, Result};
9
10/// Retention mode enforced by an Object Lock enabled bucket.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum RetentionMode {
14    /// Authorized users may shorten active retention when governance bypass is explicit.
15    Governance,
16    /// Active retention cannot be shortened or changed to another mode.
17    Compliance,
18}
19
20impl fmt::Display for RetentionMode {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        formatter.write_str(match self {
23            Self::Governance => "governance",
24            Self::Compliance => "compliance",
25        })
26    }
27}
28
29/// Unit used by a bucket's default retention rule.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "lowercase")]
32pub enum RetentionDurationUnit {
33    /// Calendar days.
34    Days,
35    /// Calendar years.
36    Years,
37}
38
39impl fmt::Display for RetentionDurationUnit {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(match self {
42            Self::Days => "days",
43            Self::Years => "years",
44        })
45    }
46}
47
48/// Positive and unambiguous bucket default retention duration.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub struct RetentionDuration {
51    /// Duration unit.
52    pub unit: RetentionDurationUnit,
53    /// Positive unit count accepted by the S3 API.
54    pub value: i32,
55}
56
57impl RetentionDuration {
58    /// Create a validated day duration.
59    pub fn days(value: i32) -> Result<Self> {
60        Self::new(RetentionDurationUnit::Days, value)
61    }
62
63    /// Create a validated year duration.
64    pub fn years(value: i32) -> Result<Self> {
65        Self::new(RetentionDurationUnit::Years, value)
66    }
67
68    fn new(unit: RetentionDurationUnit, value: i32) -> Result<Self> {
69        if value <= 0 {
70            return Err(Error::InvalidPath(
71                "Retention duration must be a positive number of days or years".to_string(),
72            ));
73        }
74        Ok(Self { unit, value })
75    }
76}
77
78/// Default retention rule applied to newly created object versions.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct DefaultRetention {
81    /// Default retention mode.
82    pub mode: RetentionMode,
83    /// Default retention duration.
84    pub duration: RetentionDuration,
85}
86
87/// Bucket Object Lock configuration.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct BucketObjectLockConfiguration {
90    /// Whether Object Lock is enabled for the bucket.
91    pub enabled: bool,
92    /// Optional default retention rule.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub default_retention: Option<DefaultRetention>,
95}
96
97/// Retention applied to one object version.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct ObjectRetention {
100    /// Retention mode.
101    pub mode: RetentionMode,
102    /// Absolute UTC instant after which retention expires.
103    pub retain_until: Timestamp,
104}
105
106/// Legal-hold status applied to one object version.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "lowercase")]
109pub enum LegalHoldStatus {
110    /// The object version is not protected by legal hold.
111    Off,
112    /// The object version is protected by legal hold.
113    On,
114}
115
116impl LegalHoldStatus {
117    /// Return the stable boolean form used by structured output.
118    pub const fn is_on(self) -> bool {
119        matches!(self, Self::On)
120    }
121}
122
123impl fmt::Display for LegalHoldStatus {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str(match self {
126            Self::Off => "off",
127            Self::On => "on",
128        })
129    }
130}
131
132/// Version selection and protected-mutation options for Object Lock operations.
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
134pub struct ObjectLockOptions {
135    /// Exact object version to read or mutate. `None` selects the current version.
136    pub version_id: Option<String>,
137    /// Explicitly request governance retention bypass for this mutation.
138    pub bypass_governance: bool,
139}
140
141impl ObjectLockOptions {
142    /// Build options while rejecting ambiguous empty version identifiers.
143    pub fn new(version_id: Option<String>, bypass_governance: bool) -> Result<Self> {
144        if version_id.as_deref().is_some_and(str::is_empty) {
145            return Err(Error::InvalidPath("Version ID cannot be empty".to_string()));
146        }
147        Ok(Self {
148            version_id,
149            bypass_governance,
150        })
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn retention_duration_is_positive_and_unambiguous() {
160        assert_eq!(
161            RetentionDuration::days(30).expect("positive days"),
162            RetentionDuration {
163                unit: RetentionDurationUnit::Days,
164                value: 30,
165            }
166        );
167        assert_eq!(
168            RetentionDuration::years(2).expect("positive years"),
169            RetentionDuration {
170                unit: RetentionDurationUnit::Years,
171                value: 2,
172            }
173        );
174        assert!(RetentionDuration::days(0).is_err());
175        assert!(RetentionDuration::years(-1).is_err());
176    }
177
178    #[test]
179    fn object_lock_options_reject_empty_versions() {
180        assert!(ObjectLockOptions::new(Some(String::new()), false).is_err());
181        assert_eq!(
182            ObjectLockOptions::new(Some("v1".to_string()), true)
183                .expect("non-empty version")
184                .version_id
185                .as_deref(),
186            Some("v1")
187        );
188    }
189}