reorganize input format: make constructors lazy, parse members on get

This commit is contained in:
yggverse 2025-03-24 20:46:54 +02:00
parent 161142c809
commit a32eccf5cb
6 changed files with 243 additions and 85 deletions

View file

@ -1,12 +1,17 @@
pub mod default;
pub mod error;
pub mod sensitive;
pub use default::Default;
pub use error::Error;
pub use sensitive::Sensitive;
const DEFAULT: (u8, &str) = (10, "Input");
const SENSITIVE: (u8, &str) = (11, "Sensitive input");
const CODE: u8 = b'1';
/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected)
pub enum Input {
Default { message: Option<String> },
Sensitive { message: Option<String> },
Default(Default),
Sensitive(Sensitive),
}
impl Input {
@ -14,97 +19,63 @@ impl Input {
/// Create new `Self` from buffer include header bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
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::Sensitive(
Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?,
)),
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::Sensitive { .. } => SENSITIVE,
}
.0
}
pub fn message(&self) -> Option<&str> {
match self {
Self::Default { message } => message,
Self::Sensitive { message } => message,
}
.as_deref()
Self::Default(default) => default.message(),
Self::Sensitive(sensitive) => sensitive.message(),
}
}
impl std::fmt::Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
pub fn as_str(&self) -> &str {
match self {
Self::Default { .. } => DEFAULT,
Self::Sensitive { .. } => SENSITIVE,
}
.1
)
Self::Default(default) => default.as_str(),
Self::Sensitive(sensitive) => sensitive.as_str(),
}
}
impl std::str::FromStr for Input {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
if let Some(postfix) = header.strip_prefix("10") {
return Ok(Self::Default {
message: message(postfix),
});
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Default(default) => default.as_bytes(),
Self::Sensitive(sensitive) => sensitive.as_bytes(),
}
if let Some(postfix) = header.strip_prefix("11") {
return Ok(Self::Sensitive {
message: message(postfix),
});
}
Err(Error::Protocol)
}
}
// Tools
fn message(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
#[test]
fn test_from_str() {
use std::str::FromStr;
// 10
let default = Input::from_str("10 Default\r\n").unwrap();
assert_eq!(default.message(), Some("Default"));
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
let default = Input::from_str("10\r\n").unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
// 11
let sensitive = Input::from_str("11 Sensitive\r\n").unwrap();
assert_eq!(sensitive.message(), Some("Sensitive"));
assert_eq!(sensitive.to_code(), SENSITIVE.0);
assert_eq!(sensitive.to_string(), SENSITIVE.1);
let sensitive = Input::from_str("11\r\n").unwrap();
assert_eq!(sensitive.message(), None);
assert_eq!(sensitive.to_code(), SENSITIVE.0);
assert_eq!(sensitive.to_string(), SENSITIVE.1);
fn test() {
fn t(source: &str, message: Option<&str>) {
let bytes = source.as_bytes();
let input = Input::from_utf8(bytes).unwrap();
assert_eq!(input.message(), message);
assert_eq!(input.as_str(), source);
assert_eq!(input.as_bytes(), bytes);
}
// 10
t("10 Default\r\n", Some("Default"));
t("10\r\n", None);
// 11
t("11 Sensitive\r\n", Some("Sensitive"));
t("11\r\n", None);
}

View file

@ -0,0 +1,61 @@
pub mod error;
pub use error::Error;
const CODE: &[u8] = b"10";
/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) 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())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap();
assert_eq!(default.message(), Some("Default"));
assert_eq!(default.as_str(), "10 Default\r\n");
let default = Default::from_utf8("10\r\n".as_bytes()).unwrap();
assert_eq!(default.message(), None);
assert_eq!(default.as_str(), "10\r\n");
// err
assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err());
assert!(Default::from_utf8("22 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,23 +1,40 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Default(super::default::Error),
FirstByte(u8),
Protocol,
Utf8Error(Utf8Error),
SecondByte(u8),
Sensitive(super::sensitive::Error),
UndefinedFirstByte,
UndefinedSecondByte,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
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::Protocol => {
write!(f, "Protocol error")
}
Self::SecondByte(b) => {
write!(f, "Unexpected second byte: {b}")
}
Self::Sensitive(e) => {
write!(f, "Sensitive parse error: {e}")
}
Self::UndefinedFirstByte => {
write!(f, "Undefined first byte")
}
Self::UndefinedSecondByte => {
write!(f, "Undefined second byte")
}
}
}
}

View file

@ -0,0 +1,61 @@
pub mod error;
pub use error::Error;
const CODE: &[u8] = b"11";
/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code
/// * this response type does not contain body data
/// * the header member is closed to require valid construction
pub struct Sensitive(String);
impl Sensitive {
// 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())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[test]
fn test() {
// ok
let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap();
assert_eq!(sensitive.message(), Some("Sensitive"));
assert_eq!(sensitive.as_str(), "11 Sensitive\r\n");
let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap();
assert_eq!(sensitive.message(), None);
assert_eq!(sensitive.as_str(), "11\r\n");
// err
assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err());
assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err());
assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err());
assert!(Sensitive::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}")
}
}
}
}