update permanent status codes api

This commit is contained in:
yggverse 2025-03-25 04:43:56 +02:00
parent 0c75da793f
commit e96ff688b3
14 changed files with 644 additions and 158 deletions

View file

@ -21,7 +21,7 @@ impl Failure {
/// Create new `Self` from buffer include header bytes /// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> { pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match buffer.first() { match buffer.first() {
Some(byte) => match byte { Some(b) => match b {
b'4' => match Temporary::from_utf8(buffer) { b'4' => match Temporary::from_utf8(buffer) {
Ok(input) => Ok(Self::Temporary(input)), Ok(input) => Ok(Self::Temporary(input)),
Err(e) => Err(Error::Temporary(e)), Err(e) => Err(Error::Temporary(e)),
@ -30,7 +30,7 @@ impl Failure {
Ok(failure) => Ok(Self::Permanent(failure)), Ok(failure) => Ok(Self::Permanent(failure)),
Err(e) => Err(Error::Permanent(e)), Err(e) => Err(Error::Permanent(e)),
}, },
_ => Err(Error::Code), b => Err(Error::Code(*b)),
}, },
None => Err(Error::Protocol), None => Err(Error::Protocol),
} }
@ -38,13 +38,6 @@ impl Failure {
// Getters // Getters
pub fn to_code(&self) -> u8 {
match self {
Self::Permanent(permanent) => permanent.to_code(),
Self::Temporary(temporary) => temporary.to_code(),
}
}
pub fn message(&self) -> Option<&str> { pub fn message(&self) -> Option<&str> {
match self { match self {
Self::Permanent(permanent) => permanent.message(), Self::Permanent(permanent) => permanent.message(),

View file

@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Code, Code(u8),
Permanent(super::permanent::Error), Permanent(super::permanent::Error),
Protocol, Protocol,
Temporary(super::temporary::Error), Temporary(super::temporary::Error),
@ -11,8 +11,8 @@ pub enum Error {
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
match self { match self {
Self::Code => { Self::Code(b) => {
write!(f, "Code group error") write!(f, "Unexpected status code byte: {b}")
} }
Self::Permanent(e) => { Self::Permanent(e) => {
write!(f, "Permanent failure group error: {e}") write!(f, "Permanent failure group error: {e}")

View file

@ -1,24 +1,31 @@
pub mod bad_request;
pub mod default;
pub mod error; pub mod error;
pub use error::Error; pub mod gone;
pub mod not_found;
pub mod proxy_request_refused;
const DEFAULT: (u8, &str) = (50, "Unspecified"); pub use bad_request::BadRequest;
const NOT_FOUND: (u8, &str) = (51, "Not found"); pub use default::Default;
const GONE: (u8, &str) = (52, "Gone"); pub use error::Error;
const PROXY_REQUEST_REFUSED: (u8, &str) = (53, "Proxy request refused"); pub use gone::Gone;
const BAD_REQUEST: (u8, &str) = (59, "bad-request"); pub use not_found::NotFound;
pub use proxy_request_refused::ProxyRequestRefused;
const CODE: u8 = b'5';
/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure
pub enum Permanent { pub enum Permanent {
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50
Default { message: Option<String> }, Default(Default),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found
NotFound { message: Option<String> }, NotFound(NotFound),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone
Gone { message: Option<String> }, Gone(Gone),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused
ProxyRequestRefused { message: Option<String> }, ProxyRequestRefused(ProxyRequestRefused),
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request
BadRequest { message: Option<String> }, BadRequest(BadRequest),
} }
impl Permanent { impl Permanent {
@ -26,154 +33,105 @@ impl Permanent {
/// Create new `Self` from buffer include header bytes /// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> { pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr; match buffer.first() {
match std::str::from_utf8(buffer) { Some(b) => match *b {
Ok(header) => Self::from_str(header), CODE => match buffer.get(1) {
Err(e) => Err(Error::Utf8Error(e)), Some(b) => match *b {
b'0' => Ok(Self::Default(
Default::from_utf8(buffer).map_err(Error::Default)?,
)),
b'1' => Ok(Self::NotFound(
NotFound::from_utf8(buffer).map_err(Error::NotFound)?,
)),
b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)),
b'3' => Ok(Self::ProxyRequestRefused(
ProxyRequestRefused::from_utf8(buffer)
.map_err(Error::ProxyRequestRefused)?,
)),
b'9' => Ok(Self::BadRequest(
BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?,
)),
b => Err(Error::SecondByte(b)),
},
None => Err(Error::UndefinedSecondByte),
},
b => Err(Error::FirstByte(b)),
},
None => Err(Error::UndefinedFirstByte),
} }
} }
// Getters // Getters
pub fn to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT,
Self::NotFound { .. } => NOT_FOUND,
Self::Gone { .. } => GONE,
Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED,
Self::BadRequest { .. } => BAD_REQUEST,
}
.0
}
pub fn message(&self) -> Option<&str> { pub fn message(&self) -> Option<&str> {
match self { match self {
Self::Default { message } => message, Self::Default(default) => default.message(),
Self::NotFound { message } => message, Self::NotFound(not_found) => not_found.message(),
Self::Gone { message } => message, Self::Gone(gone) => gone.message(),
Self::ProxyRequestRefused { message } => message, Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(),
Self::BadRequest { message } => message, Self::BadRequest(bad_request) => bad_request.message(),
} }
.as_deref()
} }
}
impl std::fmt::Display for Permanent { /// Get optional message for `Self`
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE`
write!( pub fn message_or_default(&self) -> &str {
f, match self {
"{}", Self::Default(default) => default.message_or_default(),
match self { Self::NotFound(not_found) => not_found.message_or_default(),
Self::Default { .. } => DEFAULT, Self::Gone(gone) => gone.message_or_default(),
Self::NotFound { .. } => NOT_FOUND, Self::ProxyRequestRefused(proxy_request_refused) => {
Self::Gone { .. } => GONE, proxy_request_refused.message_or_default()
Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED,
Self::BadRequest { .. } => BAD_REQUEST,
} }
.1 Self::BadRequest(bad_request) => bad_request.message_or_default(),
) }
} }
}
impl std::str::FromStr for Permanent { /// Get header string of `Self`
type Err = Error; pub fn as_str(&self) -> &str {
fn from_str(header: &str) -> Result<Self, Self::Err> { match self {
if let Some(postfix) = header.strip_prefix("50") { Self::Default(default) => default.as_str(),
return Ok(Self::Default { Self::NotFound(not_found) => not_found.as_str(),
message: message(postfix), Self::Gone(gone) => gone.as_str(),
}); Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(),
Self::BadRequest(bad_request) => bad_request.as_str(),
} }
if let Some(postfix) = header.strip_prefix("51") {
return Ok(Self::NotFound {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("52") {
return Ok(Self::Gone {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("53") {
return Ok(Self::ProxyRequestRefused {
message: message(postfix),
});
}
if let Some(postfix) = header.strip_prefix("59") {
return Ok(Self::BadRequest {
message: message(postfix),
});
}
Err(Error::Code)
} }
}
// Tools /// Get header bytes of `Self`
pub fn as_bytes(&self) -> &[u8] {
fn message(value: &str) -> Option<String> { match self {
let value = value.trim(); Self::Default(default) => default.as_bytes(),
if value.is_empty() { Self::NotFound(not_found) => not_found.as_bytes(),
None Self::Gone(gone) => gone.as_bytes(),
} else { Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(),
Some(value.to_string()) Self::BadRequest(bad_request) => bad_request.as_bytes(),
}
} }
} }
#[test] #[test]
fn test_from_str() { fn test() {
use std::str::FromStr; fn t(source: &str, message: Option<&str>) {
let b = source.as_bytes();
let i = Permanent::from_utf8(b).unwrap();
assert_eq!(i.message(), message);
assert_eq!(i.as_str(), source);
assert_eq!(i.as_bytes(), b);
}
// 50 // 50
let default = Permanent::from_str("50 Message\r\n").unwrap(); t("50 Message\r\n", Some("Message"));
assert_eq!(default.message(), Some("Message")); t("50\r\n", None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
let default = Permanent::from_str("50\r\n").unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
// 51 // 51
let not_found = Permanent::from_str("51 Message\r\n").unwrap(); t("51 Message\r\n", Some("Message"));
assert_eq!(not_found.message(), Some("Message")); t("51\r\n", None);
assert_eq!(not_found.to_code(), NOT_FOUND.0);
assert_eq!(not_found.to_string(), NOT_FOUND.1);
let not_found = Permanent::from_str("51\r\n").unwrap();
assert_eq!(not_found.message(), None);
assert_eq!(not_found.to_code(), NOT_FOUND.0);
assert_eq!(not_found.to_string(), NOT_FOUND.1);
// 52 // 52
let gone = Permanent::from_str("52 Message\r\n").unwrap(); t("52 Message\r\n", Some("Message"));
assert_eq!(gone.message(), Some("Message")); t("52\r\n", None);
assert_eq!(gone.to_code(), GONE.0);
assert_eq!(gone.to_string(), GONE.1);
let gone = Permanent::from_str("52\r\n").unwrap();
assert_eq!(gone.message(), None);
assert_eq!(gone.to_code(), GONE.0);
assert_eq!(gone.to_string(), GONE.1);
// 53 // 53
let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap(); t("53 Message\r\n", Some("Message"));
assert_eq!(proxy_request_refused.message(), Some("Message")); t("53\r\n", None);
assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0);
assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1);
let proxy_request_refused = Permanent::from_str("53\r\n").unwrap();
assert_eq!(proxy_request_refused.message(), None);
assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0);
assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1);
// 59 // 59
let bad_request = Permanent::from_str("59 Message\r\n").unwrap(); t("59 Message\r\n", Some("Message"));
assert_eq!(bad_request.message(), Some("Message")); t("59\r\n", None);
assert_eq!(bad_request.to_code(), BAD_REQUEST.0);
assert_eq!(bad_request.to_string(), BAD_REQUEST.1);
let bad_request = Permanent::from_str("59\r\n").unwrap();
assert_eq!(bad_request.message(), None);
assert_eq!(bad_request.to_code(), BAD_REQUEST.0);
assert_eq!(bad_request.to_string(), BAD_REQUEST.1);
} }

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
pub const CODE: &[u8] = b"59";
/// 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 = "Bad request";
/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct BadRequest(String);
impl BadRequest {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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 br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap();
assert_eq!(br.message(), Some("Message"));
assert_eq!(br.message_or_default(), "Message");
assert_eq!(br.as_str(), "59 Message\r\n");
assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes());
let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap();
assert_eq!(br.message(), None);
assert_eq!(br.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(br.as_str(), "59\r\n");
assert_eq!(br.as_bytes(), "59\r\n".as_bytes());
// err
assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -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}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code
pub const CODE: &[u8] = b"50";
/// 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 = "Permanent error";
/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) 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<Self, Error> {
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("50 Message\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), Some("Message"));
assert_eq!(d.message_or_default(), "Message");
assert_eq!(d.as_str(), "50 Message\r\n");
assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes());
let d = Default::from_utf8("50\r\n".as_bytes()).unwrap();
assert_eq!(d.message(), None);
assert_eq!(d.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(d.as_str(), "50\r\n");
assert_eq!(d.as_bytes(), "50\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());
}

View file

@ -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}")
}
}
}
}

View file

@ -1,22 +1,47 @@
use std::{ use std::fmt::{Display, Formatter, Result};
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Code, BadRequest(super::bad_request::Error),
Utf8Error(Utf8Error), Default(super::default::Error),
FirstByte(u8),
Gone(super::gone::Error),
NotFound(super::not_found::Error),
ProxyRequestRefused(super::proxy_request_refused::Error),
SecondByte(u8),
UndefinedFirstByte,
UndefinedSecondByte,
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
match self { match self {
Self::Code => { Self::BadRequest(e) => {
write!(f, "Status code error") write!(f, "BadRequest parse error: {e}")
} }
Self::Utf8Error(e) => { Self::Default(e) => {
write!(f, "UTF-8 error: {e}") write!(f, "Default parse error: {e}")
}
Self::FirstByte(b) => {
write!(f, "Unexpected first byte: {b}")
}
Self::Gone(e) => {
write!(f, "Gone parse error: {e}")
}
Self::NotFound(e) => {
write!(f, "NotFound parse error: {e}")
}
Self::ProxyRequestRefused(e) => {
write!(f, "ProxyRequestRefused parse error: {e}")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
} }
} }
} }

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
pub const CODE: &[u8] = b"52";
/// 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 = "Resource gone";
/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Gone(String);
impl Gone {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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 g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap();
assert_eq!(g.message(), Some("Message"));
assert_eq!(g.message_or_default(), "Message");
assert_eq!(g.as_str(), "52 Message\r\n");
assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes());
let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap();
assert_eq!(g.message(), None);
assert_eq!(g.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(g.as_str(), "52\r\n");
assert_eq!(g.as_bytes(), "52\r\n".as_bytes());
// err
assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Gone::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -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}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
pub const CODE: &[u8] = b"51";
/// 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 = "Not Found";
/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct NotFound(String);
impl NotFound {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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 nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap();
assert_eq!(nf.message(), Some("Message"));
assert_eq!(nf.message_or_default(), "Message");
assert_eq!(nf.as_str(), "51 Message\r\n");
assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes());
let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap();
assert_eq!(nf.message(), None);
assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(nf.as_str(), "51\r\n");
assert_eq!(nf.as_bytes(), "51\r\n".as_bytes());
// err
assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(NotFound::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -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}")
}
}
}
}

View file

@ -0,0 +1,78 @@
pub mod error;
pub use error::Error;
/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
pub const CODE: &[u8] = b"53";
/// 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 request refused";
/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct ProxyRequestRefused(String);
impl ProxyRequestRefused {
// Constructors
/// Parse `Self` from buffer contains header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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 prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap();
assert_eq!(prf.message(), Some("Message"));
assert_eq!(prf.message_or_default(), "Message");
assert_eq!(prf.as_str(), "53 Message\r\n");
assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes());
let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap();
assert_eq!(prf.message(), None);
assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE);
assert_eq!(prf.as_str(), "53\r\n");
assert_eq!(prf.as_bytes(), "53\r\n".as_bytes());
// err
assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err());
assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err());
}

View file

@ -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}")
}
}
}
}