Skip to main content

cbr_client/
client.rs

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