Skip to main content

bes_canopy_api/
client.rs

1//! The typed client the generated per-endpoint methods hang off.
2
3use std::io::Write as _;
4
5use bytes::Bytes;
6use flate2::{Compression, write::GzEncoder};
7use serde::{Serialize, de::DeserializeOwned};
8
9use crate::{CanopyHttpError, Error, Result, transport::CanopyTransport};
10
11/// Bodies at or above this size are gzipped. Below it the compression costs
12/// more than the transfer saves.
13const COMPRESS_FROM: usize = 1024;
14
15/// Typed client for canopy's public API.
16///
17/// Carries one method per endpoint, generated from canopy's OpenAPI document and
18/// taking and returning the wire types declared there. Those methods handle the
19/// parts that don't vary by endpoint (serialising and gzipping the request body,
20/// mapping a non-2xx to [`CanopyHttpError`], parsing the response) and hand the
21/// actual HTTP to a [`CanopyTransport`].
22///
23/// The transport is the consumer's: this crate depends on no HTTP client, and
24/// every generated method works over whichever transport is supplied.
25pub struct CanopyClient<T> {
26	transport: T,
27}
28
29impl<T> std::fmt::Debug for CanopyClient<T> {
30	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31		f.debug_struct("CanopyClient").finish_non_exhaustive()
32	}
33}
34
35impl<T: CanopyTransport> CanopyClient<T> {
36	/// Build a client over `transport`.
37	pub fn new(transport: T) -> Self {
38		Self { transport }
39	}
40
41	/// The transport underneath, for a consumer that needs to inspect or
42	/// refresh its own.
43	pub fn transport(&self) -> &T {
44		&self.transport
45	}
46
47	/// Send a request and parse a JSON response body into `R`.
48	///
49	/// Used by the generated methods for operations that answer with a body.
50	pub async fn call_json<B: Serialize + ?Sized, R: DeserializeOwned>(
51		&self,
52		method: http::Method,
53		path: &str,
54		body: Option<&B>,
55	) -> Result<R> {
56		let response = self.call(method, path, body).await?;
57		serde_json::from_slice(response.body()).map_err(|source| Error::Decode {
58			path: path.to_owned(),
59			source,
60		})
61	}
62
63	/// Send a request that answers with no body.
64	///
65	/// Used by the generated methods for operations that declare no response
66	/// body. The body canopy sends, if any, is discarded.
67	pub async fn call_empty<B: Serialize + ?Sized>(
68		&self,
69		method: http::Method,
70		path: &str,
71		body: Option<&B>,
72	) -> Result<()> {
73		self.call(method, path, body).await.map(|_| ())
74	}
75
76	/// Send a request, returning the response only if the status is a success.
77	async fn call<B: Serialize + ?Sized>(
78		&self,
79		method: http::Method,
80		path: &str,
81		body: Option<&B>,
82	) -> Result<http::Response<Bytes>> {
83		let mut request = http::Request::builder().method(method).uri(path);
84
85		let payload = match body {
86			None => Bytes::new(),
87			Some(body) => {
88				let json = serde_json::to_vec(body).map_err(|source| Error::Encode {
89					path: path.to_owned(),
90					source,
91				})?;
92				request = request.header(http::header::CONTENT_TYPE, "application/json");
93				if json.len() >= COMPRESS_FROM {
94					request = request.header(http::header::CONTENT_ENCODING, "gzip");
95					Bytes::from(gzip(&json).map_err(|source| Error::Compress {
96						path: path.to_owned(),
97						source,
98					})?)
99				} else {
100					Bytes::from(json)
101				}
102			}
103		};
104
105		let request = request.body(payload).map_err(|source| Error::Request {
106			path: path.to_owned(),
107			source,
108		})?;
109
110		let response = self.transport.call(request).await?;
111		if response.status().is_success() {
112			Ok(response)
113		} else {
114			Err(CanopyHttpError {
115				status: response.status(),
116				path: path.to_owned(),
117				body: response.into_body(),
118			}
119			.into())
120		}
121	}
122}
123
124fn gzip(bytes: &[u8]) -> std::io::Result<Vec<u8>> {
125	let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
126	encoder.write_all(bytes)?;
127	encoder.finish()
128}