platz_sdk/client/
request.rs

1use super::base::PlatzClient;
2use super::error::PlatzClientError;
3use async_trait::async_trait;
4use reqwest::{ClientBuilder, RequestBuilder};
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::instrument;
8
9#[derive(Clone)]
10pub struct PlatzRequest<'a> {
11    client: &'a PlatzClient,
12    method: reqwest::Method,
13    path: String,
14    query: HashMap<String, String>,
15}
16
17#[derive(Deserialize)]
18pub struct Paginated<T> {
19    pub page: i64,
20    pub per_page: i64,
21    pub items: Vec<T>,
22    pub num_total: i64,
23}
24
25lazy_static::lazy_static! {
26    static ref HTTP_USER_AGENT: String = format!(
27        "{}/{}/{}",
28        env!("CARGO_PKG_NAME"),
29        env!("CARGO_PKG_VERSION"),
30        option_env!("CARGO_BIN_NAME").unwrap_or("lib")
31    );
32}
33
34impl<'a> PlatzRequest<'a> {
35    pub fn new<S>(client: &'a PlatzClient, method: reqwest::Method, path: S) -> Self
36    where
37        S: AsRef<str>,
38    {
39        Self {
40            client,
41            method,
42            path: path.as_ref().to_owned(),
43            query: Default::default(),
44        }
45    }
46
47    pub fn query<K, V>(mut self, key: K, value: V) -> Self
48    where
49        K: AsRef<str>,
50        V: AsRef<str>,
51    {
52        self.query
53            .insert(key.as_ref().to_owned(), value.as_ref().to_owned());
54        self
55    }
56
57    pub fn add_to_query<I, K, V>(mut self, iter: I) -> Self
58    where
59        I: IntoIterator<Item = (K, V)>,
60        K: AsRef<str>,
61        V: AsRef<str>,
62    {
63        for (key, value) in iter.into_iter() {
64            self.query
65                .insert(key.as_ref().to_owned(), value.as_ref().to_owned());
66        }
67        self
68    }
69
70    pub async fn request_builder(&self) -> Result<RequestBuilder, PlatzClientError> {
71        let (header_key, header_value) = self.client.authorization().await?;
72        Ok(ClientBuilder::new()
73            .user_agent(HTTP_USER_AGENT.clone())
74            .gzip(true)
75            .brotli(true)
76            .deflate(true)
77            .build()?
78            .request(
79                self.method.clone(),
80                self.client.build_url(&self.path).await?,
81            )
82            .header(header_key, header_value)
83            .query(&self.query))
84    }
85
86    pub async fn send_with_no_response(self) -> Result<(), PlatzClientError> {
87        self.request_builder()
88            .await?
89            .send()
90            .await?
91            .error_for_status_with_body()
92            .await?;
93        Ok(())
94    }
95
96    pub async fn send<T>(self) -> Result<T, PlatzClientError>
97    where
98        T: DeserializeOwned + Send,
99    {
100        Ok(self
101            .request_builder()
102            .await?
103            .send()
104            .await?
105            .error_for_status_with_body()
106            .await?
107            .json()
108            .await?)
109    }
110
111    #[instrument(skip_all, fields(path=self.path))]
112    pub async fn send_with_body<T, R>(self, body: T) -> Result<R, PlatzClientError>
113    where
114        T: Serialize,
115        R: DeserializeOwned + Send,
116    {
117        Ok(self
118            .request_builder()
119            .await?
120            .json(&body)
121            .send()
122            .await?
123            .error_for_status_with_body()
124            .await?
125            .json()
126            .await?)
127    }
128
129    pub async fn single_page<T>(
130        &self,
131        page_index: i64,
132        page_size: Option<i64>,
133    ) -> Result<Paginated<T>, PlatzClientError>
134    where
135        T: DeserializeOwned + Send,
136        Paginated<T>: DeserializeOwned + Send,
137    {
138        let mut paging_info = vec![("page", page_index.to_string())];
139        if let Some(size) = page_size {
140            paging_info.push(("page_size", size.to_string()))
141        }
142        let page = self
143            .request_builder()
144            .await?
145            .query(&paging_info)
146            .send()
147            .await?
148            .error_for_status_with_body()
149            .await?
150            .json()
151            .await?;
152        Ok(page)
153    }
154
155    #[instrument(skip_all, fields(path=self.path))]
156    pub async fn paginated<T>(self) -> Result<Vec<T>, PlatzClientError>
157    where
158        T: DeserializeOwned + Send,
159        Paginated<T>: DeserializeOwned + Send,
160    {
161        let page_size: Option<i64> = None;
162        let mut cur_page = self.single_page(1, page_size).await?;
163        let mut items = cur_page.items;
164
165        while cur_page.page * cur_page.per_page < cur_page.num_total {
166            let next_page = cur_page.page + 1;
167            cur_page = self.single_page(next_page, page_size).await?;
168            items.extend(cur_page.items.into_iter());
169        }
170
171        Ok(items)
172    }
173
174    #[instrument(skip_all, fields(path=self.path))]
175    pub async fn paginated_expect_one<T>(self) -> Result<T, PlatzClientError>
176    where
177        T: DeserializeOwned + Send,
178        Paginated<T>: DeserializeOwned + Send,
179    {
180        let items = self.paginated().await?;
181        match items.len() {
182            0 => Err(PlatzClientError::ExpectedOneGotNone),
183            1 => Ok(items.into_iter().next().unwrap()),
184            n => Err(PlatzClientError::ExpectedOneGotMany(n)),
185        }
186    }
187}
188#[async_trait]
189trait ResponseExt {
190    async fn error_for_status_with_body(self) -> Result<reqwest::Response, PlatzClientError>;
191}
192
193#[async_trait]
194impl ResponseExt for reqwest::Response {
195    async fn error_for_status_with_body(self) -> Result<reqwest::Response, PlatzClientError> {
196        let status = self.status();
197        if status.is_success() {
198            Ok(self)
199        } else {
200            let body = self
201                .text()
202                .await
203                .ok()
204                .map(|s| format!(": {s:?}"))
205                .unwrap_or_default();
206            let err_msg = format!("{status}{body}");
207            Err(PlatzClientError::HttpError(err_msg))
208        }
209    }
210}