1use std::fmt;
4use std::str::FromStr;
5
6use crate::error::{OssError, OssErrorKind};
7
8#[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 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
62pub struct HttpHeaders {
64 pub(crate) inner: http::HeaderMap,
65}
66
67impl HttpHeaders {
68 pub fn new() -> Self {
70 Self {
71 inner: http::HeaderMap::new(),
72 }
73 }
74
75 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 pub fn get(&self, key: impl AsRef<str>) -> Option<&http::HeaderValue> {
90 self.inner.get(key.as_ref())
91 }
92
93 pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
95 self.inner.contains_key(key.as_ref())
96 }
97
98 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#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ContentType {
121 inner: String,
122}
123
124impl ContentType {
125 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 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 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}