This commit is contained in:
Freya Murphy 2023-07-05 21:34:20 -04:00
commit 168b8937eb
12 changed files with 1044 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "http"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
multimap = "0.9.0"

17
src/error.rs Normal file
View file

@ -0,0 +1,17 @@
#[derive(Debug, Clone)]
pub enum HTTPError {
InvalidHeaderName(String),
InvalidHeader(String),
InvalidQueryFragment(String),
InvalidQueryFragmentName(String),
InvalidEncodedString(String),
InvalidPath(String),
InvalidScheme(String),
InvalidPort(String),
InvalidURI(String),
InvalidMethod(String),
InvalidStatus(String),
MissingHeader,
MissingHeaderBreak,
InvalidBody
}

174
src/header.rs Normal file
View file

@ -0,0 +1,174 @@
use std::{string::ToString, cmp::{PartialEq, Eq}, hash::{Hash, Hasher}};
use multimap::{MultiMap, IterAll};
use crate::{error::HTTPError, parse::{TryParse, Parse}};
#[derive(Debug, Clone, Eq)]
pub struct HeaderName {
inner: String
}
impl HeaderName {
pub fn as_str(&self) -> &str {
self.inner.as_ref()
}
}
impl TryParse for HeaderName {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let name = s.into();
if name.len() < 1 {
return Err(HTTPError::InvalidHeaderName(name))
} else {
Ok(Self { inner: name })
}
}
}
impl ToString for HeaderName {
fn to_string(&self) -> String {
self.inner.clone()
}
}
impl PartialEq for HeaderName {
fn eq(&self, other: &Self) -> bool {
self.inner.eq_ignore_ascii_case(&other.inner)
}
}
impl Hash for HeaderName {
fn hash<H: Hasher>(&self, state: &mut H) {
let mut hash: i32 = 0;
for char in self.inner.chars() {
let byte = char.to_ascii_lowercase() as u8;
hash = hash ^ ((byte as i32) << 0);
hash = hash ^ ((byte as i32) << 8);
hash = hash ^ ((byte as i32) << 16);
hash = hash ^ ((byte as i32) << 24);
hash = hash % 16777213;
}
state.write_i32(hash);
}
}
#[derive(Debug, Clone)]
pub struct HeaderValue {
inner: String
}
impl HeaderValue {
pub fn as_str(&self) -> &str {
self.inner.as_ref()
}
}
impl Parse for HeaderValue {
fn parse(value: impl Into<String>) -> Self {
Self { inner: value.into() }
}
}
impl ToString for HeaderValue {
fn to_string(&self) -> String {
self.inner.clone()
}
}
pub struct Header {
pub name: HeaderName,
pub value: HeaderValue
}
impl Header {
pub fn new(name: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self {
Self { name: name.into(), value: value.into() }
}
}
impl TryParse for Header {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let Some(mid) = s.find(": ") else {
return Err(HTTPError::InvalidHeader(s.to_string()))
};
if mid == 0 || mid >= s.len() - 2 {
return Err(HTTPError::InvalidHeader(s.to_string()))
}
let name = HeaderName::try_parse(&s[0..mid])?;
let value = HeaderValue::parse(&s[(mid+2)..]);
Ok(Header::new(name, value))
}
}
impl ToString for Header {
fn to_string(&self) -> String {
let mut s = String::new();
s.push_str(self.name.as_str());
s.push_str(": ");
s.push_str(self.value.as_str());
s
}
}
impl<H,V> From<(H, V)> for Header
where
H: Into<HeaderName>,
V: Into<HeaderValue>
{
fn from(value: (H, V)) -> Self {
Self { name: value.0.into(), value: value.1.into() }
}
}
pub struct HeaderMap {
inner: MultiMap<HeaderName, Header>
}
impl HeaderMap {
pub fn new() -> Self {
Self::with_headers(Vec::new())
}
pub fn with_headers(headers: Vec<Header>) -> Self {
let mut inner = MultiMap::with_capacity(headers.len());
for header in headers {
inner.insert(header.name.clone(), Header::new(header.name, header.value));
}
Self { inner }
}
pub fn insert(&mut self, header: impl Into<Header>) {
let header = header.into();
self.inner.insert(header.name.clone(), Header::new(header.name, header.value))
}
pub fn remove(&mut self, name: &HeaderName) -> Option<Vec<Header>> {
self.inner.remove(name)
}
pub fn get(&mut self, name: &HeaderName) -> Option<&Vec<Header>> {
self.inner.get_vec(name)
}
pub fn iter(&self) -> HeaderMapIter {
HeaderMapIter { inner: self.inner.iter_all() }
}
}
pub struct HeaderMapIter<'i> {
inner: IterAll<'i, HeaderName, Vec<Header>>
}
impl<'i> Iterator for HeaderMapIter<'i> {
type Item = &'i Vec<Header>;
fn next(&mut self) -> Option<Self::Item> {
match self.inner.next() {
Some(h) => Some(h.1),
None => None,
}
}
}

9
src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
pub mod error;
pub mod header;
pub mod uri;
pub mod method;
pub mod version;
pub mod status;
pub mod request;
pub mod response;
pub mod parse;

65
src/method.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::parse::Parse;
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
Brew,
PropFind,
When,
Unkown(String)
}
impl Method {
pub fn as_str(&self) -> &str {
match self {
Self::Get => "GET",
Self::Head => "HEAD",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Connect => "CONNECT",
Self::Options => "OPTIONS",
Self::Trace => "TRACE",
Self::Patch => "PATCH",
Self::Brew => "BREW",
Self::PropFind => "PROPFIND",
Self::When => "WHEN",
Self::Unkown(ref s) => s
}
}
}
impl Parse for Method {
fn parse(s: impl Into<String>) -> Self {
let str: String = s.into();
match str.as_str() {
"GET" => Self::Get,
"HEAD" => Self::Head,
"POST" => Self::Post,
"PUT" => Self::Put,
"DELETE" => Self::Delete,
"CONNECT" => Self::Connect,
"OPTIONS" => Self::Options,
"TRACE" => Self::Trace,
"PATCH" => Self::Patch,
"BREW" => Self::Brew,
"PROPFIND" => Self::PropFind,
"WHEN" => Self::When,
_ => Self::Unkown(str)
}
}
}
impl ToString for Method {
fn to_string(&self) -> String {
self.as_str().to_string()
}
}

110
src/parse.rs Normal file
View file

@ -0,0 +1,110 @@
use crate::header::{Header, HeaderValue, HeaderName};
use crate::error::HTTPError;
use crate::method::Method;
use crate::status::Status;
use crate::uri::{QueryMap, QueryFragment, Authority, Path, Scheme, URI};
use crate::version::Version;
use crate::response::Response;
use crate::request::Request;
use std::convert::{From, TryFrom};
use std::str::FromStr;
use std::borrow::Cow;
pub trait Parse
where
Self: Sized
{
fn parse(s: impl Into<String>) -> Self;
}
pub trait TryParse
where
Self: Sized
{
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError>;
}
macro_rules! try_into {
($type:ty, $struct:ty) => {
impl TryFrom<$type> for $struct {
type Error = HTTPError;
fn try_from(value: $type) -> Result<Self, Self::Error> {
Self::try_parse(value)
}
}
};
}
macro_rules! try_into_struct {
($struct:ty) => {
impl From<$struct> for String {
fn from(value: $struct) -> Self {
value.to_string()
}
}
try_into!(String, $struct);
try_into!(&String, $struct);
try_into!(&str, $struct);
try_into!(Cow<'_, str>, $struct);
};
}
try_into_struct!(Header);
try_into_struct!(HeaderName);
try_into_struct!(QueryMap);
try_into_struct!(QueryFragment);
try_into_struct!(Authority);
try_into_struct!(Path);
try_into_struct!(URI);
try_into_struct!(Status);
macro_rules! try_into_res {
($type:ty, $struct:ty) => {
impl<T: FromStr + ToString> TryFrom<$type> for $struct {
type Error = HTTPError;
fn try_from(value: $type) -> Result<Self, Self::Error> {
Self::try_parse(value)
}
}
};
}
macro_rules! try_into_struct_res {
($struct:ty) => {
impl<T: FromStr + ToString> From<$struct> for String {
fn from(value: $struct) -> Self {
value.to_string()
}
}
try_into_res!(String, $struct);
try_into_res!(&String, $struct);
try_into_res!(&str, $struct);
try_into_res!(Cow<'_, str>, $struct);
};
}
try_into_struct_res!(Request<T>);
try_into_struct_res!(Response<T>);
macro_rules! into_struct {
($struct:ty) => {
impl<T> From<T> for $struct
where
T: Into<String>
{
fn from(value: T) -> Self {
Self::parse(value)
}
}
};
}
into_struct!(Scheme);
into_struct!(HeaderValue);
into_struct!(Method);
into_struct!(Version);

85
src/request.rs Normal file
View file

@ -0,0 +1,85 @@
use std::str::FromStr;
use crate::{version::Version, method::Method, uri::URI, header::{HeaderMap, Header}, error::HTTPError, parse::TryParse};
pub struct Request<T: ToString + FromStr> {
pub method: Method,
pub uri: URI,
pub version: Version,
pub headers: HeaderMap,
pub body: Option<T>
}
impl<T: ToString + FromStr> TryParse for Request<T> {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let mut lines = s.lines();
let Some(head) = lines.next() else {
return Err(HTTPError::MissingHeader)
};
let mut head_parts = head.split_whitespace();
let err = HTTPError::InvalidHeader(s.clone());
let Some(method_str) = head_parts.next() else { return Err(err) };
let method = method_str.into();
let Some(uri_str) = head_parts.next() else { return Err(err) };
let uri = uri_str.try_into()?;
let Some(version_str) = head_parts.next() else { return Err(err) };
let version = version_str.into();
let mut headers = HeaderMap::new();
loop {
let Some(next) = lines.next() else {
return Err(HTTPError::MissingHeaderBreak)
};
if next.len() < 1 { break }
let header = Header::try_parse(next)?;
headers.insert(header);
}
let rest: String = lines.collect();
let body;
if rest.len() < 1 {
body = None
} else {
body = match T::from_str(&rest) {
Ok(body) => Some(body),
Err(_) => return Err(HTTPError::InvalidBody)
}
}
Ok(Self { method, uri, version, headers, body })
}
}
impl<T: ToString + FromStr> ToString for Request<T> {
fn to_string(&self) -> String {
let mut s = String::new();
s.push_str(&self.method.as_str());
s.push(' ');
s.push_str(&self.uri.to_string());
s.push(' ');
s.push_str(self.version.as_str());
s.push_str("\r\n");
let iter = self.headers.iter();
for headers in iter {
for header in headers {
s.push_str(header.name.as_str());
s.push_str(": ");
s.push_str(header.value.as_str());
s.push_str("\r\n");
}
}
s.push_str("\r\n");
if let Some(body) = &self.body {
s.push_str(&body.to_string());
}
s
}
}

111
src/response.rs Normal file
View file

@ -0,0 +1,111 @@
use std::str::FromStr;
use crate::{status::{Status, self}, version::Version, header::{HeaderMap, Header}, parse::TryParse, error::HTTPError};
pub struct Response<T: ToString + FromStr> {
version: Version,
status: Status,
headers: HeaderMap,
body: Option<T>
}
impl<T: ToString + FromStr> Response<T> {
pub fn new() -> Self {
Self {
version: Version::HTTP11,
status: status::SUCCESS,
headers: HeaderMap::new(),
body: None
}
}
pub fn set_version(mut self, version: impl Into<Version>) -> Self {
self.version = version.into();
self
}
pub fn set_status(mut self, status: impl Into<Status>) -> Self {
self.status = status.into();
self
}
pub fn add_header(mut self, header: impl Into<Header>) -> Self {
self.headers.insert(header.into());
self
}
pub fn set_body(mut self, body: T) -> Self {
self.body = Some(body);
self
}
}
impl<T: ToString + FromStr> TryParse for Response<T> {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let mut lines = s.lines();
let Some(header_str) = lines.next() else {
return Err(HTTPError::MissingHeader)
};
let mut header_parts = header_str.split(" ");
let Some(version_str) = header_parts.next() else {
return Err(HTTPError::InvalidHeader(header_str.into()))
};
let version = version_str.into();
let status_str: String = header_parts.collect();
let status = status_str.try_into()?;
let mut headers = HeaderMap::new();
loop {
let Some(next) = lines.next() else {
return Err(HTTPError::MissingHeaderBreak)
};
if next.len() < 1 { break }
let header = Header::try_parse(next)?;
headers.insert(header);
}
let rest: String = lines.collect();
let body;
if rest.len() < 1 {
body = None
} else {
body = match T::from_str(&rest) {
Ok(body) => Some(body),
Err(_) => return Err(HTTPError::InvalidBody)
}
}
Ok(Self { version, status, headers, body })
}
}
impl<T: ToString + FromStr> ToString for Response<T> {
fn to_string(&self) -> String {
let mut s = String::new();
s.push_str(self.version.as_str());
s.push(' ');
s.push_str(&self.status.to_string());
s.push_str("\r\n");
let iter = self.headers.iter();
for headers in iter {
for header in headers {
s.push_str(header.name.as_str());
s.push_str(": ");
s.push_str(header.value.as_str());
s.push_str("\r\n");
}
}
s.push_str("\r\n");
if let Some(body) = &self.body {
s.push_str(&body.to_string());
}
s
}
}

83
src/status.rs Normal file
View file

@ -0,0 +1,83 @@
use std::borrow::Cow;
use crate::{parse::TryParse, error::HTTPError};
pub struct Status(u16, Cow<'static, str>);
impl Status {
pub fn new(code: u16, msg: impl Into<String>) -> Self {
Self(code, Cow::Owned(msg.into()))
}
pub fn code(&self) -> u16 {
self.0
}
pub fn msg(&self) -> &str {
self.1.as_ref()
}
}
impl ToString for Status {
fn to_string(&self) -> String {
if self.1.len() < 1 {
format!("{}", self.0)
} else {
format!("{} {}", self.0, self.1)
}
}
}
impl TryParse for Status {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let mut parts = s.split(" ");
let Some(code_str) = parts.next() else {
return Err(HTTPError::InvalidStatus(s))
};
let Ok(code) = code_str.parse::<u16>() else {
return Err(HTTPError::InvalidStatus(s))
};
let msg: String = parts.collect();
Ok(Self::new(code, msg))
}
}
impl PartialEq for Status {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
macro_rules! status_from {
($type:ty) => {
impl From<$type> for Status {
fn from(value: $type) -> Self {
Self(value as u16, Cow::Owned(String::new()))
}
}
impl<T> From<($type, T)> for Status
where
T: Into<String>
{
fn from(value: ($type, T)) -> Self {
Self(value.0 as u16, Cow::Owned(value.1.into()))
}
}
};
}
status_from!(u8);
status_from!(u16);
status_from!(u32);
status_from!(u64);
status_from!(u128);
macro_rules! status {
($name:ident, $code:literal, $msg:literal) => {
pub const $name: Status = Status($code, Cow::Borrowed($msg));
};
}
status!(SUCCESS, 200, "Ok");

345
src/uri.rs Normal file
View file

@ -0,0 +1,345 @@
use std::collections::HashMap;
use crate::{error::HTTPError, parse::{TryParse, Parse}};
#[derive(Debug, Clone)]
pub struct Path {
inner: String
}
impl Path {
pub fn as_str(&self) -> &str {
self.inner.as_ref()
}
}
impl TryParse for Path {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s: String = s.into();
let Some(first) = s.chars().nth(0) else {
return Err(HTTPError::InvalidPath(s))
};
if first != '/' {
return Err(HTTPError::InvalidPath(s))
} else {
return Ok(Self { inner: s })
}
}
}
impl ToString for Path {
fn to_string(&self) -> String {
self.as_str().to_string()
}
}
#[derive(Debug, Clone)]
pub enum Scheme {
Http,
Https,
Unkown(String)
}
impl Scheme {
pub fn as_str(&self) -> &str {
match self {
Scheme::Http => "http",
Scheme::Https => "https",
Scheme::Unkown(ref s) => s,
}
}
}
impl Parse for Scheme {
fn parse(s: impl Into<String>) -> Self {
let s = s.into();
match s.as_str() {
"http" => Self::Http,
"https" => Self::Https,
_ => Self::Unkown(s)
}
}
}
impl ToString for Scheme {
fn to_string(&self) -> String {
self.as_str().to_string()
}
}
#[derive(Debug, Clone)]
pub struct Authority {
pub sub_domain: Option<String>,
pub domain: String,
pub tld: Option<String>,
pub port: Option<u16>
}
impl TryParse for Authority {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let domain_end = s.find(":").unwrap_or(s.len());
let authority = &s[..domain_end];
let tld_start = authority.rfind(".").unwrap_or(authority.len());
let full_domain = &authority[..tld_start];
let domain_start = match full_domain.find(".") {
Some(u) => u + 1,
None => 0
};
let domain = &full_domain[domain_start..];
let sub_domain;
if domain_start > 0 {
sub_domain = Some(full_domain[..(domain_start-1)].to_string());
} else {
sub_domain = None;
}
let tld;
if tld_start < authority.len() {
tld = Some(authority[(tld_start+1)..domain_end].to_string());
} else {
tld = None;
}
let port;
if domain_end < s.len() {
let port_str = &s[(domain_end+1)..];
let num = match port_str.parse::<u16>() {
Ok(n) => n,
Err(_) => return Err(HTTPError::InvalidPort(port_str.to_string()))
};
port = Some(num);
} else {
port = None;
}
Ok(Self { sub_domain, domain: domain.to_string(), tld, port } )
}
}
impl ToString for Authority {
fn to_string(&self) -> String {
let mut s = String::new();
if let Some(sub_domain) = &self.sub_domain {
s.push_str(sub_domain);
s.push('.');
}
s.push_str(&self.domain);
if let Some(tld) = &self.tld {
s.push('.');
s.push_str(tld);
};
if let Some(port) = self.port {
s.push(':');
s.push_str(&format!("{port}"));
}
s
}
}
#[derive(Debug, Clone)]
pub struct QueryFragment {
pub name: String,
pub value: String
}
impl QueryFragment {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self, HTTPError> {
let name = name.into();
if name.len() < 1 {
return Err(HTTPError::InvalidQueryFragmentName(name))
}
Ok(Self { name, value: value.into() })
}
}
impl TryParse for QueryFragment {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let Some(index) = s.find('=') else {
return Err(HTTPError::InvalidQueryFragment(s.to_string()))
};
if index == 0 {
return Err(HTTPError::InvalidQueryFragment(s.to_string()))
}
Ok(Self {
name: decode_str(&s[0..index])?,
value: decode_str(&s[(index+1)..])?
})
}
}
impl ToString for QueryFragment {
fn to_string(&self) -> String {
let mut s = String::new();
s.push_str(&encode_str(&self.name));
s.push('=');
s.push_str(&encode_str(&self.value));
s
}
}
#[derive(Debug, Clone)]
pub struct QueryMap {
inner: HashMap<String, QueryFragment>
}
impl QueryMap {
fn new() -> Self {
Self::with_fragments(Vec::new())
}
pub fn with_fragments(fragments: Vec<QueryFragment>) -> Self {
let mut inner = HashMap::with_capacity(fragments.len());
for fragment in fragments {
inner.insert(fragment.name.clone(), fragment);
}
Self { inner }
}
pub fn insert(&mut self, fragment: &QueryFragment) {
self.inner.insert(fragment.name.clone(), fragment.clone());
}
pub fn remove(&mut self, name: &str) -> Option<QueryFragment> {
self.inner.remove(name)
}
pub fn get(&self, name: &str) -> Option<&QueryFragment> {
self.inner.get(name)
}
}
impl TryParse for QueryMap {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let parts: Vec<QueryFragment> = s.split('&').map(|f| QueryFragment::try_parse(f)).flatten().collect();
Ok(Self::with_fragments(parts))
}
}
impl ToString for QueryMap {
fn to_string(&self) -> String {
let mut s = String::new();
for (i, fragment) in self.inner.values().enumerate() {
if i == 0 {
s.push('?');
} else {
s.push('&');
}
s.push_str(&fragment.to_string());
}
s
}
}
#[derive(Debug, Clone)]
pub struct URI {
pub scheme: Option<Scheme>,
pub authority: Option<Authority>,
pub path: Path,
pub query: QueryMap
}
impl TryParse for URI {
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
let s = s.into();
let mut n = s.as_str();
let scheme: Option<Scheme>;
if let Some(scheme_end) = n.find("://") {
scheme = Some(Scheme::parse(&n[..scheme_end]));
n = &n[(scheme_end + 3)..];
} else {
scheme = None;
}
let authority_end = match n.find("/") {
Some(i) => i,
None => return Err(HTTPError::InvalidURI(s.to_string()))
};
let authority: Option<Authority>;
if authority_end == 0 {
if scheme.is_some() {
return Err(HTTPError::InvalidURI(s.to_string()))
}
authority = None;
} else {
authority = Some(Authority::try_parse(&n[..authority_end])?);
n = &n[authority_end..];
}
let path_end = n.find("?").unwrap_or(n.len());
let path = Path::try_parse(&n[..path_end])?;
let query: QueryMap;
if path_end + 1 < n.len() {
query = QueryMap::try_parse(&n[(path_end+1)..])?;
} else {
query = QueryMap::new();
}
Ok(Self { scheme, authority, path, query })
}
}
impl ToString for URI {
fn to_string(&self) -> String {
let mut s = String::new();
if let Some(scheme) = &self.scheme {
s.push_str(scheme.as_str());
}
if let Some(authority) = &self.authority {
s.push_str(&authority.to_string());
}
s.push_str(self.path.as_str());
s.push_str(&self.query.to_string());
s
}
}
fn encoded(c: char) -> bool {
match c {
'/' | '&' | '%' | '?' | '=' => true,
_ => false
}
}
pub fn encode_str(s: &str) -> String {
let mut e = String::new();
for c in s.chars() {
if encoded(c) {
e.push('%');
e.push_str(&format!("{:02X}", c as u8));
} else {
e.push(c);
}
}
e
}
pub fn decode_str(s: &str) -> Result<String, HTTPError> {
let mut e = String::new();
let mut iter = s.chars();
loop {
let Some(char) = iter.next() else { break };
if char == '%' {
let err = HTTPError::InvalidEncodedString(s.to_string());
let f = iter.next().ok_or(err.clone())?;
let s = iter.next().ok_or(err.clone())?;
let Some(fd) = f.to_digit(16) else { return Err(err) };
let Some(sd) = s.to_digit(16) else { return Err(err) };
let b = (fd * 16 + sd) as u8;
e.push(b as char);
} else {
e.push(char);
}
}
Ok(e)
}

34
src/version.rs Normal file
View file

@ -0,0 +1,34 @@
use crate::parse::Parse;
pub enum Version {
HTTP11,
HTTP2,
Unknown(String)
}
impl Version {
pub fn as_str(&self) -> &str {
match self {
Self::HTTP11 => "HTTP/1.1",
Self::HTTP2 => "HTTP/2",
Self::Unknown(ref s) => s,
}
}
}
impl Parse for Version {
fn parse(s: impl Into<String>) -> Self {
let s = s.into();
match s.as_str() {
"HTTP/1.1" => Self::HTTP11,
"HTTP/2" => Self::HTTP2,
_ => Self::Unknown(s)
}
}
}
impl ToString for Version {
fn to_string(&self) -> String {
self.as_str().to_string()
}
}