1use bytes::Bytes;
5use futures::TryStreamExt;
6use reqwest::{header::USER_AGENT, RequestBuilder, Response, StatusCode};
7use snafu::{ensure, ResultExt, Snafu};
8use url::Url;
9
10const PATH: &str = "/render";
11
12const VERSION: &str = env!("CARGO_PKG_VERSION");
13const NAME: &str = env!("CARGO_PKG_NAME");
14
15pub use terdoc_types::{
16 InputFormat, OutputFormat, PdfEngine, TaskData, Template, TerdocClient as TerdocClientTrait,
17 TerdocStreamingClient as TerdocStreamingClientTrait,
18};
19
20fn default_user_agent() -> String {
21 format!("{NAME}/{VERSION}")
22}
23
24#[derive(Debug, Snafu)]
25pub enum Error {
26 #[snafu(display("Network error: {message}"))]
27 Network {
28 message: String,
29 source: reqwest::Error,
30 },
31
32 #[snafu(display("Invalid render task"))]
33 InvalidRequest { message: String },
34
35 #[snafu(display("Server error"))]
36 Server,
37
38 #[snafu(display("Unable to process response: {status_code}"))]
39 InvalidResponse { status_code: StatusCode },
40}
41
42#[derive(Debug, Snafu)]
43pub enum UrlError {
44 Malformed { source: url::ParseError },
46
47 Invalid { message: String, url: String },
49}
50
51#[derive(Debug, Clone)]
52pub struct TerdocClient {
53 client: reqwest::Client,
54 server_url: Url,
55 user_agent: String,
56 middleware: Option<fn(RequestBuilder) -> RequestBuilder>,
57}
58
59impl TerdocClient {
60 pub fn new(addr: &str) -> Result<Self, UrlError> {
62 let client = reqwest::Client::new();
63 Self::with_reqwest_client(addr, client)
64 }
65
66 fn with_reqwest_client(addr: &str, client: reqwest::Client) -> Result<Self, UrlError> {
67 let server_url = Url::parse(addr).context(MalformedSnafu)?;
68
69 ensure!(
70 server_url.fragment().is_none(),
71 InvalidSnafu {
72 message: "The URL must not have a fragment",
73 url: addr
74 }
75 );
76
77 ensure!(
78 server_url.query().is_none(),
79 InvalidSnafu {
80 message: "The URL must not have query parameter",
81 url: addr
82 }
83 );
84
85 ensure!(
86 server_url.path() == "/",
87 InvalidSnafu {
88 message: "The path of the URL must be empty",
89 url: addr
90 }
91 );
92
93 Ok(Self {
94 server_url,
95 client,
96 user_agent: default_user_agent(),
97 middleware: None,
98 })
99 }
100
101 fn render_request_url(&self) -> Url {
102 let mut server_url = self.server_url.clone();
103
104 server_url.set_path(PATH);
105 server_url
106 }
107
108 async fn checked_render_request(&self, task: &TaskData) -> Result<Response, Error> {
109 let server_url = self.render_request_url();
110
111 let mut req_builder = self
112 .client
113 .post(server_url)
114 .header(USER_AGENT, &self.user_agent)
115 .json(&task);
116 if let Some(middleware) = &self.middleware {
117 req_builder = middleware(req_builder);
118 }
119 let res = req_builder.send().await.context(NetworkSnafu {
120 message: "Failed to connect",
121 })?;
122 let status = res.status();
123 if status.is_success() {
124 Ok(res)
125 } else {
126 let error_response = res
127 .text()
128 .await
129 .unwrap_or_else(|_| "<unparsable response body>".to_string());
130 log::debug!("render request failed: {} - {}", status, error_response);
131
132 if status.is_client_error() {
133 InvalidRequestSnafu {
134 message: error_response,
135 }
136 .fail()
137 } else if status.is_server_error() {
138 ServerSnafu.fail()
139 } else {
140 InvalidResponseSnafu {
141 status_code: status,
142 }
143 .fail()
144 }
145 }
146 }
147
148 pub fn set_user_agent(&mut self, user_agent: String) {
149 self.user_agent = user_agent;
150 }
151
152 pub fn user_agent(&self) -> &str {
153 &self.user_agent
154 }
155
156 pub fn set_middleware(&mut self, middleware: fn(RequestBuilder) -> RequestBuilder) {
157 self.middleware = Some(middleware);
158 }
159
160 pub fn remove_middleware(&mut self) {
161 self.middleware = None;
162 }
163}
164
165impl terdoc_types::TerdocClient for TerdocClient {
166 type Error = Error;
167
168 async fn request_render(&mut self, task: &TaskData) -> Result<Vec<u8>, Self::Error> {
169 let res = self.checked_render_request(task).await?;
170
171 let bytes = res.bytes().await.context(NetworkSnafu {
172 message: "Failed to receive response body",
173 })?;
174 Ok(bytes.to_vec())
175 }
176}
177
178impl terdoc_types::TerdocStreamingClient for TerdocClient {
179 type Error = Error;
180
181 async fn request_render(
182 &mut self,
183 task: TaskData,
184 ) -> Result<impl futures::Stream<Item = Result<Bytes, Self::Error>>, Self::Error> {
185 let res = self.checked_render_request(&task).await?;
186
187 let stream = res.bytes_stream().map_err(|e| Error::Network {
188 message: "Failed to receive response body".to_string(),
189 source: e,
190 });
191 Ok(stream)
192 }
193}