From 28162bde5c0678014609cb8317f4fcf5a4373060 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 04:43:29 +0100 Subject: [PATCH] move split types module into submodules --- src/lib.rs | 2 - src/types.rs | 464 ++--------------------------------- src/types/body.rs | 36 +++ src/types/meta.rs | 135 ++++++++++ src/types/request.rs | 74 ++++++ src/types/response.rs | 64 +++++ src/types/response_header.rs | 75 ++++++ src/types/status.rs | 82 +++++++ 8 files changed, 480 insertions(+), 452 deletions(-) create mode 100644 src/types/body.rs create mode 100644 src/types/meta.rs create mode 100644 src/types/request.rs create mode 100644 src/types/response.rs create mode 100644 src/types/response_header.rs create mode 100644 src/types/status.rs diff --git a/src/lib.rs b/src/lib.rs index c217c79..e3b1bc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc}; use futures::{future::BoxFuture, FutureExt}; -use mime::Mime; use tokio::{ prelude::*, io::{self, BufStream}, @@ -22,7 +21,6 @@ pub mod util; pub use mime; pub use uriparse as uri; pub use types::*; -pub use rustls::Certificate; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; diff --git a/src/types.rs b/src/types.rs index 9b2d538..b52d0da 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,456 +1,20 @@ -use std::ops; -use anyhow::*; -use mime::Mime; -use percent_encoding::percent_decode_str; -use tokio::{io::AsyncRead, fs::File}; -use uriparse::URIReference; -use rustls::Certificate; +pub use ::mime::Mime; +pub use rustls::Certificate; -pub struct Request { - uri: URIReference<'static>, - input: Option, - certificate: Option, -} +mod meta; +pub use self::meta::Meta; -impl Request { - pub fn from_uri(uri: URIReference<'static>) -> Result { - Self::with_certificate(uri, None) - } +mod request; +pub use request::Request; - pub fn with_certificate( - mut uri: URIReference<'static>, - certificate: Option - ) -> Result { - uri.normalize(); +mod response_header; +pub use response_header::ResponseHeader; - let input = match uri.query() { - None => None, - Some(query) => { - let input = percent_decode_str(query.as_str()) - .decode_utf8() - .context("Request URI query contains invalid UTF-8")? - .into_owned(); - Some(input) - } - }; +mod status; +pub use status::{Status, StatusCategory}; - Ok(Self { - uri, - input, - certificate, - }) - } +mod response; +pub use response::Response; - pub fn uri(&self) -> &URIReference { - &self.uri - } - - pub fn path_segments(&self) -> Vec { - self.uri() - .path() - .segments() - .iter() - .map(|segment| percent_decode_str(segment.as_str()).decode_utf8_lossy().into_owned()) - .collect::>() - } - - pub fn input(&self) -> Option<&str> { - self.input.as_deref() - } - - pub fn set_cert(&mut self, cert: Option) { - self.certificate = cert; - } - - pub fn certificate(&self) -> Option<&Certificate> { - self.certificate.as_ref() - } -} - -impl ops::Deref for Request { - type Target = URIReference<'static>; - - fn deref(&self) -> &Self::Target { - &self.uri - } -} - -#[derive(Debug,Clone)] -pub struct ResponseHeader { - pub status: Status, - pub meta: Meta, -} - -impl ResponseHeader { - pub fn input(prompt: impl AsRef + Into) -> Result { - Ok(Self { - status: Status::INPUT, - meta: Meta::new(prompt).context("Invalid input prompt")?, - }) - } - - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { - Self { - status: Status::INPUT, - meta: Meta::new_lossy(prompt), - } - } - - pub fn success(mime: &Mime) -> Self { - Self { - status: Status::SUCCESS, - meta: Meta::new_lossy(mime.to_string()), - } - } - - pub fn server_error(reason: impl AsRef + Into) -> Result { - Ok(Self { - status: Status::PERMANENT_FAILURE, - meta: Meta::new(reason).context("Invalid server error reason")?, - }) - } - - pub fn server_error_lossy(reason: impl AsRef + Into) -> Self { - Self { - status: Status::PERMANENT_FAILURE, - meta: Meta::new_lossy(reason), - } - } - - pub fn not_found() -> Self { - Self { - status: Status::NOT_FOUND, - meta: Meta::new_lossy("Not found"), - } - } - - pub fn client_certificate_required() -> Self { - Self { - status: Status::CLIENT_CERTIFICATE_REQUIRED, - meta: Meta::new_lossy("No certificate provided"), - } - } - - pub fn certificate_not_authorized() -> Self { - Self { - status: Status::CERTIFICATE_NOT_AUTHORIZED, - meta: Meta::new_lossy("Your certificate is not authorized to view this content"), - } - } - - pub fn status(&self) -> &Status { - &self.status - } - - pub fn meta(&self) -> &Meta { - &self.meta - } -} - -#[derive(Debug,Copy,Clone,PartialEq,Eq)] -pub struct Status(u8); - -impl Status { - pub const INPUT: Self = Self(10); - pub const SENSITIVE_INPUT: Self = Self(11); - pub const SUCCESS: Self = Self(20); - pub const REDIRECT_TEMPORARY: Self = Self(30); - pub const REDIRECT_PERMANENT: Self = Self(31); - pub const TEMPORARY_FAILURE: Self = Self(40); - pub const SERVER_UNAVAILABLE: Self = Self(41); - pub const CGI_ERROR: Self = Self(42); - pub const PROXY_ERROR: Self = Self(43); - pub const SLOW_DOWN: Self = Self(44); - pub const PERMANENT_FAILURE: Self = Self(50); - pub const NOT_FOUND: Self = Self(51); - pub const GONE: Self = Self(52); - pub const PROXY_REQUEST_REFUSED: Self = Self(53); - pub const BAD_REQUEST: Self = Self(59); - pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60); - pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61); - pub const CERTIFICATE_NOT_VALID: Self = Self(62); - - pub fn code(&self) -> u8 { - self.0 - } - - pub fn is_success(&self) -> bool { - self.category().is_success() - } - - pub fn category(&self) -> StatusCategory { - let class = self.0 / 10; - - match class { - 1 => StatusCategory::Input, - 2 => StatusCategory::Success, - 3 => StatusCategory::Redirect, - 4 => StatusCategory::TemporaryFailure, - 5 => StatusCategory::PermanentFailure, - 6 => StatusCategory::ClientCertificateRequired, - _ => StatusCategory::PermanentFailure, - } - } -} - -#[derive(Copy,Clone,PartialEq,Eq)] -pub enum StatusCategory { - Input, - Success, - Redirect, - TemporaryFailure, - PermanentFailure, - ClientCertificateRequired, -} - -impl StatusCategory { - pub fn is_input(&self) -> bool { - *self == Self::Input - } - - pub fn is_success(&self) -> bool { - *self == Self::Success - } - - pub fn redirect(&self) -> bool { - *self == Self::Redirect - } - - pub fn is_temporary_failure(&self) -> bool { - *self == Self::TemporaryFailure - } - - pub fn is_permanent_failure(&self) -> bool { - *self == Self::PermanentFailure - } - - pub fn is_client_certificate_required(&self) -> bool { - *self == Self::ClientCertificateRequired - } -} - -#[derive(Debug,Clone,PartialEq,Eq,Default)] -pub struct Meta(String); - -impl Meta { - pub const MAX_LEN: usize = 1024; - - /// Creates a new "Meta" string. Fails if `meta` contains `\n`. - pub fn new(meta: impl AsRef + Into) -> Result { - ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); - ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); - - Ok(Self(meta.into())) - } - - /// Cretaes a new "Meta" string. Truncates `meta` to before the first occurrence of `\n`. - pub fn new_lossy(meta: impl AsRef + Into) -> Self { - let meta = meta.as_ref(); - let truncate_pos = meta.char_indices().position(|(i, ch)| { - let is_newline = ch == '\n'; - let exceeds_limit = (i + ch.len_utf8()) > Self::MAX_LEN; - - is_newline || exceeds_limit - }); - - let meta: String = match truncate_pos { - None => meta.into(), - Some(truncate_pos) => meta.get(..truncate_pos).expect("northstar BUG").into(), - }; - - Self(meta) - } - - pub fn empty() -> Self { - Self::default() - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_mime(&self) -> Result { - let mime = self.as_str().parse::() - .context("Meta is not a valid MIME")?; - Ok(mime) - } -} - -pub struct Response { - header: ResponseHeader, - body: Option, -} - -impl Response { - pub fn new(header: ResponseHeader) -> Self { - Self { - header, - body: None, - } - } - - pub fn input(prompt: impl AsRef + Into) -> Result { - let header = ResponseHeader::input(prompt)?; - Ok(Self::new(header)) - } - - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { - let header = ResponseHeader::input_lossy(prompt); - Self::new(header) - } - - pub fn success(mime: &Mime) -> Self { - let header = ResponseHeader::success(&mime); - Self::new(header) - } - - pub fn server_error(reason: impl AsRef + Into) -> Result { - let header = ResponseHeader::server_error(reason)?; - Ok(Self::new(header)) - } - - pub fn not_found() -> Self { - let header = ResponseHeader::not_found(); - Self::new(header) - } - - pub fn client_certificate_required() -> Self { - let header = ResponseHeader::client_certificate_required(); - Self::new(header) - } - - pub fn certificate_not_authorized() -> Self { - let header = ResponseHeader::certificate_not_authorized(); - Self::new(header) - } - - pub fn with_body(mut self, body: impl Into) -> Self { - self.body = Some(body.into()); - self - } - - pub fn header(&self) -> &ResponseHeader { - &self.header - } - - pub fn take_body(&mut self) -> Option { - self.body.take() - } -} - -pub enum Body { - Bytes(Vec), - Reader(Box), -} - -impl From> for Body { - fn from(bytes: Vec) -> Self { - Self::Bytes(bytes) - } -} - -impl<'a> From<&'a [u8]> for Body { - fn from(bytes: &[u8]) -> Self { - Self::Bytes(bytes.to_owned()) - } -} - -impl From for Body { - fn from(text: String) -> Self { - Self::Bytes(text.into_bytes()) - } -} - -impl<'a> From<&'a str> for Body { - fn from(text: &str) -> Self { - Self::Bytes(text.to_owned().into_bytes()) - } -} - -impl From for Body { - fn from(file: File) -> Self { - Self::Reader(Box::new(file)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::iter::repeat; - - #[test] - fn meta_new_rejects_newlines() { - let meta = "foo\nbar"; - let meta = Meta::new(meta); - - assert!(meta.is_err()); - } - - #[test] - fn meta_new_accepts_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN).collect(); - let meta = Meta::new(meta); - - assert!(meta.is_ok()); - } - - #[test] - fn meta_new_rejects_exceeding_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); - let meta = Meta::new(meta); - - assert!(meta.is_err()); - } - - #[test] - fn meta_new_lossy_truncates() { - let meta = "foo\r\nbar\nquux"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), "foo\r"); - } - - #[test] - fn meta_new_lossy_no_truncate() { - let meta = "foo bar\r"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), "foo bar\r"); - } - - #[test] - fn meta_new_lossy_empty() { - let meta = ""; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), ""); - } - - #[test] - fn meta_new_lossy_truncates_to_empty() { - let meta = "\n\n\n"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), ""); - } - - #[test] - fn meta_new_lossy_truncates_to_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str().len(), Meta::MAX_LEN); - } - - #[test] - fn meta_new_lossy_truncates_multi_byte_sequences() { - let mut meta: String = repeat('x').take(Meta::MAX_LEN - 1).collect(); - meta.push('🦀'); - - assert_eq!(meta.len(), Meta::MAX_LEN + 3); - - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str().len(), Meta::MAX_LEN - 1); - } -} +mod body; +pub use body::Body; diff --git a/src/types/body.rs b/src/types/body.rs new file mode 100644 index 0000000..7e697b5 --- /dev/null +++ b/src/types/body.rs @@ -0,0 +1,36 @@ +use tokio::{io::AsyncRead, fs::File}; + +pub enum Body { + Bytes(Vec), + Reader(Box), +} + +impl From> for Body { + fn from(bytes: Vec) -> Self { + Self::Bytes(bytes) + } +} + +impl<'a> From<&'a [u8]> for Body { + fn from(bytes: &[u8]) -> Self { + Self::Bytes(bytes.to_owned()) + } +} + +impl From for Body { + fn from(text: String) -> Self { + Self::Bytes(text.into_bytes()) + } +} + +impl<'a> From<&'a str> for Body { + fn from(text: &str) -> Self { + Self::Bytes(text.to_owned().into_bytes()) + } +} + +impl From for Body { + fn from(file: File) -> Self { + Self::Reader(Box::new(file)) + } +} diff --git a/src/types/meta.rs b/src/types/meta.rs new file mode 100644 index 0000000..144fc13 --- /dev/null +++ b/src/types/meta.rs @@ -0,0 +1,135 @@ +use anyhow::*; +use mime::Mime; + +#[derive(Debug,Clone,PartialEq,Eq,Default)] +pub struct Meta(String); + +impl Meta { + pub const MAX_LEN: usize = 1024; + + /// Creates a new "Meta" string. + /// Fails if `meta` contains `\n`. + pub fn new(meta: impl AsRef + Into) -> Result { + ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); + ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); + + Ok(Self(meta.into())) + } + + /// Creates a new "Meta" string. + /// Truncates `meta` to before: + /// - the first occurrence of `\n` + /// - the character that makes `meta` exceed `Meta::MAX_LEN` + pub fn new_lossy(meta: impl AsRef + Into) -> Self { + let meta = meta.as_ref(); + let truncate_pos = meta.char_indices().position(|(i, ch)| { + let is_newline = ch == '\n'; + let exceeds_limit = (i + ch.len_utf8()) > Self::MAX_LEN; + + is_newline || exceeds_limit + }); + + let meta: String = match truncate_pos { + None => meta.into(), + Some(truncate_pos) => meta.get(..truncate_pos).expect("northstar BUG").into(), + }; + + Self(meta) + } + + pub fn empty() -> Self { + Self::default() + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_mime(&self) -> Result { + let mime = self.as_str().parse::() + .context("Meta is not a valid MIME")?; + Ok(mime) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::iter::repeat; + + #[test] + fn new_rejects_newlines() { + let meta = "foo\nbar"; + let meta = Meta::new(meta); + + assert!(meta.is_err()); + } + + #[test] + fn new_accepts_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN).collect(); + let meta = Meta::new(meta); + + assert!(meta.is_ok()); + } + + #[test] + fn new_rejects_exceeding_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); + let meta = Meta::new(meta); + + assert!(meta.is_err()); + } + + #[test] + fn new_lossy_truncates() { + let meta = "foo\r\nbar\nquux"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), "foo\r"); + } + + #[test] + fn new_lossy_no_truncate() { + let meta = "foo bar\r"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), "foo bar\r"); + } + + #[test] + fn new_lossy_empty() { + let meta = ""; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), ""); + } + + #[test] + fn new_lossy_truncates_to_empty() { + let meta = "\n\n\n"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), ""); + } + + #[test] + fn new_lossy_truncates_to_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str().len(), Meta::MAX_LEN); + } + + #[test] + fn new_lossy_truncates_multi_byte_sequences() { + let mut meta: String = repeat('x').take(Meta::MAX_LEN - 1).collect(); + meta.push('🦀'); + + assert_eq!(meta.len(), Meta::MAX_LEN + 3); + + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str().len(), Meta::MAX_LEN - 1); + } +} diff --git a/src/types/request.rs b/src/types/request.rs new file mode 100644 index 0000000..a99ea2f --- /dev/null +++ b/src/types/request.rs @@ -0,0 +1,74 @@ +use std::ops; +use anyhow::*; +use percent_encoding::percent_decode_str; +use uriparse::URIReference; +use rustls::Certificate; + +pub struct Request { + uri: URIReference<'static>, + input: Option, + certificate: Option, +} + +impl Request { + pub fn from_uri(uri: URIReference<'static>) -> Result { + Self::with_certificate(uri, None) + } + + pub fn with_certificate( + mut uri: URIReference<'static>, + certificate: Option + ) -> Result { + uri.normalize(); + + let input = match uri.query() { + None => None, + Some(query) => { + let input = percent_decode_str(query.as_str()) + .decode_utf8() + .context("Request URI query contains invalid UTF-8")? + .into_owned(); + Some(input) + } + }; + + Ok(Self { + uri, + input, + certificate, + }) + } + + pub fn uri(&self) -> &URIReference { + &self.uri + } + + pub fn path_segments(&self) -> Vec { + self.uri() + .path() + .segments() + .iter() + .map(|segment| percent_decode_str(segment.as_str()).decode_utf8_lossy().into_owned()) + .collect::>() + } + + pub fn input(&self) -> Option<&str> { + self.input.as_deref() + } + + pub fn set_cert(&mut self, cert: Option) { + self.certificate = cert; + } + + pub fn certificate(&self) -> Option<&Certificate> { + self.certificate.as_ref() + } +} + +impl ops::Deref for Request { + type Target = URIReference<'static>; + + fn deref(&self) -> &Self::Target { + &self.uri + } +} diff --git a/src/types/response.rs b/src/types/response.rs new file mode 100644 index 0000000..f2389a0 --- /dev/null +++ b/src/types/response.rs @@ -0,0 +1,64 @@ +use anyhow::*; +use crate::types::{ResponseHeader, Body, Mime}; + +pub struct Response { + header: ResponseHeader, + body: Option, +} + +impl Response { + pub fn new(header: ResponseHeader) -> Self { + Self { + header, + body: None, + } + } + + pub fn input(prompt: impl AsRef + Into) -> Result { + let header = ResponseHeader::input(prompt)?; + Ok(Self::new(header)) + } + + pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + let header = ResponseHeader::input_lossy(prompt); + Self::new(header) + } + + pub fn success(mime: &Mime) -> Self { + let header = ResponseHeader::success(&mime); + Self::new(header) + } + + pub fn server_error(reason: impl AsRef + Into) -> Result { + let header = ResponseHeader::server_error(reason)?; + Ok(Self::new(header)) + } + + pub fn not_found() -> Self { + let header = ResponseHeader::not_found(); + Self::new(header) + } + + pub fn client_certificate_required() -> Self { + let header = ResponseHeader::client_certificate_required(); + Self::new(header) + } + + pub fn certificate_not_authorized() -> Self { + let header = ResponseHeader::certificate_not_authorized(); + Self::new(header) + } + + pub fn with_body(mut self, body: impl Into) -> Self { + self.body = Some(body.into()); + self + } + + pub fn header(&self) -> &ResponseHeader { + &self.header + } + + pub fn take_body(&mut self) -> Option { + self.body.take() + } +} diff --git a/src/types/response_header.rs b/src/types/response_header.rs new file mode 100644 index 0000000..8307707 --- /dev/null +++ b/src/types/response_header.rs @@ -0,0 +1,75 @@ +use anyhow::*; +use mime::Mime; +use crate::types::{Status, Meta}; + +#[derive(Debug,Clone)] +pub struct ResponseHeader { + pub status: Status, + pub meta: Meta, +} + +impl ResponseHeader { + pub fn input(prompt: impl AsRef + Into) -> Result { + Ok(Self { + status: Status::INPUT, + meta: Meta::new(prompt).context("Invalid input prompt")?, + }) + } + + pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + Self { + status: Status::INPUT, + meta: Meta::new_lossy(prompt), + } + } + + pub fn success(mime: &Mime) -> Self { + Self { + status: Status::SUCCESS, + meta: Meta::new_lossy(mime.to_string()), + } + } + + pub fn server_error(reason: impl AsRef + Into) -> Result { + Ok(Self { + status: Status::PERMANENT_FAILURE, + meta: Meta::new(reason).context("Invalid server error reason")?, + }) + } + + pub fn server_error_lossy(reason: impl AsRef + Into) -> Self { + Self { + status: Status::PERMANENT_FAILURE, + meta: Meta::new_lossy(reason), + } + } + + pub fn not_found() -> Self { + Self { + status: Status::NOT_FOUND, + meta: Meta::new_lossy("Not found"), + } + } + + pub fn client_certificate_required() -> Self { + Self { + status: Status::CLIENT_CERTIFICATE_REQUIRED, + meta: Meta::new_lossy("No certificate provided"), + } + } + + pub fn certificate_not_authorized() -> Self { + Self { + status: Status::CERTIFICATE_NOT_AUTHORIZED, + meta: Meta::new_lossy("Your certificate is not authorized to view this content"), + } + } + + pub fn status(&self) -> &Status { + &self.status + } + + pub fn meta(&self) -> &Meta { + &self.meta + } +} diff --git a/src/types/status.rs b/src/types/status.rs new file mode 100644 index 0000000..a06e9f4 --- /dev/null +++ b/src/types/status.rs @@ -0,0 +1,82 @@ + +#[derive(Debug,Copy,Clone,PartialEq,Eq)] +pub struct Status(u8); + +impl Status { + pub const INPUT: Self = Self(10); + pub const SENSITIVE_INPUT: Self = Self(11); + pub const SUCCESS: Self = Self(20); + pub const REDIRECT_TEMPORARY: Self = Self(30); + pub const REDIRECT_PERMANENT: Self = Self(31); + pub const TEMPORARY_FAILURE: Self = Self(40); + pub const SERVER_UNAVAILABLE: Self = Self(41); + pub const CGI_ERROR: Self = Self(42); + pub const PROXY_ERROR: Self = Self(43); + pub const SLOW_DOWN: Self = Self(44); + pub const PERMANENT_FAILURE: Self = Self(50); + pub const NOT_FOUND: Self = Self(51); + pub const GONE: Self = Self(52); + pub const PROXY_REQUEST_REFUSED: Self = Self(53); + pub const BAD_REQUEST: Self = Self(59); + pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60); + pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61); + pub const CERTIFICATE_NOT_VALID: Self = Self(62); + + pub fn code(&self) -> u8 { + self.0 + } + + pub fn is_success(&self) -> bool { + self.category().is_success() + } + + pub fn category(&self) -> StatusCategory { + let class = self.0 / 10; + + match class { + 1 => StatusCategory::Input, + 2 => StatusCategory::Success, + 3 => StatusCategory::Redirect, + 4 => StatusCategory::TemporaryFailure, + 5 => StatusCategory::PermanentFailure, + 6 => StatusCategory::ClientCertificateRequired, + _ => StatusCategory::PermanentFailure, + } + } +} + +#[derive(Copy,Clone,PartialEq,Eq)] +pub enum StatusCategory { + Input, + Success, + Redirect, + TemporaryFailure, + PermanentFailure, + ClientCertificateRequired, +} + +impl StatusCategory { + pub fn is_input(&self) -> bool { + *self == Self::Input + } + + pub fn is_success(&self) -> bool { + *self == Self::Success + } + + pub fn redirect(&self) -> bool { + *self == Self::Redirect + } + + pub fn is_temporary_failure(&self) -> bool { + *self == Self::TemporaryFailure + } + + pub fn is_permanent_failure(&self) -> bool { + *self == Self::PermanentFailure + } + + pub fn is_client_certificate_required(&self) -> bool { + *self == Self::ClientCertificateRequired + } +}