Skip to main content

barter_integration/protocol/http/rest/
client.rs

1use crate::{
2    error::SocketError,
3    metric::{Field, Metric, Tag},
4    protocol::http::{BuildStrategy, HttpParser, rest::RestRequest},
5};
6use bytes::Bytes;
7use chrono::Utc;
8
9/// Configurable REST client capable of executing signed [`RestRequest`]s. Use this when
10/// integrating APIs that require Http in order to interact with resources. Each API will require
11/// a specific combination of [`Signer`](super::super::private::Signer), [`Mac`](hmac::Mac),
12/// signature [`Encoder`](super::super::private::encoder::Encoder), and
13/// [`HttpParser`].
14#[derive(Debug)]
15pub struct RestClient<Strategy, Parser> {
16    /// HTTP [`reqwest::Client`] for executing signed [`reqwest::Request`]s.
17    pub http_client: reqwest::Client,
18
19    /// Base Url of the API being interacted with.
20    pub base_url: Box<str>,
21
22    /// [`RestRequest`] build strategy for the API being interacted with that implements
23    /// [`BuildStrategy`].
24    ///
25    /// An authenticated [`RestClient`] will utilise API specific
26    /// [`Signer`](super::super::private::Signer) logic, a hashable [`Mac`](hmac::Mac), and a
27    /// signature [`Encoder`](super::super::private::encoder::Encoder). Where as a non authorised
28    /// [`RestRequest`] may add any mandatory `reqwest` headers that are required.
29    pub strategy: Strategy,
30
31    /// [`HttpParser`] that deserialises [`RestRequest::Response`]s, and upon failure parses
32    /// API errors returned from the server.
33    pub parser: Parser,
34}
35
36impl<Strategy, Parser> RestClient<Strategy, Parser>
37where
38    Strategy: BuildStrategy,
39    Parser: HttpParser,
40{
41    /// Execute the provided [`RestRequest`].
42    pub async fn execute<Request>(
43        &self,
44        request: Request,
45    ) -> Result<(Request::Response, Metric), Parser::OutputError>
46    where
47        Request: RestRequest,
48    {
49        // Use provided Request to construct a signed reqwest::Request
50        let request = self.build(request)?;
51
52        // Measure request execution
53        let (status, payload, latency) = self.measured_execution::<Request>(request).await?;
54
55        // Attempt to parse API Success or Error response
56        self.parser
57            .parse::<Request::Response>(status, &payload)
58            .map(|response| (response, latency))
59    }
60
61    /// Use the provided [`RestRequest`] to construct a signed Http [`reqwest::Request`].
62    pub fn build<Request>(&self, request: Request) -> Result<reqwest::Request, SocketError>
63    where
64        Request: RestRequest,
65    {
66        // Construct url
67        let url = format!("{}{}", self.base_url, request.path());
68
69        // Construct RequestBuilder with method & url
70        let mut builder = self
71            .http_client
72            .request(Request::method(), url)
73            .timeout(Request::timeout());
74
75        // Add optional query parameters
76        if let Some(query_params) = request.query_params() {
77            builder = builder.query(query_params);
78        }
79
80        // Add optional Body
81        if let Some(body) = request.body() {
82            builder = builder.json(body);
83        }
84
85        // Use RequestBuilder (public or private strategy) to build reqwest::Request
86        self.strategy.build(request, builder)
87    }
88
89    /// Execute the built [`reqwest::Request`] using the [`reqwest::Client`].
90    ///
91    /// Measures and returns the Http request round trip duration.
92    pub async fn measured_execution<Request>(
93        &self,
94        request: reqwest::Request,
95    ) -> Result<(reqwest::StatusCode, Bytes, Metric), SocketError>
96    where
97        Request: RestRequest,
98    {
99        // Construct Http request duration Metric
100        let mut latency = Metric {
101            name: "http_request_duration",
102            time: Utc::now().timestamp_millis() as u64,
103            tags: vec![
104                Tag::new("http_method", Request::method().as_str()),
105                Tag::new("base_url", self.base_url.as_ref()),
106                Tag::new("path", request.url().path()),
107            ],
108            fields: Vec::with_capacity(1),
109        };
110
111        // Measure the HTTP request round trip duration
112        let start = std::time::Instant::now();
113        let response = self.http_client.execute(request).await?;
114        let duration = start.elapsed().as_millis() as u64;
115
116        // Update Metric with response status and request duration
117        latency
118            .tags
119            .push(Tag::new("status_code", response.status().as_str()));
120        latency.fields.push(Field::new("duration", duration));
121
122        // Extract Status Code & reqwest::Response Bytes
123        let status_code = response.status();
124        let payload = response.bytes().await?;
125
126        Ok((status_code, payload, latency))
127    }
128}
129
130impl<Strategy, Parser> RestClient<Strategy, Parser> {
131    /// Construct a new [`Self`] using the provided configuration.
132    pub fn new(base_url: impl Into<Box<str>>, strategy: Strategy, parser: Parser) -> Self {
133        Self {
134            http_client: reqwest::Client::new(),
135            base_url: base_url.into(),
136            strategy,
137            parser,
138        }
139    }
140}