fiberplane_api_client/
api_client.rs

1use crate::builder::ApiClientBuilder;
2use anyhow::Result;
3use bytes::Bytes;
4use fiberplane_models::paging::{PagedVec, HAS_MORE_RESULTS_KEY, TOTAL_RESULTS_KEY};
5use reqwest::{Client, Method, RequestBuilder, Response, StatusCode};
6use thiserror::Error;
7use url::Url;
8
9#[derive(Debug, Error)]
10pub enum ApiClientError<T> {
11    /// This can only occur when a invalid base URL was provided.
12    #[error("An invalid URL was provided: {0}")]
13    ParseError(#[from] url::ParseError),
14
15    /// An error occurred in reqwest.
16    #[error("An error occurred while making the request: {0}")]
17    ClientError(#[from] reqwest::Error),
18
19    /// An error returned from the service. These errors are specific to the
20    /// endpoint that was called.
21    #[error(transparent)]
22    ServiceError(T),
23
24    /// A response was received, but we were unable to deserialize it. The
25    /// status code and the receive body are returned.
26    #[error("API returned an unknown response: Status: {0}, Body: {1:?}")]
27    InvalidResponse(StatusCode, Bytes),
28}
29
30#[derive(Debug)]
31pub struct ApiClient {
32    pub client: Client,
33    pub server: Url,
34}
35
36impl ApiClient {
37    pub fn request(
38        &self,
39        method: Method,
40        endpoint: &str,
41    ) -> Result<RequestBuilder, url::ParseError> {
42        let url = self.server.join(endpoint)?;
43
44        Ok(self.client.request(method, url))
45    }
46
47    pub fn builder(base_url: Url) -> ApiClientBuilder {
48        ApiClientBuilder::new(base_url)
49    }
50
51    pub async fn do_req_paged<T, E>(
52        &self,
53        req: RequestBuilder,
54    ) -> Result<PagedVec<T>, ApiClientError<E>>
55    where
56        T: serde::de::DeserializeOwned,
57        E: serde::de::DeserializeOwned,
58    {
59        // Send the request
60        let response = req.send().await?;
61
62        // Copy the status code here in case we are unable to parse the response as
63        // the Ok or Err variant. Same applies to the headers.
64        let status_code = response.status();
65        let has_more_results = Self::parse_has_more_results_header(&response);
66        let total_results = Self::parse_total_results_header(&response);
67
68        // Read the entire response into a buffer.
69        let body = response.bytes().await?;
70
71        // Try to parse the result as R. If it succeeds, return the result.
72        if let Ok(result) = serde_json::from_slice::<Vec<T>>(&body) {
73            //get header values
74            let result = PagedVec {
75                inner: result,
76                has_more_results,
77                total_results,
78            };
79            return Ok(result);
80        }
81
82        // Try to parse the result as E.
83        if let Ok(result) = serde_json::from_slice::<E>(&body) {
84            return Err(ApiClientError::ServiceError(result));
85        }
86
87        // If both failed, return the status_code and the body for the user to
88        // debug.
89        Err(ApiClientError::InvalidResponse(status_code, body))
90    }
91
92    pub async fn do_req<T, E>(&self, req: RequestBuilder) -> Result<T, ApiClientError<E>>
93    where
94        T: serde::de::DeserializeOwned,
95        E: serde::de::DeserializeOwned,
96    {
97        // Make request
98        let response = req.send().await?;
99
100        // Copy the status code here in case we are unable to parse the response as
101        // the Ok or Err variant.
102        let status_code = response.status();
103
104        // Read the entire response into a local buffer.
105        let body = response.bytes().await?;
106
107        // Try to parse the result as R.
108        if let Ok(result) = serde_json::from_slice::<T>(&body) {
109            return Ok(result);
110        }
111
112        // Try to parse the result as E.
113        if let Ok(result) = serde_json::from_slice::<E>(&body) {
114            return Err(ApiClientError::ServiceError(result));
115        }
116
117        // If both failed, return the status_code and the body for the user to
118        // debug.
119        Err(ApiClientError::InvalidResponse(status_code, body))
120    }
121
122    /// Parse the `has_more_results` header from the response. If no header is
123    /// found, or the value does not contain "true", then it will return false.
124    fn parse_has_more_results_header(response: &Response) -> bool {
125        response
126            .headers()
127            .get(HAS_MORE_RESULTS_KEY)
128            .map_or(false, |value| {
129                value
130                    .to_str()
131                    .map(|value| value.parse().unwrap_or_default())
132                    .unwrap_or_default()
133            })
134    }
135
136    /// Parse the `total_results` header from the response. If no header is
137    /// found or an invalid number is found it will return None.
138    fn parse_total_results_header(response: &Response) -> Option<u32> {
139        response
140            .headers()
141            .get(TOTAL_RESULTS_KEY)
142            .map(|value| value.to_str().ok().and_then(|value| value.parse().ok()))
143            .unwrap_or_default()
144    }
145}