1use std::num::NonZeroU32;
4use std::time::{Duration, Instant};
5
6#[cfg(any(feature = "async", feature = "blocking"))]
7use reqwest::{StatusCode, header::HeaderMap};
8use thiserror::Error;
9
10use crate::models::{
11 BoardId, EngineName, IndexId, MarketName, ParseBoardError, ParseCandleBorderError,
12 ParseCandleError, ParseEngineError, ParseEventError, ParseHistoryDatesError,
13 ParseHistoryRecordError, ParseIndexAnalyticsError, ParseIndexError, ParseMarketError,
14 ParseOrderbookError, ParseSecStatError, ParseSecurityBoardError, ParseSecurityError,
15 ParseSecuritySnapshotError, ParseSiteNewsError, ParseTradeError, ParseTurnoverError, SecId,
16};
17
18#[cfg(all(feature = "async", feature = "history"))]
20pub use client::AsyncHistoryPages;
21#[cfg(all(feature = "blocking", feature = "history"))]
23pub use client::HistoryPages;
24#[cfg(feature = "async")]
26pub use client::{
27 AsyncCandlesPages, AsyncGlobalSecuritiesPages, AsyncIndexAnalyticsPages,
28 AsyncMarketSecuritiesPages, AsyncMarketTradesPages, AsyncMoexClient, AsyncMoexClientBuilder,
29 AsyncOwnedBoardScope, AsyncOwnedEngineScope, AsyncOwnedIndexScope, AsyncOwnedMarketScope,
30 AsyncOwnedMarketSecurityScope, AsyncOwnedSecurityResourceScope, AsyncOwnedSecurityScope,
31 AsyncRawIssRequestBuilder, AsyncSecStatsPages, AsyncSecuritiesPages, AsyncTradesPages,
32};
33#[cfg(all(feature = "async", feature = "news"))]
34pub use client::{AsyncEventsPages, AsyncSiteNewsPages};
36#[cfg(feature = "blocking")]
38pub use client::{
39 CandlesPages, GlobalSecuritiesPages, IndexAnalyticsPages, MarketSecuritiesPages,
40 MarketTradesPages, OwnedBoardScope, OwnedEngineScope, OwnedIndexScope, OwnedMarketScope,
41 OwnedMarketSecurityScope, OwnedSecurityResourceScope, OwnedSecurityScope, RawIssRequestBuilder,
42 SecStatsPages, SecuritiesPages, TradesPages,
43};
44#[cfg(all(feature = "blocking", feature = "news"))]
45pub use client::{EventsPages, SiteNewsPages};
47
48#[cfg(any(feature = "async", feature = "blocking"))]
49mod client;
50mod constants;
51mod convert;
52pub mod decode;
53mod payload;
54mod wire;
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, Default)]
297pub enum IssToggle {
299 #[default]
301 Off,
302 On,
304}
305
306impl IssToggle {
307 pub const fn as_query_value(self) -> &'static str {
309 match self {
310 Self::Off => "off",
311 Self::On => "on",
312 }
313 }
314}
315
316impl From<bool> for IssToggle {
317 fn from(value: bool) -> Self {
318 if value { Self::On } else { Self::Off }
319 }
320}
321
322#[derive(Debug, Error)]
323pub enum MoexError {
325 #[error("invalid base URL '{base_url}': {reason}")]
327 InvalidBaseUrl {
328 base_url: &'static str,
330 reason: String,
332 },
333 #[cfg(any(feature = "async", feature = "blocking"))]
335 #[error("failed to build HTTP client: {source}")]
336 BuildHttpClient {
337 #[source]
339 source: reqwest::Error,
340 },
341 #[error(
343 "async rate limit requires sleep function; set AsyncMoexClientBuilder::rate_limit_sleep(...)"
344 )]
345 MissingAsyncRateLimitSleep,
346 #[error("failed to build URL for endpoint '{endpoint}': {reason}")]
348 EndpointUrl {
349 endpoint: Box<str>,
351 reason: String,
353 },
354 #[error("raw request path is not set")]
356 MissingRawPath,
357 #[error("invalid raw request path '{path}': {reason}")]
359 InvalidRawPath {
360 path: Box<str>,
362 reason: Box<str>,
364 },
365 #[error("raw endpoint '{endpoint}' does not contain table '{table}'")]
367 MissingRawTable {
368 endpoint: Box<str>,
370 table: Box<str>,
372 },
373 #[error(
375 "raw table '{table}' from endpoint '{endpoint}' has invalid row width at row {row}: expected {expected}, got {actual}"
376 )]
377 InvalidRawTableRowWidth {
378 endpoint: Box<str>,
380 table: Box<str>,
382 row: usize,
384 expected: usize,
386 actual: usize,
388 },
389 #[error("failed to decode raw table '{table}' row {row} from endpoint '{endpoint}': {source}")]
391 InvalidRawTableRow {
392 endpoint: Box<str>,
394 table: Box<str>,
396 row: usize,
398 #[source]
400 source: serde_json::Error,
401 },
402 #[cfg(any(feature = "async", feature = "blocking"))]
404 #[error("request to endpoint '{endpoint}' failed: {source}")]
405 Request {
406 endpoint: Box<str>,
408 #[source]
410 source: reqwest::Error,
411 },
412 #[cfg(any(feature = "async", feature = "blocking"))]
414 #[error(
415 "endpoint '{endpoint}' returned HTTP {status} (content-type={content_type:?}, prefix={body_prefix:?})"
416 )]
417 HttpStatus {
418 endpoint: Box<str>,
420 status: StatusCode,
422 content_type: Option<Box<str>>,
424 body_prefix: Box<str>,
426 },
427 #[cfg(any(feature = "async", feature = "blocking"))]
429 #[error("failed to read endpoint '{endpoint}' response body: {source}")]
430 ReadBody {
431 endpoint: Box<str>,
433 #[source]
435 source: reqwest::Error,
436 },
437 #[error("failed to decode endpoint '{endpoint}' JSON payload: {source}")]
439 Decode {
440 endpoint: Box<str>,
442 #[source]
444 source: serde_json::Error,
445 },
446 #[error(
448 "endpoint '{endpoint}' returned non-JSON payload (content-type={content_type:?}, prefix={body_prefix:?})"
449 )]
450 NonJsonPayload {
451 endpoint: Box<str>,
453 content_type: Option<Box<str>>,
455 body_prefix: Box<str>,
457 },
458 #[error("endpoint '{endpoint}' returned unexpected security rows count: {row_count}")]
460 UnexpectedSecurityRows {
461 endpoint: Box<str>,
463 row_count: usize,
465 },
466 #[error("endpoint '{endpoint}' returned unexpected history dates rows count: {row_count}")]
468 UnexpectedHistoryDatesRows {
469 endpoint: Box<str>,
471 row_count: usize,
473 },
474 #[error("invalid index row {row} from endpoint '{endpoint}': {source}")]
476 InvalidIndex {
477 endpoint: Box<str>,
479 row: usize,
481 #[source]
483 source: ParseIndexError,
484 },
485 #[error("invalid history dates row {row} from endpoint '{endpoint}': {source}")]
487 InvalidHistoryDates {
488 endpoint: Box<str>,
490 row: usize,
492 #[source]
494 source: ParseHistoryDatesError,
495 },
496 #[error("invalid history row {row} from endpoint '{endpoint}': {source}")]
498 InvalidHistory {
499 endpoint: Box<str>,
501 row: usize,
503 #[source]
505 source: ParseHistoryRecordError,
506 },
507 #[error("invalid turnover row {row} from endpoint '{endpoint}': {source}")]
509 InvalidTurnover {
510 endpoint: Box<str>,
512 row: usize,
514 #[source]
516 source: ParseTurnoverError,
517 },
518 #[error("invalid sitenews row {row} from endpoint '{endpoint}': {source}")]
520 InvalidSiteNews {
521 endpoint: Box<str>,
523 row: usize,
525 #[source]
527 source: ParseSiteNewsError,
528 },
529 #[error("invalid events row {row} from endpoint '{endpoint}': {source}")]
531 InvalidEvent {
532 endpoint: Box<str>,
534 row: usize,
536 #[source]
538 source: ParseEventError,
539 },
540 #[error("invalid secstats row {row} from endpoint '{endpoint}': {source}")]
542 InvalidSecStat {
543 endpoint: Box<str>,
545 row: usize,
547 #[source]
549 source: ParseSecStatError,
550 },
551 #[error("invalid index analytics row {row} from endpoint '{endpoint}': {source}")]
553 InvalidIndexAnalytics {
554 endpoint: Box<str>,
556 row: usize,
558 #[source]
560 source: ParseIndexAnalyticsError,
561 },
562 #[error("invalid engine row {row} from endpoint '{endpoint}': {source}")]
564 InvalidEngine {
565 endpoint: Box<str>,
567 row: usize,
569 #[source]
571 source: ParseEngineError,
572 },
573 #[error("invalid market row {row} from endpoint '{endpoint}': {source}")]
575 InvalidMarket {
576 endpoint: Box<str>,
578 row: usize,
580 #[source]
582 source: ParseMarketError,
583 },
584 #[error("invalid board row {row} from endpoint '{endpoint}': {source}")]
586 InvalidBoard {
587 endpoint: Box<str>,
589 row: usize,
591 #[source]
593 source: ParseBoardError,
594 },
595 #[error("invalid security board row {row} from endpoint '{endpoint}': {source}")]
597 InvalidSecurityBoard {
598 endpoint: Box<str>,
600 row: usize,
602 #[source]
604 source: ParseSecurityBoardError,
605 },
606 #[error("invalid security row {row} from endpoint '{endpoint}': {source}")]
608 InvalidSecurity {
609 endpoint: Box<str>,
611 row: usize,
613 #[source]
615 source: ParseSecurityError,
616 },
617 #[error("invalid security snapshot {table} row {row} from endpoint '{endpoint}': {source}")]
619 InvalidSecuritySnapshot {
620 endpoint: Box<str>,
622 table: &'static str,
624 row: usize,
626 #[source]
628 source: ParseSecuritySnapshotError,
629 },
630 #[error("invalid orderbook row {row} from endpoint '{endpoint}': {source}")]
632 InvalidOrderbook {
633 endpoint: Box<str>,
635 row: usize,
637 #[source]
639 source: ParseOrderbookError,
640 },
641 #[error("invalid candle border row {row} from endpoint '{endpoint}': {source}")]
643 InvalidCandleBorder {
644 endpoint: Box<str>,
646 row: usize,
648 #[source]
650 source: ParseCandleBorderError,
651 },
652 #[error("invalid candle row {row} from endpoint '{endpoint}': {source}")]
654 InvalidCandle {
655 endpoint: Box<str>,
657 row: usize,
659 #[source]
661 source: ParseCandleError,
662 },
663 #[error("invalid trade row {row} from endpoint '{endpoint}': {source}")]
665 InvalidTrade {
666 endpoint: Box<str>,
668 row: usize,
670 #[source]
672 source: ParseTradeError,
673 },
674 #[error(
676 "pagination overflow for endpoint '{endpoint}': start={start}, limit={limit} exceeds u32"
677 )]
678 PaginationOverflow {
679 endpoint: Box<str>,
681 start: u32,
683 limit: u32,
685 },
686 #[error(
688 "pagination is stuck for endpoint '{endpoint}': repeated page at start={start}, limit={limit}"
689 )]
690 PaginationStuck {
691 endpoint: Box<str>,
693 start: u32,
695 limit: u32,
697 },
698}
699
700impl MoexError {
701 pub fn is_retryable(&self) -> bool {
703 match self {
704 #[cfg(any(feature = "async", feature = "blocking"))]
705 Self::BuildHttpClient { .. } => false,
706 #[cfg(any(feature = "async", feature = "blocking"))]
707 Self::Request { source, .. } => {
708 source.is_timeout()
709 || source.is_connect()
710 || source.status().is_some_and(is_retryable_status)
711 }
712 #[cfg(any(feature = "async", feature = "blocking"))]
713 Self::ReadBody { .. } => true,
714 #[cfg(any(feature = "async", feature = "blocking"))]
715 Self::HttpStatus { status, .. } => is_retryable_status(*status),
716 _ => false,
717 }
718 }
719
720 #[cfg(any(feature = "async", feature = "blocking"))]
722 pub fn status_code(&self) -> Option<StatusCode> {
723 match self {
724 Self::Request { source, .. } => source.status(),
725 Self::HttpStatus { status, .. } => Some(*status),
726 _ => None,
727 }
728 }
729
730 pub fn response_body_prefix(&self) -> Option<&str> {
732 match self {
733 #[cfg(any(feature = "async", feature = "blocking"))]
734 Self::HttpStatus { body_prefix, .. } | Self::NonJsonPayload { body_prefix, .. } => {
735 Some(body_prefix)
736 }
737 #[cfg(not(any(feature = "async", feature = "blocking")))]
738 Self::NonJsonPayload { body_prefix, .. } => Some(body_prefix),
739 _ => None,
740 }
741 }
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq)]
745enum RepeatPagePolicy {
746 Error,
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub struct RetryPolicy {
752 max_attempts: NonZeroU32,
753 delay: Duration,
754}
755
756impl RetryPolicy {
757 pub fn new(max_attempts: NonZeroU32) -> Self {
761 Self {
762 max_attempts,
763 delay: Duration::from_millis(400),
764 }
765 }
766
767 pub fn with_delay(mut self, delay: Duration) -> Self {
769 self.delay = delay;
770 self
771 }
772
773 pub fn max_attempts(self) -> NonZeroU32 {
775 self.max_attempts
776 }
777
778 pub fn delay(self) -> Duration {
780 self.delay
781 }
782}
783
784impl Default for RetryPolicy {
785 fn default() -> Self {
786 Self::new(NonZeroU32::new(3).expect("retry policy default attempts must be non-zero"))
787 }
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub struct RateLimit {
795 min_interval: Duration,
796}
797
798impl RateLimit {
799 pub fn every(min_interval: Duration) -> Self {
801 Self { min_interval }
802 }
803
804 pub fn per_second(requests_per_second: NonZeroU32) -> Self {
808 let per_second_nanos: u128 = 1_000_000_000;
809 let requests = u128::from(requests_per_second.get());
810 let nanos = per_second_nanos.div_ceil(requests);
811 let nanos = u64::try_from(nanos).unwrap_or(u64::MAX);
812 Self::every(Duration::from_nanos(nanos))
813 }
814
815 pub fn min_interval(self) -> Duration {
817 self.min_interval
818 }
819}
820
821#[derive(Debug, Clone)]
822pub struct RateLimiter {
824 limit: RateLimit,
825 next_allowed_at: Option<Instant>,
826}
827
828impl RateLimiter {
829 pub fn new(limit: RateLimit) -> Self {
831 Self {
832 limit,
833 next_allowed_at: None,
834 }
835 }
836
837 pub fn limit(&self) -> RateLimit {
839 self.limit
840 }
841
842 pub fn reserve_delay(&mut self) -> Duration {
844 self.reserve_delay_at(Instant::now())
845 }
846
847 fn reserve_delay_at(&mut self, now: Instant) -> Duration {
848 let scheduled_at = match self.next_allowed_at {
849 Some(next_allowed_at) if next_allowed_at > now => next_allowed_at,
850 _ => now,
851 };
852 let delay = scheduled_at.saturating_duration_since(now);
853 self.next_allowed_at = Some(scheduled_at + self.limit.min_interval);
854 delay
855 }
856}
857
858#[derive(Debug, Clone, Default, PartialEq, Eq)]
859pub struct IssRequestOptions {
861 metadata: Option<IssToggle>,
862 data: Option<IssToggle>,
863 version: Option<IssToggle>,
864 json: Option<Box<str>>,
865}
866
867impl IssRequestOptions {
868 pub fn new() -> Self {
870 Self::default()
871 }
872
873 pub fn metadata(mut self, metadata: IssToggle) -> Self {
875 self.metadata = Some(metadata);
876 self
877 }
878
879 pub fn data(mut self, data: IssToggle) -> Self {
881 self.data = Some(data);
882 self
883 }
884
885 pub fn version(mut self, version: IssToggle) -> Self {
887 self.version = Some(version);
888 self
889 }
890
891 pub fn json(mut self, json: impl Into<String>) -> Self {
893 self.json = Some(json.into().into_boxed_str());
894 self
895 }
896
897 pub fn metadata_value(&self) -> Option<IssToggle> {
899 self.metadata
900 }
901
902 pub fn data_value(&self) -> Option<IssToggle> {
904 self.data
905 }
906
907 pub fn version_value(&self) -> Option<IssToggle> {
909 self.version
910 }
911
912 pub fn json_value(&self) -> Option<&str> {
914 self.json.as_deref()
915 }
916}
917
918#[derive(Debug, Clone)]
919#[cfg(any(feature = "async", feature = "blocking"))]
921pub struct RawIssResponse {
922 status: StatusCode,
923 headers: HeaderMap,
924 body: String,
925}
926
927#[cfg(any(feature = "async", feature = "blocking"))]
928impl RawIssResponse {
929 pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: String) -> Self {
930 Self {
931 status,
932 headers,
933 body,
934 }
935 }
936
937 pub fn status(&self) -> StatusCode {
939 self.status
940 }
941
942 pub fn headers(&self) -> &HeaderMap {
944 &self.headers
945 }
946
947 pub fn body(&self) -> &str {
949 &self.body
950 }
951
952 pub fn into_parts(self) -> (StatusCode, HeaderMap, String) {
954 (self.status, self.headers, self.body)
955 }
956}
957
958pub fn with_retry<T, F>(policy: RetryPolicy, mut action: F) -> Result<T, MoexError>
962where
963 F: FnMut() -> Result<T, MoexError>,
964{
965 let mut attempts_left = policy.max_attempts().get();
966 loop {
967 match action() {
968 Ok(value) => return Ok(value),
969 Err(error) if attempts_left > 1 && error.is_retryable() => {
970 attempts_left -= 1;
971 std::thread::sleep(policy.delay());
972 }
973 Err(error) => return Err(error),
974 }
975 }
976}
977
978pub fn with_rate_limit<T, F>(limiter: &mut RateLimiter, action: F) -> T
980where
981 F: FnOnce() -> T,
982{
983 let delay = limiter.reserve_delay();
984 if !delay.is_zero() {
985 std::thread::sleep(delay);
986 }
987 action()
988}
989
990#[cfg(any(feature = "async", feature = "blocking"))]
991fn is_retryable_status(status: StatusCode) -> bool {
992 status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
993}
994
995#[cfg(feature = "async")]
999pub async fn with_retry_async<T, F, Fut, S, SleepFut>(
1000 policy: RetryPolicy,
1001 mut action: F,
1002 mut sleep: S,
1003) -> Result<T, MoexError>
1004where
1005 F: FnMut() -> Fut,
1006 Fut: std::future::Future<Output = Result<T, MoexError>>,
1007 S: FnMut(Duration) -> SleepFut,
1008 SleepFut: std::future::Future<Output = ()>,
1009{
1010 let mut attempts_left = policy.max_attempts().get();
1011 loop {
1012 match action().await {
1013 Ok(value) => return Ok(value),
1014 Err(error) if attempts_left > 1 && error.is_retryable() => {
1015 attempts_left -= 1;
1016 sleep(policy.delay()).await;
1017 }
1018 Err(error) => return Err(error),
1019 }
1020 }
1021}
1022
1023#[cfg(feature = "async")]
1027pub async fn with_rate_limit_async<T, F, Fut, S, SleepFut>(
1028 limiter: &mut RateLimiter,
1029 action: F,
1030 mut sleep: S,
1031) -> T
1032where
1033 F: FnOnce() -> Fut,
1034 Fut: std::future::Future<Output = T>,
1035 S: FnMut(Duration) -> SleepFut,
1036 SleepFut: std::future::Future<Output = ()>,
1037{
1038 let delay = limiter.reserve_delay();
1039 if !delay.is_zero() {
1040 sleep(delay).await;
1041 }
1042 action().await
1043}
1044
1045#[cfg(test)]
1046mod tests;