Skip to main content

oxihttp_core/
version.rs

1//! HTTP protocol version enum.
2
3use std::fmt;
4use std::str::FromStr;
5
6/// HTTP protocol version.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum HttpVersion {
9    /// HTTP/1.0
10    Http10,
11    /// HTTP/1.1
12    Http11,
13    /// HTTP/2
14    H2,
15    /// HTTP/3
16    H3,
17}
18
19impl HttpVersion {
20    /// Returns the canonical string representation of this HTTP version.
21    ///
22    /// # Examples
23    ///
24    /// ```
25    /// use oxihttp_core::HttpVersion;
26    ///
27    /// assert_eq!(HttpVersion::Http11.as_str(), "HTTP/1.1");
28    /// assert_eq!(HttpVersion::H2.as_str(), "HTTP/2");
29    /// ```
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            HttpVersion::Http10 => "HTTP/1.0",
33            HttpVersion::Http11 => "HTTP/1.1",
34            HttpVersion::H2 => "HTTP/2",
35            HttpVersion::H3 => "HTTP/3",
36        }
37    }
38
39    /// Returns `true` if this is HTTP/1.x.
40    pub fn is_http1(&self) -> bool {
41        matches!(self, HttpVersion::Http10 | HttpVersion::Http11)
42    }
43
44    /// Returns `true` if this is HTTP/2.
45    pub fn is_http2(&self) -> bool {
46        matches!(self, HttpVersion::H2)
47    }
48
49    /// Returns `true` if this is HTTP/3.
50    pub fn is_http3(&self) -> bool {
51        matches!(self, HttpVersion::H3)
52    }
53}
54
55impl fmt::Display for HttpVersion {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str(self.as_str())
58    }
59}
60
61impl FromStr for HttpVersion {
62    type Err = crate::OxiHttpError;
63
64    /// Parses an HTTP version string, case-insensitively.
65    ///
66    /// Accepts: `"HTTP/1.0"`, `"HTTP/1.1"`, `"HTTP/2"`, `"HTTP/3"` and
67    /// lowercase variants thereof.
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        match s.to_ascii_uppercase().as_str() {
70            "HTTP/1.0" => Ok(HttpVersion::Http10),
71            "HTTP/1.1" => Ok(HttpVersion::Http11),
72            "HTTP/2" | "H2" => Ok(HttpVersion::H2),
73            "HTTP/3" | "H3" => Ok(HttpVersion::H3),
74            _ => Err(crate::OxiHttpError::InvalidHeader(format!(
75                "unknown HTTP version: {s}"
76            ))),
77        }
78    }
79}
80
81impl From<http::Version> for HttpVersion {
82    fn from(v: http::Version) -> Self {
83        match v {
84            http::Version::HTTP_10 => HttpVersion::Http10,
85            http::Version::HTTP_11 => HttpVersion::Http11,
86            http::Version::HTTP_2 => HttpVersion::H2,
87            http::Version::HTTP_3 => HttpVersion::H3,
88            // Unknown / future versions — use HTTP/1.1 as a safe default.
89            _ => HttpVersion::Http11,
90        }
91    }
92}
93
94impl From<HttpVersion> for http::Version {
95    fn from(v: HttpVersion) -> http::Version {
96        match v {
97            HttpVersion::Http10 => http::Version::HTTP_10,
98            HttpVersion::Http11 => http::Version::HTTP_11,
99            HttpVersion::H2 => http::Version::HTTP_2,
100            HttpVersion::H3 => http::Version::HTTP_3,
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_as_str() {
111        assert_eq!(HttpVersion::Http10.as_str(), "HTTP/1.0");
112        assert_eq!(HttpVersion::Http11.as_str(), "HTTP/1.1");
113        assert_eq!(HttpVersion::H2.as_str(), "HTTP/2");
114        assert_eq!(HttpVersion::H3.as_str(), "HTTP/3");
115    }
116
117    #[test]
118    fn test_display() {
119        assert_eq!(HttpVersion::Http11.to_string(), "HTTP/1.1");
120        assert_eq!(HttpVersion::H2.to_string(), "HTTP/2");
121    }
122
123    #[test]
124    fn test_from_str_canonical() {
125        assert_eq!(
126            "HTTP/1.0".parse::<HttpVersion>().unwrap(),
127            HttpVersion::Http10
128        );
129        assert_eq!(
130            "HTTP/1.1".parse::<HttpVersion>().unwrap(),
131            HttpVersion::Http11
132        );
133        assert_eq!("HTTP/2".parse::<HttpVersion>().unwrap(), HttpVersion::H2);
134        assert_eq!("HTTP/3".parse::<HttpVersion>().unwrap(), HttpVersion::H3);
135    }
136
137    #[test]
138    fn test_from_str_case_insensitive() {
139        assert_eq!(
140            "http/1.0".parse::<HttpVersion>().unwrap(),
141            HttpVersion::Http10
142        );
143        assert_eq!(
144            "http/1.1".parse::<HttpVersion>().unwrap(),
145            HttpVersion::Http11
146        );
147        assert_eq!("http/2".parse::<HttpVersion>().unwrap(), HttpVersion::H2);
148        assert_eq!("http/3".parse::<HttpVersion>().unwrap(), HttpVersion::H3);
149        assert_eq!("h2".parse::<HttpVersion>().unwrap(), HttpVersion::H2);
150        assert_eq!("h3".parse::<HttpVersion>().unwrap(), HttpVersion::H3);
151    }
152
153    #[test]
154    fn test_from_str_invalid() {
155        assert!("HTTP/4".parse::<HttpVersion>().is_err());
156        assert!("".parse::<HttpVersion>().is_err());
157        assert!("1.1".parse::<HttpVersion>().is_err());
158    }
159
160    #[test]
161    fn test_from_http_version() {
162        assert_eq!(
163            HttpVersion::from(http::Version::HTTP_10),
164            HttpVersion::Http10
165        );
166        assert_eq!(
167            HttpVersion::from(http::Version::HTTP_11),
168            HttpVersion::Http11
169        );
170        assert_eq!(HttpVersion::from(http::Version::HTTP_2), HttpVersion::H2);
171        assert_eq!(HttpVersion::from(http::Version::HTTP_3), HttpVersion::H3);
172    }
173
174    #[test]
175    fn test_into_http_version() {
176        assert_eq!(
177            http::Version::from(HttpVersion::Http10),
178            http::Version::HTTP_10
179        );
180        assert_eq!(
181            http::Version::from(HttpVersion::Http11),
182            http::Version::HTTP_11
183        );
184        assert_eq!(http::Version::from(HttpVersion::H2), http::Version::HTTP_2);
185        assert_eq!(http::Version::from(HttpVersion::H3), http::Version::HTTP_3);
186    }
187
188    #[test]
189    fn test_round_trip() {
190        for v in [
191            HttpVersion::Http10,
192            HttpVersion::Http11,
193            HttpVersion::H2,
194            HttpVersion::H3,
195        ] {
196            let http_v: http::Version = v.into();
197            let back: HttpVersion = http_v.into();
198            assert_eq!(v, back);
199        }
200    }
201
202    #[test]
203    fn test_predicates() {
204        assert!(HttpVersion::Http10.is_http1());
205        assert!(HttpVersion::Http11.is_http1());
206        assert!(!HttpVersion::H2.is_http1());
207        assert!(HttpVersion::H2.is_http2());
208        assert!(!HttpVersion::H2.is_http3());
209        assert!(HttpVersion::H3.is_http3());
210    }
211}