From ea1fb8ea66687219e68dd8d22ccb0abca3143e50 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 05:13:56 +0200 Subject: [PATCH] update temporary status codes api --- src/client/connection/response/failure.rs | 42 ++++ .../connection/response/failure/temporary.rs | 231 +++++++----------- .../response/failure/temporary/cgi_error.rs | 78 ++++++ .../failure/temporary/cgi_error/error.rs | 24 ++ .../response/failure/temporary/default.rs | 78 ++++++ .../failure/temporary/default/error.rs | 24 ++ .../response/failure/temporary/error.rs | 45 +++- .../response/failure/temporary/proxy_error.rs | 78 ++++++ .../failure/temporary/proxy_error/error.rs | 24 ++ .../failure/temporary/server_unavailable.rs | 81 ++++++ .../temporary/server_unavailable/error.rs | 24 ++ .../response/failure/temporary/slow_down.rs | 81 ++++++ .../failure/temporary/slow_down/error.rs | 24 ++ 13 files changed, 682 insertions(+), 152 deletions(-) create mode 100644 src/client/connection/response/failure/temporary/cgi_error.rs create mode 100644 src/client/connection/response/failure/temporary/cgi_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/default.rs create mode 100644 src/client/connection/response/failure/temporary/default/error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable/error.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 419af23..1ace0ed 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -38,10 +38,52 @@ impl Failure { // Getters + /// Get optional message for `Self` + /// * return `None` if the message is empty pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), Self::Temporary(temporary) => temporary.message(), } } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.message_or_default(), + Self::Temporary(temporary) => temporary.message_or_default(), + } + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.as_str(), + Self::Temporary(temporary) => temporary.as_str(), + } + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Permanent(permanent) => permanent.as_bytes(), + Self::Temporary(temporary) => temporary.as_bytes(), + } + } +} + +#[test] +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Failure::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..cc20834 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -1,24 +1,31 @@ +pub mod cgi_error; +pub mod default; pub mod error; -pub use error::Error; +pub mod proxy_error; +pub mod server_unavailable; +pub mod slow_down; -const DEFAULT: (u8, &str) = (40, "Unspecified"); -const SERVER_UNAVAILABLE: (u8, &str) = (41, "Server unavailable"); -const CGI_ERROR: (u8, &str) = (42, "CGI error"); -const PROXY_ERROR: (u8, &str) = (43, "Proxy error"); -const SLOW_DOWN: (u8, &str) = (44, "Slow down"); +pub use cgi_error::CgiError; +pub use default::Default; +pub use error::Error; +pub use proxy_error::ProxyError; +pub use server_unavailable::ServerUnavailable; +pub use slow_down::SlowDown; + +const CODE: u8 = b'4'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable(ServerUnavailable), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError(CgiError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError(ProxyError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown(SlowDown), } impl Temporary { @@ -26,154 +33,94 @@ impl Temporary { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::ServerUnavailable( + ServerUnavailable::from_utf8(buffer) + .map_err(Error::ServerUnavailable)?, + )), + b'2' => Ok(Self::CgiError( + CgiError::from_utf8(buffer).map_err(Error::CgiError)?, + )), + b'3' => Ok(Self::ProxyError( + ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?, + )), + b'4' => Ok(Self::SlowDown( + SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::ServerUnavailable { message } => message, - Self::CgiError { message } => message, - Self::ProxyError { message } => message, - Self::SlowDown { message } => message, + Self::Default(default) => default.message(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message(), + Self::CgiError(cgi_error) => cgi_error.message(), + Self::ProxyError(proxy_error) => proxy_error.message(), + Self::SlowDown(slow_down) => slow_down.message(), } - .as_deref() } -} -impl std::fmt::Display for Temporary { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .1 - ) + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(), + Self::CgiError(cgi_error) => cgi_error.message_or_default(), + Self::ProxyError(proxy_error) => proxy_error.message_or_default(), + Self::SlowDown(slow_down) => slow_down.message_or_default(), + } } -} -impl std::str::FromStr for Temporary { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("40") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(), + Self::CgiError(cgi_error) => cgi_error.as_str(), + Self::ProxyError(proxy_error) => proxy_error.as_str(), + Self::SlowDown(slow_down) => slow_down.as_str(), } - if let Some(postfix) = header.strip_prefix("41") { - return Ok(Self::ServerUnavailable { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("42") { - return Ok(Self::CgiError { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("43") { - return Ok(Self::ProxyError { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("44") { - return Ok(Self::SlowDown { - message: message(postfix), - }); - } - Err(Error::Code) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(), + Self::CgiError(cgi_error) => cgi_error.as_bytes(), + Self::ProxyError(proxy_error) => proxy_error.as_bytes(), + Self::SlowDown(slow_down) => slow_down.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - - // 40 - let default = Temporary::from_str("40 Message\r\n").unwrap(); - assert_eq!(default.message(), Some("Message")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Temporary::from_str("40\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - // 41 - let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap(); - assert_eq!(server_unavailable.message(), Some("Message")); - assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - let server_unavailable = Temporary::from_str("41\r\n").unwrap(); - assert_eq!(server_unavailable.message(), None); - assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - // 42 - let cgi_error = Temporary::from_str("42 Message\r\n").unwrap(); - assert_eq!(cgi_error.message(), Some("Message")); - assert_eq!(cgi_error.to_code(), CGI_ERROR.0); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - let cgi_error = Temporary::from_str("42\r\n").unwrap(); - assert_eq!(cgi_error.message(), None); - assert_eq!(cgi_error.to_code(), CGI_ERROR.0); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - // 43 - let proxy_error = Temporary::from_str("43 Message\r\n").unwrap(); - assert_eq!(proxy_error.message(), Some("Message")); - assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - let proxy_error = Temporary::from_str("43\r\n").unwrap(); - assert_eq!(proxy_error.message(), None); - assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - // 44 - let slow_down = Temporary::from_str("44 Message\r\n").unwrap(); - assert_eq!(slow_down.message(), Some("Message")); - assert_eq!(slow_down.to_code(), SLOW_DOWN.0); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); - - let slow_down = Temporary::from_str("44\r\n").unwrap(); - assert_eq!(slow_down.message(), None); - assert_eq!(slow_down.to_code(), SLOW_DOWN.0); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Temporary::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary/cgi_error.rs b/src/client/connection/response/failure/temporary/cgi_error.rs new file mode 100644 index 0000000..8843fa9 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +pub const CODE: &[u8] = b"42"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "CGI Error"; + +/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct CgiError(String); + +impl CgiError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), Some("Message")); + assert_eq!(ce.message_or_default(), "Message"); + assert_eq!(ce.as_str(), "42 Message\r\n"); + assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes()); + + let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), None); + assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(ce.as_str(), "42\r\n"); + assert_eq!(ce.as_bytes(), "42\r\n".as_bytes()); + + // err + assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/cgi_error/error.rs b/src/client/connection/response/failure/temporary/cgi_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/default.rs b/src/client/connection/response/failure/temporary/default.rs new file mode 100644 index 0000000..e56d90b --- /dev/null +++ b/src/client/connection/response/failure/temporary/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +pub const CODE: &[u8] = b"40"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Temporary error"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "40 Message\r\n"); + assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes()); + + let d = Default::from_utf8("40\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "40\r\n"); + assert_eq!(d.as_bytes(), "40\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/default/error.rs b/src/client/connection/response/failure/temporary/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs index 5cf1cf6..afa9154 100644 --- a/src/client/connection/response/failure/temporary/error.rs +++ b/src/client/connection/response/failure/temporary/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + CgiError(super::cgi_error::Error), + Default(super::default::Error), + FirstByte(u8), + ProxyError(super::proxy_error::Error), + SecondByte(u8), + ServerUnavailable(super::server_unavailable::Error), + SlowDown(super::slow_down::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::CgiError(e) => { + write!(f, "CgiError parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::ProxyError(e) => { + write!(f, "ProxyError parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::ServerUnavailable(e) => { + write!(f, "ServerUnavailable parse error: {e}") + } + Self::SlowDown(e) => { + write!(f, "SlowDown parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/temporary/proxy_error.rs b/src/client/connection/response/failure/temporary/proxy_error.rs new file mode 100644 index 0000000..1264c34 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +pub const CODE: &[u8] = b"43"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy error"; + +/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyError(String); + +impl ProxyError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), Some("Message")); + assert_eq!(pe.message_or_default(), "Message"); + assert_eq!(pe.as_str(), "43 Message\r\n"); + assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes()); + + let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), None); + assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(pe.as_str(), "43\r\n"); + assert_eq!(pe.as_bytes(), "43\r\n".as_bytes()); + + // err + assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/proxy_error/error.rs b/src/client/connection/response/failure/temporary/proxy_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable.rs b/src/client/connection/response/failure/temporary/server_unavailable.rs new file mode 100644 index 0000000..f42802e --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +pub const CODE: &[u8] = b"41"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Server unavailable"; + +/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ServerUnavailable(String); + +impl ServerUnavailable { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), Some("Message")); + assert_eq!(su.message_or_default(), "Message"); + assert_eq!(su.as_str(), "41 Message\r\n"); + assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes()); + + let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), None); + assert_eq!(su.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(su.as_str(), "41\r\n"); + assert_eq!(su.as_bytes(), "41\r\n".as_bytes()); + + // err + assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable/error.rs b/src/client/connection/response/failure/temporary/server_unavailable/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/slow_down.rs b/src/client/connection/response/failure/temporary/slow_down.rs new file mode 100644 index 0000000..3ca346d --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +pub const CODE: &[u8] = b"44"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Slow down"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct SlowDown(String); + +impl SlowDown { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), Some("Message")); + assert_eq!(sd.message_or_default(), "Message"); + assert_eq!(sd.as_str(), "44 Message\r\n"); + assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes()); + + let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), None); + assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(sd.as_str(), "44\r\n"); + assert_eq!(sd.as_bytes(), "44\r\n".as_bytes()); + + // err + assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/slow_down/error.rs b/src/client/connection/response/failure/temporary/slow_down/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +}