aliyun_oss/types/
object.rs1use std::fmt;
4
5use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
6
7const MAX_OBJECT_KEY_LENGTH: usize = 1024;
8
9#[derive(Clone, PartialEq, Eq, Hash)]
11pub struct ObjectKey(String);
12
13impl ObjectKey {
14 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 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25
26 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 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#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ETag(String);
93
94impl ETag {
95 pub fn new(tag: impl Into<String>) -> Self {
97 Self(tag.into())
98 }
99
100 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 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#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Owner {
128 pub id: String,
129 pub display_name: Option<String>,
130}
131
132impl Owner {
133 pub fn new(id: impl Into<String>) -> Self {
135 Self {
136 id: id.into(),
137 display_name: None,
138 }
139 }
140
141 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}