From 8ee088270f5ba128a7dd766e57490f1778148501 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:36:48 +0200 Subject: [PATCH] implement high-level getters, add comments, improve tests --- src/client/connection/response/success.rs | 47 +++++++++++++++++-- .../connection/response/success/default.rs | 23 ++++++--- .../response/success/default/header.rs | 41 ++++++++++------ 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index ecee769..f9493d6 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -24,14 +24,53 @@ impl Success { Err(e) => Err(Error::Default(e)), } } + + // Getters + + /// Get header bytes for `Self` type + pub fn as_header_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.header.as_bytes(), + } + } + + /// Get header string for `Self` type + pub fn as_header_str(&self) -> &str { + match self { + Self::Default(default) => default.header.as_str(), + } + } + + /// Get parsed MIME for `Self` type + /// + /// * high-level method, useful to skip extra match case constructions; + /// * at this moment, Gemini protocol has only one status code in this scope,\ + /// this method would be deprecated in future, use on your own risk! + pub fn mime(&self) -> Result { + match self { + Self::Default(default) => default + .header + .mime() + .map_err(|e| Error::Default(default::Error::Header(e))), + } + } } #[test] fn test() { - match Success::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { - Success::Default(default) => { - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let r = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = r.as_bytes(); + let s = Success::from_utf8(b).unwrap(); + + match s { + Success::Default(ref d) => { + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()) } } + assert_eq!(s.as_header_bytes(), b); + assert_eq!(s.as_header_str(), r); + assert_eq!(s.mime().unwrap(), "text/gemini"); + + assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()) } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index a905318..488c3e6 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -11,13 +11,18 @@ pub const CODE: &[u8] = b"20"; /// * this response type MAY contain body data /// * the header has closed members to require valid construction pub struct Default { + /// Formatted header holder with additional API pub header: Header, - pub content: Option>, + /// Default success response MAY include body data + /// * if the `Request` constructed with `Mode::HeaderOnly` flag,\ + /// this value wants to be processed manually, using external application logic (specific for content-type) + pub content: Vec, } 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); @@ -25,9 +30,9 @@ impl Default { let header = Header::from_utf8(buffer).map_err(Error::Header)?; Ok(Self { content: buffer - .get(header.len() + 1..) + .get(header.as_bytes().len()..) .filter(|s| !s.is_empty()) - .map(|v| v.to_vec()), + .map_or(Vec::new(), |v| v.to_vec()), header, }) } @@ -35,8 +40,12 @@ impl Default { #[test] fn test() { - let default = - Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()); + + let d = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert_eq!(d.content.len(), 4); } diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index 929cb96..dab58b7 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -1,19 +1,22 @@ pub mod error; pub use error::Error; -pub struct Header(Vec); +pub struct Header(String); impl Header { // Constructors + /// Parse `Self` from buffer contains header bytes pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(super::CODE) { return Err(Error::Code); } Ok(Self( - crate::client::connection::response::header_bytes(buffer) - .map_err(Error::Header)? - .to_vec(), + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), )) } @@ -23,7 +26,7 @@ impl Header { pub fn mime(&self) -> Result { glib::Regex::split_simple( r"^\d{2}\s([^\/]+\/[^\s;]+)", - std::str::from_utf8(&self.0).map_err(Error::Utf8Error)?, + &self.0, glib::RegexCompileFlags::DEFAULT, glib::RegexMatchFlags::DEFAULT, ) @@ -33,15 +36,25 @@ impl Header { .map_or(Err(Error::Mime), |s| Ok(s.to_lowercase())) } - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { - &self.0 + self.0.as_bytes() + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + self.0.as_str() } } + +#[test] +fn test() { + let s = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = s.as_bytes(); + let h = Header::from_utf8(b).unwrap(); + assert_eq!(h.mime().unwrap(), "text/gemini"); + assert_eq!(h.as_bytes(), b); + assert_eq!(h.as_str(), s); + + assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()); +}