Skip to main content

aliyun_oss/types/
object.rs

1//! Object key and ETag newtype wrappers.
2
3use std::fmt;
4
5use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
6
7const MAX_OBJECT_KEY_LENGTH: usize = 1024;
8
9/// A validated OSS object key (non-empty, max 1024 characters).
10#[derive(Clone, PartialEq, Eq, Hash)]
11pub struct ObjectKey(String);
12
13impl ObjectKey {
14    /// Creates a new `ObjectKey` after validating the input.
15    pub fn new(key: impl Into<String>) -> Result<Self> {
16        let key = key.into();
17        validate_object_key(&key)?;
18        Ok(Self(key))
19    }
20
21    /// Returns the object key as a string slice.
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25
26    /// Returns the parent directory path, if any.
27    pub fn parent(&self) -> Option<&str> {
28        let path = &self.0;
29        if let Some(pos) = path.rfind('/') {
30            Some(&path[..=pos])
31        } else {
32            None
33        }
34    }
35
36    /// Returns the file name component of the object key.
37    pub fn file_name(&self) -> &str {
38        let path = &self.0;
39        if let Some(pos) = path.rfind('/') {
40            &path[pos + 1..]
41        } else {
42            path
43        }
44    }
45}
46
47impl fmt::Debug for ObjectKey {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.debug_tuple("ObjectKey").field(&self.0).finish()
50    }
51}
52
53impl fmt::Display for ObjectKey {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(&self.0)
56    }
57}
58
59fn validate_object_key(key: &str) -> Result<()> {
60    if key.is_empty() {
61        return Err(OssError {
62            kind: OssErrorKind::ValidationError,
63            context: Box::new(ErrorContext {
64                operation: Some("validate ObjectKey: empty".into()),
65                object_key: Some(key.to_string()),
66                ..Default::default()
67            }),
68            source: None,
69        });
70    }
71
72    if key.len() > MAX_OBJECT_KEY_LENGTH {
73        return Err(OssError {
74            kind: OssErrorKind::ValidationError,
75            context: Box::new(ErrorContext {
76                operation: Some(format!(
77                    "validate ObjectKey length: {} (max {MAX_OBJECT_KEY_LENGTH})",
78                    key.len()
79                )),
80                object_key: Some(key.to_string()),
81                ..Default::default()
82            }),
83            source: None,
84        });
85    }
86
87    Ok(())
88}
89
90/// An entity tag (ETag) for object versioning, returned from OSS responses.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ETag(String);
93
94impl ETag {
95    /// Creates a new `ETag` from a raw string value.
96    pub fn new(tag: impl Into<String>) -> Self {
97        Self(tag.into())
98    }
99
100    /// Parses an ETag from an HTTP response header value, stripping surrounding quotes.
101    pub fn from_header(value: &str) -> Option<Self> {
102        let trimmed = value.trim();
103        if trimmed.len() < 2 {
104            return None;
105        }
106        if trimmed.starts_with('"') && trimmed.ends_with('"') {
107            Some(Self(trimmed[1..trimmed.len() - 1].to_string()))
108        } else {
109            Some(Self(trimmed.to_string()))
110        }
111    }
112
113    /// Returns the ETag as a string slice (without quotes).
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117}
118
119impl fmt::Display for ETag {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "\"{}\"", self.0)
122    }
123}
124
125/// Represents an OSS object owner identity.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Owner {
128    pub id: String,
129    pub display_name: Option<String>,
130}
131
132impl Owner {
133    /// Creates a new `Owner` with the given ID.
134    pub fn new(id: impl Into<String>) -> Self {
135        Self {
136            id: id.into(),
137            display_name: None,
138        }
139    }
140
141    /// Sets the display name for the owner.
142    pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
143        self.display_name = Some(name.into());
144        self
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn object_key_new_valid() {
154        assert!(ObjectKey::new("hello.txt").is_ok());
155        assert!(ObjectKey::new("a/b/c/d.jpg").is_ok());
156        assert!(ObjectKey::new("中文/文件名.jpg").is_ok());
157    }
158
159    #[test]
160    fn object_key_rejects_empty() {
161        assert!(ObjectKey::new("").is_err());
162    }
163
164    #[test]
165    fn object_key_rejects_over_max_length() {
166        assert!(ObjectKey::new("a".repeat(1025)).is_err());
167    }
168
169    #[test]
170    fn object_key_accepts_exact_max_length() {
171        assert!(ObjectKey::new("a".repeat(1024)).is_ok());
172    }
173
174    #[test]
175    fn object_key_parent_returns_directory() {
176        let key = ObjectKey::new("a/b/c.txt").unwrap();
177        assert_eq!(key.parent(), Some("a/b/"));
178    }
179
180    #[test]
181    fn object_key_parent_no_directory() {
182        let key = ObjectKey::new("file.txt").unwrap();
183        assert_eq!(key.parent(), None);
184    }
185
186    #[test]
187    fn object_key_file_name_extracts_name() {
188        let key = ObjectKey::new("a/b/c.txt").unwrap();
189        assert_eq!(key.file_name(), "c.txt");
190        let key2 = ObjectKey::new("noext").unwrap();
191        assert_eq!(key2.file_name(), "noext");
192    }
193
194    #[test]
195    fn etag_from_response_header_with_quotes() {
196        let etag = ETag::from_header("\"abc123\"").unwrap();
197        assert_eq!(etag.as_str(), "abc123");
198    }
199
200    #[test]
201    fn etag_from_response_header_without_quotes() {
202        let etag = ETag::from_header("abc123").unwrap();
203        assert_eq!(etag.as_str(), "abc123");
204    }
205
206    #[test]
207    fn etag_from_response_header_empty() {
208        assert!(ETag::from_header("").is_none());
209    }
210
211    #[test]
212    fn etag_display_includes_quotes() {
213        let etag = ETag::new("abc123");
214        assert_eq!(etag.to_string(), "\"abc123\"");
215    }
216
217    #[test]
218    fn owner_new() {
219        let owner = Owner::new("owner-id");
220        assert_eq!(owner.id, "owner-id");
221        assert!(owner.display_name.is_none());
222    }
223
224    #[test]
225    fn owner_with_display_name() {
226        let owner = Owner::new("owner-id").with_display_name("Owner Name");
227        assert_eq!(owner.id, "owner-id");
228        assert_eq!(owner.display_name.as_deref(), Some("Owner Name"));
229    }
230
231    #[test]
232    fn object_key_send_sync() {
233        fn assert_send_sync<T: Send + Sync>() {}
234        assert_send_sync::<ObjectKey>();
235        assert_send_sync::<ETag>();
236        assert_send_sync::<Owner>();
237    }
238}