Compare commits

..

4 commits

4 changed files with 63 additions and 12 deletions

View file

@ -1,3 +1,5 @@
//! Builder for Client
use std::sync::Arc;
use crate::{
@ -12,6 +14,7 @@ use tokio_rustls::rustls::{
SupportedProtocolVersion,
};
/// Builder for creating configured [`Client`] instance
pub struct ClientBuilder {
root_certs: rustls::RootCertStore,
ss_verifier: Option<Box<dyn SelfsignedCertVerifier>>,

View file

@ -99,7 +99,7 @@ impl Client {
/// Check the inner error if you need to determine what exactly happened
/// - [`LibError::StatusOutOfRange`] means that a server returned an incorrect status code
/// (less than 10 or greater than 69).
/// - [`LibError::MessageNotUtf8`] is returned when metadata (the text after the status code)
/// - [`LibError::DataNotUtf8`] is returned when metadata (the text after a status code)
/// is not in UTF-8 and cannot be converted to string without errors.
pub async fn request_with_host(
&self,

View file

@ -1,3 +1,5 @@
//! Client-side response structure
use crate::{status::Status, LibError, ReplyType};
use bytes::Bytes;
@ -5,6 +7,8 @@ use tokio::io::AsyncReadExt;
type BodyStream = tokio::io::BufReader<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
/// Client-side response structure wrapping a [`Status`],
/// a metadata string and a TLS stream
#[derive(Debug)]
pub struct Response {
status: Status,
@ -21,11 +25,25 @@ impl Response {
}
}
/// Check if status code is 2x (success).
#[inline]
pub fn is_ok(&self) -> bool {
self.status.reply_type() == ReplyType::Success
}
/// Return `Ok(self)` if status code is 2x, otherwise `Err(self)`.
///
/// # Examples
/// ```
/// match client.request("gemini://dc09.ru").await?.ensure_ok() {
/// Ok(resp) => {
/// println!("{}", resp.text().await?);
/// }
/// Err(resp) => {
/// println!("{}", resp.message());
/// }
/// }
/// ```
pub fn ensure_ok(self) -> Result<Self, Self> {
if self.status.reply_type() == ReplyType::Success {
Ok(self)
@ -34,28 +52,50 @@ impl Response {
}
}
/// Get the response status code as [`Status`].
pub fn status(&self) -> Status {
self.status
}
/// Get the response metadata -- the text after a status code + space.
/// - In 1x responses (input), it contains an input prompt message.
/// - In 2x responses (success), it contains a MIME type of the body.
/// - In 3x responses (redirect), it contains a URL.
/// - In 4x, 5x (fail), 6x (auth), it contains an error message.
pub fn message(&self) -> &str {
&self.message
}
/// Get the response body MIME type by parsing the metadata field.
/// If you call this method on a non-2x response, you'll get a parse error
/// ([`LibError::InvalidMime`]).
/// It's strongly recommended to check the status code first
/// (most handy is [`Response::is_ok()`]).
pub fn mime(&self) -> Result<mime::Mime, LibError> {
self.message.parse().map_err(LibError::InvalidMime)
}
/// Borrow the wrapped TLS stream as `&mut`.
///
/// # Reminder
/// You can read data from one stream only once,
/// so calling `.bytes()` after `.stream().read_to_end(…)`
/// (or `.bytes()` twice, or `.text()` after `.bytes()` and vice versa)
/// on the same response will result in empty output.
pub fn stream(&mut self) -> &mut BodyStream {
&mut self.stream
}
/// Read the whole response body and return as [`Bytes`].
/// See also [the note](#reminder) to `stream()`.
pub async fn bytes(&mut self) -> Result<Bytes, LibError> {
let mut buf = Vec::new();
self.stream.read_to_end(&mut buf).await?;
Ok(Bytes::from(buf))
}
/// Read the whole response body as a UTF-8 [`String`].
/// See also [the note](#reminder) to `stream()`.
pub async fn text(&mut self) -> Result<String, LibError> {
let mut buf = String::new();
self.stream.read_to_string(&mut buf).await?;

View file

@ -1,10 +1,19 @@
//! Library error structures and enums
/// Main error structure, also a wrapper for everything else
#[derive(Debug)]
pub enum LibError {
/// General I/O error
// TODO: separate somehow
IoError(std::io::Error),
/// URL parse or check error
InvalidUrlError(InvalidUrl),
/// Response status code is out of [10; 69] range
StatusOutOfRange(u8),
MessageNotUtf8(std::string::FromUtf8Error),
BodyNotUtf8(std::str::Utf8Error),
/// Response metadata or content cannot be parsed
/// as a UTF-8 string without errors
DataNotUtf8(std::string::FromUtf8Error),
/// Provided string is not a valid MIME type
InvalidMime(mime::FromStrError),
}
@ -39,14 +48,7 @@ impl LibError {
impl From<std::string::FromUtf8Error> for LibError {
#[inline]
fn from(err: std::string::FromUtf8Error) -> Self {
Self::MessageNotUtf8(err)
}
}
impl From<std::str::Utf8Error> for LibError {
#[inline]
fn from(err: std::str::Utf8Error) -> Self {
Self::BodyNotUtf8(err)
Self::DataNotUtf8(err)
}
}
@ -57,11 +59,17 @@ impl From<mime::FromStrError> for LibError {
}
}
/// URL parse or check error
#[derive(Debug)]
pub enum InvalidUrl {
/// Provided string cannot be parsed as a valid URL with [`url::Url`]
ParseError(url::ParseError),
/// URL scheme is not `gemini://`
SchemeNotGemini,
/// URL contains userinfo -- `user:pswd@` --
/// which is forbidden by Gemini spec
UserinfoPresent,
NoHostFound,
/// Could not extract host from the URL or convert host and port into
/// [`std::net::SocketAddr`] or [`crate::certs::ServerName`]
ConvertError,
}