Skip to main content

cbr_client/
client.rs

1use std::time::Duration;
2
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5
6pub use crate::client_common::DEFAULT_BASE_URL;
7use crate::client_common::{
8    cbr_endpoint_methods, configure_reqwest_builder, endpoint, normalize_base_url,
9};
10use crate::error::{CbrError, parse_json_body};
11use crate::models::{
12    CategoryNewResponse, DataExResponse, DataNewResponse, DataResponse, Dataset,
13    DatasetDescription, DatasetsExResponse, MeasuresResponse, Publication, YearRange,
14};
15use crate::query::{
16    DataExQuery, DataNewQuery, DataQuery, dataset_id_query, publication_id_query, years_ex_query,
17    years_query,
18};
19use crate::types::{DatasetId, MeasureId, PublicationId};
20
21const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
22
23macro_rules! impl_async_endpoint_method {
24    (
25        $doc:literal,
26        $name:ident,
27        ($($arg_name:ident : $arg_ty:ty),* $(,)?),
28        $ret:ty,
29        $path:literal,
30        no_query
31    ) => {
32        #[doc = $doc]
33        #[inline]
34        pub async fn $name(&self $(, $arg_name: $arg_ty)*) -> Result<$ret, CbrError> {
35            self.request_json($path).await
36        }
37    };
38    (
39        $doc:literal,
40        $name:ident,
41        ($($arg_name:ident : $arg_ty:ty),* $(,)?),
42        $ret:ty,
43        $path:literal,
44        query($query:expr)
45    ) => {
46        #[doc = $doc]
47        #[inline]
48        pub async fn $name(&self $(, $arg_name: $arg_ty)*) -> Result<$ret, CbrError> {
49            self.request_json_with_query($path, &$query).await
50        }
51    };
52}
53
54/// Builder асинхронного клиента [`CbrClient`].
55#[derive(Debug, Clone)]
56pub struct CbrClientBuilder {
57    pub(crate) base_url: String,
58    pub(crate) timeout: Duration,
59    pub(crate) user_agent: Option<String>,
60    pub(crate) proxy_url: Option<String>,
61    pub(crate) use_system_proxy: bool,
62}
63
64impl Default for CbrClientBuilder {
65    fn default() -> Self {
66        Self {
67            base_url: DEFAULT_BASE_URL.to_owned(),
68            timeout: DEFAULT_TIMEOUT,
69            user_agent: None,
70            proxy_url: None,
71            use_system_proxy: false,
72        }
73    }
74}
75
76impl CbrClientBuilder {
77    /// Создаёт builder с настройками по умолчанию.
78    #[must_use]
79    #[inline]
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Устанавливает базовый URL API.
85    ///
86    /// Если передана пустая строка, будет использован [`DEFAULT_BASE_URL`].
87    #[must_use]
88    #[inline]
89    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
90        self.base_url = normalize_base_url(base_url.into());
91        self
92    }
93
94    /// Устанавливает timeout для HTTP-запросов.
95    #[must_use]
96    #[inline]
97    pub fn timeout(mut self, timeout: Duration) -> Self {
98        self.timeout = timeout;
99        self
100    }
101
102    /// Устанавливает заголовок `User-Agent`.
103    #[must_use]
104    #[inline]
105    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
106        self.user_agent = Some(user_agent.into());
107        self
108    }
109
110    /// Устанавливает явный proxy URL для всех HTTP/HTTPS-запросов.
111    ///
112    /// Примеры:
113    /// - `http://127.0.0.1:8080`
114    /// - `socks5h://127.0.0.1:1080`
115    #[must_use]
116    #[inline]
117    pub fn proxy(mut self, proxy_url: impl Into<String>) -> Self {
118        self.proxy_url = Some(proxy_url.into());
119        self
120    }
121
122    /// Включает или отключает использование системных proxy-настроек
123    /// (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`).
124    ///
125    /// По умолчанию отключено (`false`) для предсказуемого поведения.
126    #[must_use]
127    #[inline]
128    pub fn use_system_proxy(mut self, enabled: bool) -> Self {
129        self.use_system_proxy = enabled;
130        self
131    }
132
133    /// Собирает асинхронный клиент.
134    pub fn build(self) -> Result<CbrClient, CbrError> {
135        // Принудительно используем HTTP/1.1 для стабильной работы с API ЦБ и mock-серверами.
136        let builder = configure_reqwest_builder!(
137            reqwest::Client::builder(),
138            timeout = self.timeout,
139            use_system_proxy = self.use_system_proxy,
140            proxy_url = self.proxy_url.as_deref(),
141            user_agent = self.user_agent.as_deref()
142        );
143
144        let http = builder.build().map_err(CbrError::build)?;
145        Ok(CbrClient {
146            base_url: normalize_base_url(&self.base_url),
147            http,
148        })
149    }
150
151    /// Собирает блокирующий клиент.
152    ///
153    /// Доступно только с feature `blocking`.
154    #[cfg(feature = "blocking")]
155    #[inline]
156    pub fn build_blocking(self) -> Result<crate::blocking::BlockingCbrClient, CbrError> {
157        crate::blocking::BlockingCbrClient::from_builder(self)
158    }
159}
160
161/// Асинхронный клиент API ЦБ РФ.
162#[derive(Debug, Clone)]
163pub struct CbrClient {
164    base_url: String,
165    http: reqwest::Client,
166}
167
168impl CbrClient {
169    /// Создаёт клиент с настройками по умолчанию.
170    #[inline]
171    pub fn new() -> Result<Self, CbrError> {
172        Self::builder().build()
173    }
174
175    /// Возвращает builder для тонкой настройки клиента.
176    #[must_use]
177    #[inline]
178    pub fn builder() -> CbrClientBuilder {
179        CbrClientBuilder::new()
180    }
181
182    /// Возвращает текущий базовый URL клиента.
183    #[must_use]
184    #[inline]
185    pub fn base_url(&self) -> &str {
186        &self.base_url
187    }
188
189    /// Выполняет GET-запрос к произвольному endpoint и десериализует JSON в тип пользователя.
190    ///
191    /// `path` указывается относительно `base_url`. Начальный `/` опционален.
192    #[inline]
193    pub async fn request_json<T>(&self, path: &str) -> Result<T, CbrError>
194    where
195        T: DeserializeOwned,
196    {
197        self.get_json(path).await
198    }
199
200    /// Выполняет GET-запрос с query-параметрами и десериализует JSON в тип пользователя.
201    ///
202    /// `path` указывается относительно `base_url`. Начальный `/` опционален.
203    #[inline]
204    pub async fn request_json_with_query<T, Q>(&self, path: &str, query: &Q) -> Result<T, CbrError>
205    where
206        T: DeserializeOwned,
207        Q: Serialize + ?Sized,
208    {
209        self.get_json_with_query(path, query).await
210    }
211
212    cbr_endpoint_methods!(impl_async_endpoint_method);
213
214    async fn get_json<T>(&self, path: &str) -> Result<T, CbrError>
215    where
216        T: DeserializeOwned,
217    {
218        let response = self
219            .http
220            .get(endpoint(&self.base_url, path))
221            .send()
222            .await
223            .map_err(CbrError::transport)?;
224        let status = response.status();
225        let body = response.bytes().await.map_err(CbrError::transport)?;
226        parse_json_body(status, body.as_ref())
227    }
228
229    async fn get_json_with_query<T, Q>(&self, path: &str, query: &Q) -> Result<T, CbrError>
230    where
231        T: DeserializeOwned,
232        Q: Serialize + ?Sized,
233    {
234        let response = self
235            .http
236            .get(endpoint(&self.base_url, path))
237            .query(query)
238            .send()
239            .await
240            .map_err(CbrError::transport)?;
241        let status = response.status();
242        let body = response.bytes().await.map_err(CbrError::transport)?;
243        parse_json_body(status, body.as_ref())
244    }
245}