terdoc_client/
lib.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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    /// The provided string is not a valid URL
45    Malformed { source: url::ParseError },
46
47    /// The provided URL is invalid
48    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    /// Initialize the terdoc client.
61    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}