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