http_request_derive/
http_request.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use http::{HeaderMap, StatusCode, Uri};
8use snafu::{OptionExt, ResultExt};
9use url::Url;
10
11use crate::{
12    error::{
13        BuildRequestSnafu, NonSuccessStatusSnafu, ParseUriSnafu, UnauthorizedSnafu,
14        UrlCannotBeABaseSnafu,
15    },
16    Error, FromHttpResponse, HttpRequestBody, HttpRequestQueryParams,
17};
18
19/// A trait implemented for types that are sent to the API as parameters
20#[async_trait]
21pub trait HttpRequest {
22    /// The response type that is expected to the request
23    type Response: FromHttpResponse;
24
25    /// The query type that is sent to the API endpoint
26    type Query: HttpRequestQueryParams;
27
28    /// The body type that is sent to the API endpoint
29    type Body: HttpRequestBody;
30
31    /// The method used to send the data to the API endpoint
32    const METHOD: http::Method;
33
34    /// Get the API endpoint path relative to the base URL
35    fn path(&self) -> String;
36
37    /// Get query parameters for the `http::Request`
38    fn query(&self) -> Option<&Self::Query> {
39        None
40    }
41
42    /// Get the body for the `http::Request`
43    fn body(&self) -> Option<&Self::Body> {
44        None
45    }
46
47    /// Get the headers for the `http::Request`
48    fn apply_headers(&self, headers: &mut HeaderMap) {
49        if let Some(body) = self.body() {
50            body.apply_headers(headers);
51        }
52    }
53
54    /// Build a HTTP request from the request type
55    fn to_http_request(&self, base_url: &Url) -> Result<http::request::Request<Vec<u8>>, Error> {
56        let body = self
57            .body()
58            .map(HttpRequestBody::to_vec)
59            .transpose()?
60            .unwrap_or_default();
61
62        let uri = {
63            let mut url = base_url.clone();
64            {
65                let mut segments =
66                    url.path_segments_mut()
67                        .ok()
68                        .with_context(|| UrlCannotBeABaseSnafu {
69                            url: base_url.clone(),
70                        })?;
71                let _ = segments.pop_if_empty().extend(self.path().split('/'));
72            }
73
74            let query = self
75                .query()
76                .map(HttpRequestQueryParams::http_request_query_string)
77                .transpose()?;
78            if let Some(query) = query {
79                let query = query.as_deref();
80                url.set_query(query);
81            }
82
83            url.as_str().parse::<Uri>().context(ParseUriSnafu)?
84        };
85
86        let mut headers = HeaderMap::new();
87        self.apply_headers(&mut headers);
88
89        let mut builder = http::request::Request::builder()
90            .method(Self::METHOD)
91            .uri(uri);
92
93        for (name, value) in &headers {
94            builder = builder.header(name, value);
95        }
96
97        builder.body(body).context(BuildRequestSnafu)
98    }
99
100    /// Convert the response from a `http::Response`
101    ///
102    /// # Errors
103    ///
104    /// Usually HTTP response codes that don't indicate success will be converted to the
105    /// corresponding [`Error`]. For example, a [`StatusCode::UNAUTHORIZED`] is converted
106    /// to [`Error::Unauthorized`]. This is the behavior found in the default implementation
107    /// and can be overwritten by a specialized implementation if required.
108    fn read_response(response: http::Response<Bytes>) -> Result<Self::Response, Error> {
109        match response.status() {
110            status if status.is_success() => Self::Response::from_http_response(response),
111
112            StatusCode::UNAUTHORIZED => Err(UnauthorizedSnafu.build()),
113            status => Err(NonSuccessStatusSnafu {
114                status,
115                data: response.into_body(),
116            }
117            .build()),
118        }
119    }
120
121    /// Convert the response from a `reqwest::Response`
122    ///
123    /// # Errors
124    ///
125    /// Usually HTTP response codes that don't indicate success will be converted to the
126    /// corresponding [`Error`]. For example, a [`StatusCode::UNAUTHORIZED`] is converted
127    /// to [`Error::Unauthorized`]. This is the behavior found in the default implementation
128    /// and can be overwritten by a specialized implementation if required.
129    #[cfg(feature = "reqwest")]
130    async fn read_reqwest_response(response: reqwest::Response) -> Result<Self::Response, Error> {
131        match response.status() {
132            status if status.is_success() => Self::Response::from_reqwest_response(response).await,
133
134            StatusCode::UNAUTHORIZED => Err(UnauthorizedSnafu.build()),
135            status => Err(NonSuccessStatusSnafu {
136                status,
137                data: response.bytes().await.context(crate::error::ReqwestSnafu {
138                    message: "Failed to receive error response",
139                })?,
140            }
141            .build()),
142        }
143    }
144}