1#[cfg(any(feature = "async", feature = "blocking"))]
4mod client;
5mod constants;
6mod convert;
7pub mod decode;
8mod payload;
9mod wire;
10
11use std::num::NonZeroU32;
12use std::time::{Duration, Instant};
13
14#[cfg(any(feature = "async", feature = "blocking"))]
15use reqwest::{StatusCode, header::HeaderMap};
16use thiserror::Error;
17
18use crate::models::{
19 BoardId, EngineName, IndexId, MarketName, ParseBoardError, ParseCandleBorderError,
20 ParseCandleError, ParseEngineError, ParseEventError, ParseHistoryDatesError,
21 ParseHistoryRecordError, ParseIndexAnalyticsError, ParseIndexError, ParseMarketError,
22 ParseOrderbookError, ParseSecStatError, ParseSecurityBoardError, ParseSecurityError,
23 ParseSecuritySnapshotError, ParseSiteNewsError, ParseTradeError, ParseTurnoverError, SecId,
24};
25
26#[cfg(all(feature = "async", feature = "history"))]
28pub use client::AsyncHistoryPages;
29#[cfg(all(feature = "blocking", feature = "history"))]
31pub use client::HistoryPages;
32#[cfg(feature = "async")]
34pub use client::{
35 AsyncCandlesPages, AsyncGlobalSecuritiesPages, AsyncIndexAnalyticsPages,
36 AsyncMarketSecuritiesPages, AsyncMarketTradesPages, AsyncMoexClient, AsyncMoexClientBuilder,
37 AsyncOwnedBoardScope, AsyncOwnedEngineScope, AsyncOwnedIndexScope, AsyncOwnedMarketScope,
38 AsyncOwnedMarketSecurityScope, AsyncOwnedSecurityResourceScope, AsyncOwnedSecurityScope,
39 AsyncRawIssRequestBuilder, AsyncSecStatsPages, AsyncSecuritiesPages, AsyncTradesPages,
40};
41#[cfg(all(feature = "async", feature = "news"))]
42pub use client::{AsyncEventsPages, AsyncSiteNewsPages};
44#[cfg(feature = "blocking")]
46pub use client::{
47 CandlesPages, GlobalSecuritiesPages, IndexAnalyticsPages, MarketSecuritiesPages,
48 MarketTradesPages, OwnedBoardScope, OwnedEngineScope, OwnedIndexScope, OwnedMarketScope,
49 OwnedMarketSecurityScope, OwnedSecurityResourceScope, OwnedSecurityScope, RawIssRequestBuilder,
50 SecStatsPages, SecuritiesPages, TradesPages,
51};
52#[cfg(all(feature = "blocking", feature = "news"))]
53pub use client::{EventsPages, SiteNewsPages};
55
56#[cfg(feature = "blocking")]
58pub type BlockingMoexClient = client::BlockingMoexClient;
59#[cfg(feature = "blocking")]
61pub type BlockingMoexClientBuilder = client::BlockingMoexClientBuilder;
62
63#[derive(Debug, Clone, Copy)]
67pub enum IssEndpoint<'a> {
68 Indexes,
70 IndexAnalytics { indexid: &'a IndexId },
72 Turnovers,
74 EngineTurnovers { engine: &'a EngineName },
76 Engines,
78 Markets { engine: &'a EngineName },
80 Boards {
82 engine: &'a EngineName,
83 market: &'a MarketName,
84 },
85 GlobalSecurities,
87 SecurityInfo { security: &'a SecId },
89 SecurityBoards { security: &'a SecId },
91 MarketSecurities {
93 engine: &'a EngineName,
94 market: &'a MarketName,
95 },
96 MarketSecurityInfo {
98 engine: &'a EngineName,
99 market: &'a MarketName,
100 security: &'a SecId,
101 },
102 MarketOrderbook {
104 engine: &'a EngineName,
105 market: &'a MarketName,
106 },
107 MarketTrades {
109 engine: &'a EngineName,
110 market: &'a MarketName,
111 },
112 SecStats {
114 engine: &'a EngineName,
115 market: &'a MarketName,
116 },
117 Securities {
119 engine: &'a EngineName,
120 market: &'a MarketName,
121 board: &'a BoardId,
122 },
123 BoardSecuritySnapshots {
125 engine: &'a EngineName,
126 market: &'a MarketName,
127 board: &'a BoardId,
128 },
129 Orderbook {
131 engine: &'a EngineName,
132 market: &'a MarketName,
133 board: &'a BoardId,
134 security: &'a SecId,
135 },
136 Trades {
138 engine: &'a EngineName,
139 market: &'a MarketName,
140 board: &'a BoardId,
141 security: &'a SecId,
142 },
143 Candles {
145 engine: &'a EngineName,
146 market: &'a MarketName,
147 board: &'a BoardId,
148 security: &'a SecId,
149 },
150 CandleBorders {
152 engine: &'a EngineName,
153 market: &'a MarketName,
154 security: &'a SecId,
155 },
156 #[cfg(feature = "news")]
158 SiteNews,
159 #[cfg(feature = "news")]
161 Events,
162 #[cfg(feature = "history")]
164 HistoryDates {
165 engine: &'a EngineName,
166 market: &'a MarketName,
167 board: &'a BoardId,
168 security: &'a SecId,
169 },
170 #[cfg(feature = "history")]
172 History {
173 engine: &'a EngineName,
174 market: &'a MarketName,
175 board: &'a BoardId,
176 security: &'a SecId,
177 },
178}
179
180impl IssEndpoint<'_> {
181 pub fn path(self) -> String {
183 match self {
184 Self::Indexes => constants::INDEXES_ENDPOINT.to_owned(),
185 Self::IndexAnalytics { indexid } => constants::index_analytics_endpoint(indexid),
186 Self::Turnovers => constants::TURNOVERS_ENDPOINT.to_owned(),
187 Self::EngineTurnovers { engine } => constants::engine_turnovers_endpoint(engine),
188 Self::Engines => constants::ENGINES_ENDPOINT.to_owned(),
189 Self::Markets { engine } => constants::markets_endpoint(engine),
190 Self::Boards { engine, market } => constants::boards_endpoint(engine, market),
191 Self::GlobalSecurities => constants::GLOBAL_SECURITIES_ENDPOINT.to_owned(),
192 Self::SecurityInfo { security } | Self::SecurityBoards { security } => {
193 constants::security_endpoint(security)
194 }
195 Self::MarketSecurities { engine, market } => {
196 constants::market_securities_endpoint(engine, market)
197 }
198 Self::MarketSecurityInfo {
199 engine,
200 market,
201 security,
202 } => constants::market_security_endpoint(engine, market, security),
203 Self::MarketOrderbook { engine, market } => {
204 constants::market_orderbook_endpoint(engine, market)
205 }
206 Self::MarketTrades { engine, market } => {
207 constants::market_trades_endpoint(engine, market)
208 }
209 Self::SecStats { engine, market } => constants::secstats_endpoint(engine, market),
210 Self::Securities {
211 engine,
212 market,
213 board,
214 }
215 | Self::BoardSecuritySnapshots {
216 engine,
217 market,
218 board,
219 } => constants::securities_endpoint(engine, market, board),
220 Self::Orderbook {
221 engine,
222 market,
223 board,
224 security,
225 } => constants::orderbook_endpoint(engine, market, board, security),
226 Self::Trades {
227 engine,
228 market,
229 board,
230 security,
231 } => constants::trades_endpoint(engine, market, board, security),
232 Self::Candles {
233 engine,
234 market,
235 board,
236 security,
237 } => constants::candles_endpoint(engine, market, board, security),
238 Self::CandleBorders {
239 engine,
240 market,
241 security,
242 } => constants::candleborders_endpoint(engine, market, security),
243 #[cfg(feature = "news")]
244 Self::SiteNews => constants::SITENEWS_ENDPOINT.to_owned(),
245 #[cfg(feature = "news")]
246 Self::Events => constants::EVENTS_ENDPOINT.to_owned(),
247 #[cfg(feature = "history")]
248 Self::HistoryDates {
249 engine,
250 market,
251 board,
252 security,
253 } => constants::history_dates_endpoint(engine, market, board, security),
254 #[cfg(feature = "history")]
255 Self::History {
256 engine,
257 market,
258 board,
259 security,
260 } => constants::history_endpoint(engine, market, board, security),
261 }
262 }
263
264 pub fn default_table(self) -> Option<&'static str> {
266 match self {
267 Self::Indexes => Some("indices"),
268 Self::IndexAnalytics { .. } => Some("analytics"),
269 Self::Turnovers | Self::EngineTurnovers { .. } => Some("turnovers"),
270 Self::Engines => Some("engines"),
271 Self::Markets { .. } => Some("markets"),
272 Self::Boards { .. } | Self::SecurityBoards { .. } => Some("boards"),
273 Self::GlobalSecurities
274 | Self::SecurityInfo { .. }
275 | Self::MarketSecurities { .. }
276 | Self::MarketSecurityInfo { .. }
277 | Self::Securities { .. } => Some("securities"),
278 Self::BoardSecuritySnapshots { .. } => Some("securities,marketdata"),
279 Self::Orderbook { .. } | Self::MarketOrderbook { .. } => Some("orderbook"),
280 Self::Trades { .. } | Self::MarketTrades { .. } => Some("trades"),
281 Self::Candles { .. } => Some("candles"),
282 Self::CandleBorders { .. } => Some("borders"),
283 Self::SecStats { .. } => Some("secstats"),
284 #[cfg(feature = "news")]
285 Self::SiteNews => Some("sitenews"),
286 #[cfg(feature = "news")]
287 Self::Events => Some("events"),
288 #[cfg(feature = "history")]
289 Self::HistoryDates { .. } => Some("dates"),
290 #[cfg(feature = "history")]
291 Self::History { .. } => Some("history"),
292 }
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub struct RetryPolicy {
299 max_attempts: NonZeroU32,
300 delay: Duration,
301}
302
303impl RetryPolicy {
304 pub fn new(max_attempts: NonZeroU32) -> Self {
308 Self {
309 max_attempts,
310 delay: Duration::from_millis(400),
311 }
312 }
313
314 pub fn with_delay(mut self, delay: Duration) -> Self {
316 self.delay = delay;
317 self
318 }
319
320 pub fn max_attempts(self) -> NonZeroU32 {
322 self.max_attempts
323 }
324
325 pub fn delay(self) -> Duration {
327 self.delay
328 }
329}
330
331impl Default for RetryPolicy {
332 fn default() -> Self {
333 Self::new(NonZeroU32::new(3).expect("retry policy default attempts must be non-zero"))
334 }
335}
336
337pub fn with_retry<T, F>(policy: RetryPolicy, mut action: F) -> Result<T, MoexError>
341where
342 F: FnMut() -> Result<T, MoexError>,
343{
344 let mut attempts_left = policy.max_attempts().get();
345 loop {
346 match action() {
347 Ok(value) => return Ok(value),
348 Err(error) if attempts_left > 1 && error.is_retryable() => {
349 attempts_left -= 1;
350 std::thread::sleep(policy.delay());
351 }
352 Err(error) => return Err(error),
353 }
354 }
355}
356
357#[cfg(feature = "async")]
361pub async fn with_retry_async<T, F, Fut, S, SleepFut>(
362 policy: RetryPolicy,
363 mut action: F,
364 mut sleep: S,
365) -> Result<T, MoexError>
366where
367 F: FnMut() -> Fut,
368 Fut: std::future::Future<Output = Result<T, MoexError>>,
369 S: FnMut(Duration) -> SleepFut,
370 SleepFut: std::future::Future<Output = ()>,
371{
372 let mut attempts_left = policy.max_attempts().get();
373 loop {
374 match action().await {
375 Ok(value) => return Ok(value),
376 Err(error) if attempts_left > 1 && error.is_retryable() => {
377 attempts_left -= 1;
378 sleep(policy.delay()).await;
379 }
380 Err(error) => return Err(error),
381 }
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub struct RateLimit {
390 min_interval: Duration,
391}
392
393impl RateLimit {
394 pub fn every(min_interval: Duration) -> Self {
396 Self { min_interval }
397 }
398
399 pub fn per_second(requests_per_second: NonZeroU32) -> Self {
403 let per_second_nanos: u128 = 1_000_000_000;
404 let requests = u128::from(requests_per_second.get());
405 let nanos = per_second_nanos.div_ceil(requests);
406 let nanos = u64::try_from(nanos).unwrap_or(u64::MAX);
407 Self::every(Duration::from_nanos(nanos))
408 }
409
410 pub fn min_interval(self) -> Duration {
412 self.min_interval
413 }
414}
415
416#[derive(Debug, Clone)]
417pub struct RateLimiter {
419 limit: RateLimit,
420 next_allowed_at: Option<Instant>,
421}
422
423impl RateLimiter {
424 pub fn new(limit: RateLimit) -> Self {
426 Self {
427 limit,
428 next_allowed_at: None,
429 }
430 }
431
432 pub fn limit(&self) -> RateLimit {
434 self.limit
435 }
436
437 pub fn reserve_delay(&mut self) -> Duration {
439 self.reserve_delay_at(Instant::now())
440 }
441
442 fn reserve_delay_at(&mut self, now: Instant) -> Duration {
443 let scheduled_at = match self.next_allowed_at {
444 Some(next_allowed_at) if next_allowed_at > now => next_allowed_at,
445 _ => now,
446 };
447 let delay = scheduled_at.saturating_duration_since(now);
448 self.next_allowed_at = Some(scheduled_at + self.limit.min_interval);
449 delay
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
454pub enum IssToggle {
456 #[default]
458 Off,
459 On,
461}
462
463impl IssToggle {
464 pub const fn as_query_value(self) -> &'static str {
466 match self {
467 Self::Off => "off",
468 Self::On => "on",
469 }
470 }
471}
472
473impl From<bool> for IssToggle {
474 fn from(value: bool) -> Self {
475 if value { Self::On } else { Self::Off }
476 }
477}
478
479#[derive(Debug, Clone, Default, PartialEq, Eq)]
480pub struct IssRequestOptions {
482 metadata: Option<IssToggle>,
483 data: Option<IssToggle>,
484 version: Option<IssToggle>,
485 json: Option<Box<str>>,
486}
487
488impl IssRequestOptions {
489 pub fn new() -> Self {
491 Self::default()
492 }
493
494 pub fn metadata(mut self, metadata: IssToggle) -> Self {
496 self.metadata = Some(metadata);
497 self
498 }
499
500 pub fn data(mut self, data: IssToggle) -> Self {
502 self.data = Some(data);
503 self
504 }
505
506 pub fn version(mut self, version: IssToggle) -> Self {
508 self.version = Some(version);
509 self
510 }
511
512 pub fn json(mut self, json: impl Into<String>) -> Self {
514 self.json = Some(json.into().into_boxed_str());
515 self
516 }
517
518 pub fn metadata_value(&self) -> Option<IssToggle> {
520 self.metadata
521 }
522
523 pub fn data_value(&self) -> Option<IssToggle> {
525 self.data
526 }
527
528 pub fn version_value(&self) -> Option<IssToggle> {
530 self.version
531 }
532
533 pub fn json_value(&self) -> Option<&str> {
535 self.json.as_deref()
536 }
537}
538
539#[derive(Debug, Clone)]
540#[cfg(any(feature = "async", feature = "blocking"))]
542pub struct RawIssResponse {
543 status: StatusCode,
544 headers: HeaderMap,
545 body: String,
546}
547
548#[cfg(any(feature = "async", feature = "blocking"))]
549impl RawIssResponse {
550 pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
551 Self {
552 status,
553 headers,
554 body,
555 }
556 }
557
558 pub fn status(&self) -> StatusCode {
560 self.status
561 }
562
563 pub fn headers(&self) -> &HeaderMap {
565 &self.headers
566 }
567
568 pub fn body(&self) -> &str {
570 &self.body
571 }
572
573 pub fn into_parts(self) -> (StatusCode, HeaderMap, String) {
575 (self.status, self.headers, self.body)
576 }
577}
578
579pub fn with_rate_limit<T, F>(limiter: &mut RateLimiter, action: F) -> T
581where
582 F: FnOnce() -> T,
583{
584 let delay = limiter.reserve_delay();
585 if !delay.is_zero() {
586 std::thread::sleep(delay);
587 }
588 action()
589}
590
591#[cfg(feature = "async")]
595pub async fn with_rate_limit_async<T, F, Fut, S, SleepFut>(
596 limiter: &mut RateLimiter,
597 action: F,
598 mut sleep: S,
599) -> T
600where
601 F: FnOnce() -> Fut,
602 Fut: std::future::Future<Output = T>,
603 S: FnMut(Duration) -> SleepFut,
604 SleepFut: std::future::Future<Output = ()>,
605{
606 let delay = limiter.reserve_delay();
607 if !delay.is_zero() {
608 sleep(delay).await;
609 }
610 action().await
611}
612
613#[derive(Debug, Error)]
614pub enum MoexError {
616 #[error("invalid base URL '{base_url}': {reason}")]
618 InvalidBaseUrl {
619 base_url: &'static str,
621 reason: String,
623 },
624 #[cfg(any(feature = "async", feature = "blocking"))]
626 #[error("failed to build HTTP client: {source}")]
627 BuildHttpClient {
628 #[source]
630 source: reqwest::Error,
631 },
632 #[error(
634 "async rate limit requires sleep function; set AsyncMoexClientBuilder::rate_limit_sleep(...)"
635 )]
636 MissingAsyncRateLimitSleep,
637 #[error("failed to build URL for endpoint '{endpoint}': {reason}")]
639 EndpointUrl {
640 endpoint: Box<str>,
642 reason: String,
644 },
645 #[error("raw request path is not set")]
647 MissingRawPath,
648 #[error("invalid raw request path '{path}': {reason}")]
650 InvalidRawPath {
651 path: Box<str>,
653 reason: Box<str>,
655 },
656 #[error("raw endpoint '{endpoint}' does not contain table '{table}'")]
658 MissingRawTable {
659 endpoint: Box<str>,
661 table: Box<str>,
663 },
664 #[error(
666 "raw table '{table}' from endpoint '{endpoint}' has invalid row width at row {row}: expected {expected}, got {actual}"
667 )]
668 InvalidRawTableRowWidth {
669 endpoint: Box<str>,
671 table: Box<str>,
673 row: usize,
675 expected: usize,
677 actual: usize,
679 },
680 #[error("failed to decode raw table '{table}' row {row} from endpoint '{endpoint}': {source}")]
682 InvalidRawTableRow {
683 endpoint: Box<str>,
685 table: Box<str>,
687 row: usize,
689 #[source]
691 source: serde_json::Error,
692 },
693 #[cfg(any(feature = "async", feature = "blocking"))]
695 #[error("request to endpoint '{endpoint}' failed: {source}")]
696 Request {
697 endpoint: Box<str>,
699 #[source]
701 source: reqwest::Error,
702 },
703 #[cfg(any(feature = "async", feature = "blocking"))]
705 #[error(
706 "endpoint '{endpoint}' returned HTTP {status} (content-type={content_type:?}, prefix={body_prefix:?})"
707 )]
708 HttpStatus {
709 endpoint: Box<str>,
711 status: StatusCode,
713 content_type: Option<Box<str>>,
715 body_prefix: Box<str>,
717 },
718 #[cfg(any(feature = "async", feature = "blocking"))]
720 #[error("failed to read endpoint '{endpoint}' response body: {source}")]
721 ReadBody {
722 endpoint: Box<str>,
724 #[source]
726 source: reqwest::Error,
727 },
728 #[error("failed to decode endpoint '{endpoint}' JSON payload: {source}")]
730 Decode {
731 endpoint: Box<str>,
733 #[source]
735 source: serde_json::Error,
736 },
737 #[error(
739 "endpoint '{endpoint}' returned non-JSON payload (content-type={content_type:?}, prefix={body_prefix:?})"
740 )]
741 NonJsonPayload {
742 endpoint: Box<str>,
744 content_type: Option<Box<str>>,
746 body_prefix: Box<str>,
748 },
749 #[error("endpoint '{endpoint}' returned unexpected security rows count: {row_count}")]
751 UnexpectedSecurityRows {
752 endpoint: Box<str>,
754 row_count: usize,
756 },
757 #[error("endpoint '{endpoint}' returned unexpected history dates rows count: {row_count}")]
759 UnexpectedHistoryDatesRows {
760 endpoint: Box<str>,
762 row_count: usize,
764 },
765 #[error("invalid index row {row} from endpoint '{endpoint}': {source}")]
767 InvalidIndex {
768 endpoint: Box<str>,
770 row: usize,
772 #[source]
774 source: ParseIndexError,
775 },
776 #[error("invalid history dates row {row} from endpoint '{endpoint}': {source}")]
778 InvalidHistoryDates {
779 endpoint: Box<str>,
781 row: usize,
783 #[source]
785 source: ParseHistoryDatesError,
786 },
787 #[error("invalid history row {row} from endpoint '{endpoint}': {source}")]
789 InvalidHistory {
790 endpoint: Box<str>,
792 row: usize,
794 #[source]
796 source: ParseHistoryRecordError,
797 },
798 #[error("invalid turnover row {row} from endpoint '{endpoint}': {source}")]
800 InvalidTurnover {
801 endpoint: Box<str>,
803 row: usize,
805 #[source]
807 source: ParseTurnoverError,
808 },
809 #[error("invalid sitenews row {row} from endpoint '{endpoint}': {source}")]
811 InvalidSiteNews {
812 endpoint: Box<str>,
814 row: usize,
816 #[source]
818 source: ParseSiteNewsError,
819 },
820 #[error("invalid events row {row} from endpoint '{endpoint}': {source}")]
822 InvalidEvent {
823 endpoint: Box<str>,
825 row: usize,
827 #[source]
829 source: ParseEventError,
830 },
831 #[error("invalid secstats row {row} from endpoint '{endpoint}': {source}")]
833 InvalidSecStat {
834 endpoint: Box<str>,
836 row: usize,
838 #[source]
840 source: ParseSecStatError,
841 },
842 #[error("invalid index analytics row {row} from endpoint '{endpoint}': {source}")]
844 InvalidIndexAnalytics {
845 endpoint: Box<str>,
847 row: usize,
849 #[source]
851 source: ParseIndexAnalyticsError,
852 },
853 #[error("invalid engine row {row} from endpoint '{endpoint}': {source}")]
855 InvalidEngine {
856 endpoint: Box<str>,
858 row: usize,
860 #[source]
862 source: ParseEngineError,
863 },
864 #[error("invalid market row {row} from endpoint '{endpoint}': {source}")]
866 InvalidMarket {
867 endpoint: Box<str>,
869 row: usize,
871 #[source]
873 source: ParseMarketError,
874 },
875 #[error("invalid board row {row} from endpoint '{endpoint}': {source}")]
877 InvalidBoard {
878 endpoint: Box<str>,
880 row: usize,
882 #[source]
884 source: ParseBoardError,
885 },
886 #[error("invalid security board row {row} from endpoint '{endpoint}': {source}")]
888 InvalidSecurityBoard {
889 endpoint: Box<str>,
891 row: usize,
893 #[source]
895 source: ParseSecurityBoardError,
896 },
897 #[error("invalid security row {row} from endpoint '{endpoint}': {source}")]
899 InvalidSecurity {
900 endpoint: Box<str>,
902 row: usize,
904 #[source]
906 source: ParseSecurityError,
907 },
908 #[error("invalid security snapshot {table} row {row} from endpoint '{endpoint}': {source}")]
910 InvalidSecuritySnapshot {
911 endpoint: Box<str>,
913 table: &'static str,
915 row: usize,
917 #[source]
919 source: ParseSecuritySnapshotError,
920 },
921 #[error("invalid orderbook row {row} from endpoint '{endpoint}': {source}")]
923 InvalidOrderbook {
924 endpoint: Box<str>,
926 row: usize,
928 #[source]
930 source: ParseOrderbookError,
931 },
932 #[error("invalid candle border row {row} from endpoint '{endpoint}': {source}")]
934 InvalidCandleBorder {
935 endpoint: Box<str>,
937 row: usize,
939 #[source]
941 source: ParseCandleBorderError,
942 },
943 #[error("invalid candle row {row} from endpoint '{endpoint}': {source}")]
945 InvalidCandle {
946 endpoint: Box<str>,
948 row: usize,
950 #[source]
952 source: ParseCandleError,
953 },
954 #[error("invalid trade row {row} from endpoint '{endpoint}': {source}")]
956 InvalidTrade {
957 endpoint: Box<str>,
959 row: usize,
961 #[source]
963 source: ParseTradeError,
964 },
965 #[error(
967 "pagination overflow for endpoint '{endpoint}': start={start}, limit={limit} exceeds u32"
968 )]
969 PaginationOverflow {
970 endpoint: Box<str>,
972 start: u32,
974 limit: u32,
976 },
977 #[error(
979 "pagination is stuck for endpoint '{endpoint}': repeated page at start={start}, limit={limit}"
980 )]
981 PaginationStuck {
982 endpoint: Box<str>,
984 start: u32,
986 limit: u32,
988 },
989}
990
991impl MoexError {
992 pub fn is_retryable(&self) -> bool {
994 match self {
995 #[cfg(any(feature = "async", feature = "blocking"))]
996 Self::BuildHttpClient { .. } => false,
997 #[cfg(any(feature = "async", feature = "blocking"))]
998 Self::Request { source, .. } => {
999 source.is_timeout()
1000 || source.is_connect()
1001 || source.status().is_some_and(is_retryable_status)
1002 }
1003 #[cfg(any(feature = "async", feature = "blocking"))]
1004 Self::ReadBody { .. } => true,
1005 #[cfg(any(feature = "async", feature = "blocking"))]
1006 Self::HttpStatus { status, .. } => is_retryable_status(*status),
1007 _ => false,
1008 }
1009 }
1010
1011 #[cfg(any(feature = "async", feature = "blocking"))]
1013 pub fn status_code(&self) -> Option<StatusCode> {
1014 match self {
1015 Self::Request { source, .. } => source.status(),
1016 Self::HttpStatus { status, .. } => Some(*status),
1017 _ => None,
1018 }
1019 }
1020
1021 pub fn response_body_prefix(&self) -> Option<&str> {
1023 match self {
1024 #[cfg(any(feature = "async", feature = "blocking"))]
1025 Self::HttpStatus { body_prefix, .. } | Self::NonJsonPayload { body_prefix, .. } => {
1026 Some(body_prefix)
1027 }
1028 #[cfg(not(any(feature = "async", feature = "blocking")))]
1029 Self::NonJsonPayload { body_prefix, .. } => Some(body_prefix),
1030 _ => None,
1031 }
1032 }
1033}
1034
1035#[cfg(any(feature = "async", feature = "blocking"))]
1036fn is_retryable_status(status: StatusCode) -> bool {
1037 status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
1038}
1039
1040#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1041enum RepeatPagePolicy {
1042 Error,
1043}
1044
1045#[cfg(test)]
1046mod tests;