rustify/endpoint.rs
1//! Contains the [Endpoint] trait and supporting traits/functions.
2
3use std::marker::PhantomData;
4
5#[cfg(feature = "blocking")]
6use crate::blocking::client::Client as BlockingClient;
7use crate::{
8 client::Client,
9 enums::{RequestMethod, RequestType, ResponseType},
10 errors::ClientError,
11};
12use async_trait::async_trait;
13use http::{Request, Response};
14use serde::de::DeserializeOwned;
15
16/// Represents a generic wrapper that can be applied to [Endpoint] results.
17///
18/// Some APIs use a generic wrapper when returning responses that contains
19/// information about the response and the actual response data in a subfield.
20/// This trait allows implementing a generic wrapper which can be used with
21/// [EndpointResult::wrap] to automatically wrap the [Endpoint::Response] in the
22/// wrapper. The only requirement is that the [Wrapper::Value] must enclose
23/// the [Endpoint::Response].
24pub trait Wrapper: DeserializeOwned + Send + Sync {
25 type Value;
26}
27
28/// Represents an [Endpoint] that has had [MiddleWare] applied to it.
29///
30/// This type wraps [Endpoint] by implementng it. The primary difference is
31/// when `exec` is called the request and response will potentially be mutated
32/// before processing. Only one [MiddleWare] can be applied to a single
33/// [Endpoint].
34pub struct MutatedEndpoint<'a, E: Endpoint, M: MiddleWare> {
35 endpoint: E,
36 middleware: &'a M,
37}
38
39impl<'a, E: Endpoint, M: MiddleWare> MutatedEndpoint<'a, E, M> {
40 /// Returns a new [MutatedEndpoint].
41 pub fn new(endpoint: E, middleware: &'a M) -> Self {
42 MutatedEndpoint {
43 endpoint,
44 middleware,
45 }
46 }
47}
48
49#[async_trait]
50impl<E: Endpoint, M: MiddleWare> Endpoint for MutatedEndpoint<'_, E, M> {
51 type Response = E::Response;
52 const REQUEST_BODY_TYPE: RequestType = E::REQUEST_BODY_TYPE;
53 const RESPONSE_BODY_TYPE: ResponseType = E::RESPONSE_BODY_TYPE;
54
55 fn path(&self) -> String {
56 self.endpoint.path()
57 }
58
59 fn method(&self) -> RequestMethod {
60 self.endpoint.method()
61 }
62
63 fn query(&self) -> Result<Option<String>, ClientError> {
64 self.endpoint.query()
65 }
66
67 fn body(&self) -> Result<Option<Vec<u8>>, ClientError> {
68 self.endpoint.body()
69 }
70
71 #[instrument(skip(self), err)]
72 fn url(&self, base: &str) -> Result<http::Uri, ClientError> {
73 self.endpoint.url(base)
74 }
75
76 #[instrument(skip(self), err)]
77 fn request(&self, base: &str) -> Result<Request<Vec<u8>>, ClientError> {
78 let mut req = crate::http::build_request(
79 base,
80 &self.path(),
81 self.method(),
82 self.query()?,
83 self.body()?,
84 )?;
85
86 self.middleware.request(self, &mut req)?;
87 Ok(req)
88 }
89
90 // TODO: remove the allow when the upstream clippy issue is fixed:
91 // <https://github.com/rust-lang/rust-clippy/issues/12281>
92 #[allow(clippy::blocks_in_conditions)]
93 #[instrument(skip(self, client), err)]
94 async fn exec(
95 &self,
96 client: &impl Client,
97 ) -> Result<EndpointResult<Self::Response>, ClientError> {
98 debug!("Executing endpoint");
99
100 let req = self.request(client.base())?;
101 let resp = exec_mut(client, self, req, self.middleware).await?;
102 Ok(EndpointResult::new(resp, Self::RESPONSE_BODY_TYPE))
103 }
104
105 #[cfg(feature = "blocking")]
106 fn exec_block(
107 &self,
108 client: &impl BlockingClient,
109 ) -> Result<EndpointResult<Self::Response>, ClientError> {
110 debug!("Executing endpoint");
111
112 let req = self.request(client.base())?;
113 let resp = exec_block_mut(client, self, req, self.middleware)?;
114 Ok(EndpointResult::new(resp, Self::RESPONSE_BODY_TYPE))
115 }
116}
117
118/// Represents a remote HTTP endpoint which can be executed using a
119/// [crate::client::Client].
120///
121/// This trait can be implemented directly, however, users should prefer using
122/// the provided `rustify_derive` macro for generating implementations. An
123/// Endpoint consists of:
124/// * An `action` which is combined with the base URL of a Client to form a
125/// fully qualified URL.
126/// * A `method` of type [RequestType] which determines the HTTP method used
127/// when a Client executes this endpoint.
128/// * A `ResponseType` type which determines the type of response this
129/// Endpoint will return when executed.
130///
131/// The fields of the struct act as a representation of data that will be
132/// serialized and sent to the remote server. Where and how each field appears
133/// in the final request is determined by how they are tagged with attributes.
134/// For example, fields with `#[endpoint(query)]` will show up as a query
135/// parameter and fields with `#[endpoint(body)]` will show up in the body in
136/// the format specified by [Endpoint::REQUEST_BODY_TYPE]. By default, if no
137/// fields are tagged with `#[endpoint(body)]` or `#[endpoint(raw)]` then any
138/// untagged fields are assumed to be tagged with `#[endpoint(body)]` (this
139/// reduces a large amount of boilerplate). Fields that should be excluded from
140/// this behavior can be tagged with `#[endpoint(skip)]`.
141///
142/// It's worth noting that fields which have the [Option] type and whose value,
143/// at runtime, is [Option::None] will not be serialized. This avoids defining
144/// data parameters which were not specified when the endpoint was created.
145///
146/// A number of useful methods are provided for obtaining information about an
147/// endpoint including its URL, HTTP method, and request data. The `request`
148/// method can be used to produce a fully valid HTTP [Request] that can be used
149/// for executing an endpoint without using a built-in [Client] provided by
150/// rustify.
151///
152/// # Example
153/// ```
154/// use rustify::clients::reqwest::Client;
155/// use rustify::endpoint::Endpoint;
156/// use rustify_derive::Endpoint;
157///
158/// #[derive(Endpoint)]
159/// #[endpoint(path = "my/endpoint")]
160/// struct MyEndpoint {}
161///
162/// // Configure a client with a base URL of http://myapi.com
163/// let client = Client::default("http://myapi.com");
164///
165/// // Construct a new instance of our Endpoint
166/// let endpoint = MyEndpoint {};
167///
168/// // Execute our Endpoint using the client
169/// // This sends a GET request to http://myapi.com/my/endpoint
170/// // It assumes an empty response
171/// # tokio_test::block_on(async {
172/// let result = endpoint.exec(&client).await;
173/// # })
174/// ```
175#[async_trait]
176pub trait Endpoint: Send + Sync + Sized {
177 /// The type that the raw response from executing this endpoint will
178 /// deserialized into. This type is passed on to the [EndpointResult] and is
179 /// used to determine the type returned when the `parse()` method is called.
180 type Response: DeserializeOwned + Send + Sync;
181
182 /// The content type of the request body
183 const REQUEST_BODY_TYPE: RequestType;
184
185 /// The content type of the response body
186 const RESPONSE_BODY_TYPE: ResponseType;
187
188 /// The relative URL path that represents the location of this Endpoint.
189 /// This is combined with the base URL from a
190 /// [Client][crate::client::Client] instance to create the fully qualified
191 /// URL.
192 fn path(&self) -> String;
193
194 /// The HTTP method to be used when executing this Endpoint.
195 fn method(&self) -> RequestMethod;
196
197 /// Optional query parameters to add to the request.
198 fn query(&self) -> Result<Option<String>, ClientError> {
199 Ok(None)
200 }
201
202 /// Optional data to add to the body of the request.
203 fn body(&self) -> Result<Option<Vec<u8>>, ClientError> {
204 Ok(None)
205 }
206
207 /// Returns the full URL address of the endpoint using the base address.
208 #[instrument(skip(self), err)]
209 fn url(&self, base: &str) -> Result<http::Uri, ClientError> {
210 crate::http::build_url(base, &self.path(), self.query()?)
211 }
212
213 /// Returns a [Request] containing all data necessary to execute against
214 /// this endpoint.
215 #[instrument(skip(self), err)]
216 fn request(&self, base: &str) -> Result<Request<Vec<u8>>, ClientError> {
217 crate::http::build_request(
218 base,
219 &self.path(),
220 self.method(),
221 self.query()?,
222 self.body()?,
223 )
224 }
225
226 /// Executes the Endpoint using the given [Client].
227 // TODO: remove the allow when the upstream clippy issue is fixed:
228 // <https://github.com/rust-lang/rust-clippy/issues/12281>
229 #[allow(clippy::blocks_in_conditions)]
230 #[instrument(skip(self, client), err)]
231 async fn exec(
232 &self,
233 client: &impl Client,
234 ) -> Result<EndpointResult<Self::Response>, ClientError> {
235 debug!("Executing endpoint");
236
237 let req = self.request(client.base())?;
238 let resp = exec(client, req).await?;
239 Ok(EndpointResult::new(resp, Self::RESPONSE_BODY_TYPE))
240 }
241
242 fn with_middleware<M: MiddleWare>(self, middleware: &M) -> MutatedEndpoint<Self, M> {
243 MutatedEndpoint::new(self, middleware)
244 }
245
246 /// Executes the Endpoint using the given [Client].
247 #[cfg(feature = "blocking")]
248 #[instrument(skip(self, client), err)]
249 fn exec_block(
250 &self,
251 client: &impl BlockingClient,
252 ) -> Result<EndpointResult<Self::Response>, ClientError> {
253 debug!("Executing endpoint");
254
255 let req = self.request(client.base())?;
256 let resp = exec_block(client, req)?;
257 Ok(EndpointResult::new(resp, Self::RESPONSE_BODY_TYPE))
258 }
259}
260
261/// A response from executing an [Endpoint].
262///
263/// All [Endpoint] executions will result in an [EndpointResult] which wraps
264/// the actual HTTP [Response] and the final result type. The response can be
265/// parsed into the final result type by calling `parse()` or optionally
266/// wrapped by a [Wrapper] by calling `wrap()`.
267pub struct EndpointResult<T: DeserializeOwned + Send + Sync> {
268 pub response: Response<Vec<u8>>,
269 pub ty: ResponseType,
270 inner: PhantomData<T>,
271}
272
273impl<T: DeserializeOwned + Send + Sync> EndpointResult<T> {
274 /// Returns a new [EndpointResult].
275 pub fn new(response: Response<Vec<u8>>, ty: ResponseType) -> Self {
276 EndpointResult {
277 response,
278 ty,
279 inner: PhantomData,
280 }
281 }
282
283 /// Parses the response into the final result type.
284 #[instrument(skip(self), err)]
285 pub fn parse(&self) -> Result<T, ClientError> {
286 match self.ty {
287 ResponseType::JSON => serde_json::from_slice(self.response.body()).map_err(|e| {
288 ClientError::ResponseParseError {
289 source: e.into(),
290 content: String::from_utf8(self.response.body().to_vec()).ok(),
291 }
292 }),
293 }
294 }
295
296 /// Returns the raw response body from the HTTP [Response].
297 pub fn raw(&self) -> Vec<u8> {
298 self.response.body().clone()
299 }
300
301 /// Parses the response into the final result type and then wraps it in the
302 /// given [Wrapper].
303 #[instrument(skip(self), err)]
304 pub fn wrap<W>(&self) -> Result<W, ClientError>
305 where
306 W: Wrapper<Value = T>,
307 {
308 match self.ty {
309 ResponseType::JSON => serde_json::from_slice(self.response.body()).map_err(|e| {
310 ClientError::ResponseParseError {
311 source: e.into(),
312 content: String::from_utf8(self.response.body().to_vec()).ok(),
313 }
314 }),
315 }
316 }
317}
318
319/// Modifies an [Endpoint] request and/or response before final processing.
320///
321/// Types implementing this trait that do not desire to implement both methods
322/// should instead return `OK(())` to bypass any processing of the [Request] or
323/// [Response].
324pub trait MiddleWare: Sync + Send {
325 /// Modifies a [Request] from an [Endpoint] before it's executed.
326 fn request<E: Endpoint>(
327 &self,
328 endpoint: &E,
329 req: &mut Request<Vec<u8>>,
330 ) -> Result<(), ClientError>;
331
332 /// Modifies a [Response] from an [Endpoint] before being returned as an
333 /// [EndpointResult].
334 fn response<E: Endpoint>(
335 &self,
336 endpoint: &E,
337 resp: &mut Response<Vec<u8>>,
338 ) -> Result<(), ClientError>;
339}
340
341async fn exec(
342 client: &impl Client,
343 req: Request<Vec<u8>>,
344) -> Result<Response<Vec<u8>>, ClientError> {
345 client.execute(req).await
346}
347
348async fn exec_mut(
349 client: &impl Client,
350 endpoint: &impl Endpoint,
351 req: Request<Vec<u8>>,
352 middle: &impl MiddleWare,
353) -> Result<Response<Vec<u8>>, ClientError> {
354 let mut resp = client.execute(req).await?;
355 middle.response(endpoint, &mut resp)?;
356 Ok(resp)
357}
358
359#[cfg(feature = "blocking")]
360fn exec_block(
361 client: &impl BlockingClient,
362 req: Request<Vec<u8>>,
363) -> Result<Response<Vec<u8>>, ClientError> {
364 client.execute(req)
365}
366
367#[cfg(feature = "blocking")]
368fn exec_block_mut(
369 client: &impl BlockingClient,
370 endpoint: &impl Endpoint,
371 req: Request<Vec<u8>>,
372 middle: &impl MiddleWare,
373) -> Result<Response<Vec<u8>>, ClientError> {
374 let mut resp = client.execute(req)?;
375 middle.response(endpoint, &mut resp)?;
376 Ok(resp)
377}