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}