Skip to main content

aliyun_oss/types/
http_types.rs

1//! HTTP method and status code wrappers.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::error::{OssError, OssErrorKind};
7
8/// Supported HTTP methods for OSS API requests.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum HttpMethod {
11    Get,
12    Put,
13    Post,
14    Delete,
15    Head,
16    Options,
17}
18
19impl HttpMethod {
20    /// Returns the uppercase HTTP method string (e.g. "GET").
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Self::Get => "GET",
24            Self::Put => "PUT",
25            Self::Post => "POST",
26            Self::Delete => "DELETE",
27            Self::Head => "HEAD",
28            Self::Options => "OPTIONS",
29        }
30    }
31}
32
33impl FromStr for HttpMethod {
34    type Err = OssError;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "GET" => Ok(Self::Get),
39            "PUT" => Ok(Self::Put),
40            "POST" => Ok(Self::Post),
41            "DELETE" => Ok(Self::Delete),
42            "HEAD" => Ok(Self::Head),
43            "OPTIONS" => Ok(Self::Options),
44            other => Err(OssError {
45                kind: OssErrorKind::ValidationError,
46                context: Box::new(crate::error::ErrorContext {
47                    operation: Some(format!("parse HttpMethod from '{}'", other)),
48                    ..Default::default()
49                }),
50                source: None,
51            }),
52        }
53    }
54}
55
56impl fmt::Display for HttpMethod {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.write_str(self.as_str())
59    }
60}
61
62/// A collection of HTTP headers backed by `http::HeaderMap`.
63pub struct HttpHeaders {
64    pub(crate) inner: http::HeaderMap,
65}
66
67impl HttpHeaders {
68    /// Creates an empty `HttpHeaders` collection.
69    pub fn new() -> Self {
70        Self {
71            inner: http::HeaderMap::new(),
72        }
73    }
74
75    /// Inserts a header name/value pair, returning any old value.
76    pub fn insert<K, V>(&mut self, key: K, value: V) -> Option<http::HeaderValue>
77    where
78        K: TryInto<http::HeaderName>,
79        K::Error: fmt::Debug,
80        V: TryInto<http::HeaderValue>,
81        V::Error: fmt::Debug,
82    {
83        let name = key.try_into().expect("valid header name");
84        let val = value.try_into().expect("valid header value");
85        self.inner.insert(name, val)
86    }
87
88    /// Gets a header value by name (case-insensitive).
89    pub fn get(&self, key: impl AsRef<str>) -> Option<&http::HeaderValue> {
90        self.inner.get(key.as_ref())
91    }
92
93    /// Returns `true` if the header collection contains the given key.
94    pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
95        self.inner.contains_key(key.as_ref())
96    }
97
98    /// Returns `true` if the header collection is empty.
99    pub fn is_empty(&self) -> bool {
100        self.inner.is_empty()
101    }
102}
103
104impl Default for HttpHeaders {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl fmt::Debug for HttpHeaders {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_map()
113            .entries(self.inner.iter().map(|(k, v)| (k.as_str(), v)))
114            .finish()
115    }
116}
117
118/// A MIME content type for HTTP requests and responses.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ContentType {
121    inner: String,
122}
123
124impl ContentType {
125    /// Common content type constants.
126    pub const TEXT_PLAIN: &'static str = "text/plain";
127    pub const TEXT_HTML: &'static str = "text/html";
128    pub const TEXT_CSS: &'static str = "text/css";
129    pub const TEXT_JAVASCRIPT: &'static str = "text/javascript";
130    pub const APPLICATION_JSON: &'static str = "application/json";
131    pub const APPLICATION_XML: &'static str = "application/xml";
132    pub const APPLICATION_OCTET_STREAM: &'static str = "application/octet-stream";
133    pub const APPLICATION_PDF: &'static str = "application/pdf";
134    pub const IMAGE_JPEG: &'static str = "image/jpeg";
135    pub const IMAGE_PNG: &'static str = "image/png";
136    pub const IMAGE_GIF: &'static str = "image/gif";
137    pub const IMAGE_WEBP: &'static str = "image/webp";
138    pub const IMAGE_SVG: &'static str = "image/svg+xml";
139    pub const VIDEO_MP4: &'static str = "video/mp4";
140    pub const AUDIO_MPEG: &'static str = "audio/mpeg";
141
142    /// Resolves a content type from a file extension.
143    pub fn from_extension(ext: &str) -> Self {
144        let mime = match ext.to_lowercase().as_str() {
145            "html" | "htm" => Self::TEXT_HTML,
146            "css" => Self::TEXT_CSS,
147            "js" | "mjs" => Self::TEXT_JAVASCRIPT,
148            "json" => Self::APPLICATION_JSON,
149            "xml" => Self::APPLICATION_XML,
150            "pdf" => Self::APPLICATION_PDF,
151            "jpg" | "jpeg" => Self::IMAGE_JPEG,
152            "png" => Self::IMAGE_PNG,
153            "gif" => Self::IMAGE_GIF,
154            "webp" => Self::IMAGE_WEBP,
155            "svg" => Self::IMAGE_SVG,
156            "mp4" => Self::VIDEO_MP4,
157            "mp3" => Self::AUDIO_MPEG,
158            "txt" | "text" => Self::TEXT_PLAIN,
159            _ => Self::APPLICATION_OCTET_STREAM,
160        };
161        ContentType {
162            inner: mime.to_string(),
163        }
164    }
165
166    /// Returns the MIME type string (e.g. "application/json").
167    pub fn as_str(&self) -> &str {
168        &self.inner
169    }
170}
171
172impl Default for ContentType {
173    fn default() -> Self {
174        ContentType {
175            inner: Self::APPLICATION_OCTET_STREAM.to_string(),
176        }
177    }
178}
179
180impl From<&str> for ContentType {
181    fn from(s: &str) -> Self {
182        ContentType {
183            inner: s.to_string(),
184        }
185    }
186}
187
188impl From<String> for ContentType {
189    fn from(s: String) -> Self {
190        ContentType { inner: s }
191    }
192}
193
194impl fmt::Display for ContentType {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.write_str(&self.inner)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn http_method_converts_to_str_correctly() {
206        assert_eq!(HttpMethod::Get.as_str(), "GET");
207        assert_eq!(HttpMethod::Put.as_str(), "PUT");
208        assert_eq!(HttpMethod::Post.as_str(), "POST");
209        assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
210        assert_eq!(HttpMethod::Head.as_str(), "HEAD");
211        assert_eq!(HttpMethod::Options.as_str(), "OPTIONS");
212    }
213
214    #[test]
215    fn http_method_from_str_case_sensitive() {
216        assert_eq!("GET".parse::<HttpMethod>().unwrap(), HttpMethod::Get);
217        assert!("get".parse::<HttpMethod>().is_err());
218        assert!("Put".parse::<HttpMethod>().is_err());
219    }
220
221    #[test]
222    fn http_method_display_matches_as_str() {
223        assert_eq!(HttpMethod::Put.to_string(), "PUT");
224        assert_eq!(HttpMethod::Head.to_string(), "HEAD");
225    }
226
227    #[test]
228    fn http_headers_case_insensitive_key() {
229        let mut headers = HttpHeaders::new();
230        headers.insert("content-type", "text/plain");
231        assert_eq!(
232            headers.get("Content-Type").unwrap().to_str().unwrap(),
233            "text/plain"
234        );
235        assert_eq!(
236            headers.get("CONTENT-TYPE").unwrap().to_str().unwrap(),
237            "text/plain"
238        );
239        assert_eq!(
240            headers.get("content-type").unwrap().to_str().unwrap(),
241            "text/plain"
242        );
243    }
244
245    #[test]
246    fn http_headers_insert_and_get() {
247        let mut headers = HttpHeaders::new();
248        headers.insert("x-oss-request-id", "abc123");
249        assert_eq!(
250            headers.get("x-oss-request-id").unwrap().to_str().unwrap(),
251            "abc123"
252        );
253        assert!(headers.contains_key("x-oss-request-id"));
254        assert!(!headers.contains_key("x-oss-nonexistent"));
255    }
256
257    #[test]
258    fn content_type_from_file_extension() {
259        assert_eq!(
260            ContentType::from_extension("json").as_str(),
261            "application/json"
262        );
263        assert_eq!(
264            ContentType::from_extension("xml").as_str(),
265            "application/xml"
266        );
267        assert_eq!(ContentType::from_extension("jpg").as_str(), "image/jpeg");
268        assert_eq!(ContentType::from_extension("jpeg").as_str(), "image/jpeg");
269        assert_eq!(ContentType::from_extension("png").as_str(), "image/png");
270        assert_eq!(ContentType::from_extension("html").as_str(), "text/html");
271        assert_eq!(ContentType::from_extension("txt").as_str(), "text/plain");
272    }
273
274    #[test]
275    fn content_type_from_unknown_extension_defaults_to_octet_stream() {
276        assert_eq!(
277            ContentType::from_extension("xyz").as_str(),
278            "application/octet-stream"
279        );
280    }
281
282    #[test]
283    fn content_type_default_is_application_octet_stream() {
284        assert_eq!(ContentType::default().as_str(), "application/octet-stream");
285    }
286
287    #[test]
288    fn content_type_from_str() {
289        let ct: ContentType = "image/webp".into();
290        assert_eq!(ct.as_str(), "image/webp");
291    }
292
293    #[test]
294    fn content_type_case_insensitive_extension() {
295        assert_eq!(
296            ContentType::from_extension("JSON").as_str(),
297            "application/json"
298        );
299        assert_eq!(ContentType::from_extension("Jpg").as_str(), "image/jpeg");
300    }
301}