use bytes::Bytes;
use futures::TryStreamExt;
use reqwest::{header::USER_AGENT, RequestBuilder, Response, StatusCode};
use snafu::{ensure, ResultExt, Snafu};
use url::Url;
const PATH: &str = "/render";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const NAME: &str = env!("CARGO_PKG_NAME");
pub use terdoc_types::{
InputFormat, OutputFormat, PdfEngine, TaskData, Template, TerdocClient as TerdocClientTrait,
TerdocStreamingClient as TerdocStreamingClientTrait,
};
fn default_user_agent() -> String {
format!("{NAME}/{VERSION}")
}
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Network error: {message}"))]
Network {
message: String,
source: reqwest::Error,
},
#[snafu(display("Invalid render task"))]
InvalidRequest { message: String },
#[snafu(display("Server error"))]
Server,
#[snafu(display("Unable to process response: {status_code}"))]
InvalidResponse { status_code: StatusCode },
}
#[derive(Debug, Snafu)]
pub enum UrlError {
Malformed { source: url::ParseError },
Invalid { message: String, url: String },
}
#[derive(Debug, Clone)]
pub struct TerdocClient {
client: reqwest::Client,
server_url: Url,
user_agent: String,
middleware: Option<fn(RequestBuilder) -> RequestBuilder>,
}
impl TerdocClient {
pub fn new(addr: &str) -> Result<Self, UrlError> {
let client = reqwest::Client::new();
Self::with_reqwest_client(addr, client)
}
fn with_reqwest_client(addr: &str, client: reqwest::Client) -> Result<Self, UrlError> {
let server_url = Url::parse(addr).context(MalformedSnafu)?;
ensure!(
server_url.fragment().is_none(),
InvalidSnafu {
message: "The URL must not have a fragment",
url: addr
}
);
ensure!(
server_url.query().is_none(),
InvalidSnafu {
message: "The URL must not have query parameter",
url: addr
}
);
ensure!(
server_url.path() == "/",
InvalidSnafu {
message: "The path of the URL must be empty",
url: addr
}
);
Ok(Self {
server_url,
client,
user_agent: default_user_agent(),
middleware: None,
})
}
fn render_request_url(&self) -> Url {
let mut server_url = self.server_url.clone();
server_url.set_path(PATH);
server_url
}
async fn checked_render_request(&self, task: &TaskData) -> Result<Response, Error> {
let server_url = self.render_request_url();
let mut req_builder = self
.client
.post(server_url)
.header(USER_AGENT, &self.user_agent)
.json(&task);
if let Some(middleware) = &self.middleware {
req_builder = middleware(req_builder);
}
let res = req_builder.send().await.context(NetworkSnafu {
message: "Failed to connect",
})?;
let status = res.status();
if status.is_success() {
Ok(res)
} else {
let error_response = res
.text()
.await
.unwrap_or_else(|_| "<unparsable response body>".to_string());
log::debug!("render request failed: {} - {}", status, error_response);
if status.is_client_error() {
InvalidRequestSnafu {
message: error_response,
}
.fail()
} else if status.is_server_error() {
ServerSnafu.fail()
} else {
InvalidResponseSnafu {
status_code: status,
}
.fail()
}
}
}
pub fn set_user_agent(&mut self, user_agent: String) {
self.user_agent = user_agent;
}
pub fn user_agent(&self) -> &str {
&self.user_agent
}
pub fn set_middleware(&mut self, middleware: fn(RequestBuilder) -> RequestBuilder) {
self.middleware = Some(middleware);
}
pub fn remove_middleware(&mut self) {
self.middleware = None;
}
}
impl terdoc_types::TerdocClient for TerdocClient {
type Error = Error;
async fn request_render(&mut self, task: &TaskData) -> Result<Vec<u8>, Self::Error> {
let res = self.checked_render_request(task).await?;
let bytes = res.bytes().await.context(NetworkSnafu {
message: "Failed to receive response body",
})?;
Ok(bytes.to_vec())
}
}
impl terdoc_types::TerdocStreamingClient for TerdocClient {
type Error = Error;
async fn request_render(
&mut self,
task: TaskData,
) -> Result<impl futures::Stream<Item = Result<Bytes, Self::Error>>, Self::Error> {
let res = self.checked_render_request(&task).await?;
let stream = res.bytes_stream().map_err(|e| Error::Network {
message: "Failed to receive response body".to_string(),
source: e,
});
Ok(stream)
}
}