Skip to main content

moex_client/moex/
payload.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use serde::de::{
5    DeserializeSeed, IgnoredAny, IntoDeserializer, MapAccess, Visitor,
6    value::{MapDeserializer, StrDeserializer},
7};
8use serde_json::value::RawValue;
9
10use crate::models::{
11    Board, Candle, CandleBorder, Engine, Index, IndexAnalytics, Market, OrderbookLevel,
12    ParseSecuritySnapshotError, SecId, SecStat, Security, SecurityBoard, SecuritySnapshot, Trade,
13    Turnover,
14};
15#[cfg(feature = "news")]
16use crate::models::{Event, SiteNews};
17#[cfg(feature = "history")]
18use crate::models::{HistoryDates, HistoryRecord};
19
20use super::MoexError;
21use super::constants::*;
22use super::convert::*;
23use super::wire::*;
24
25#[derive(Debug, serde::Deserialize)]
26struct RawIssTableRowsPayload {
27    columns: Vec<Box<str>>,
28    data: Vec<Vec<serde_json::Value>>,
29}
30
31#[derive(Debug)]
32/// Разобранные верхнеуровневые блоки ISS payload-а для декодирования нескольких таблиц.
33///
34/// Позволяет один раз разобрать payload и затем извлекать таблицы через
35/// [`RawTables::take_rows`] без повторного разбора всего JSON-документа.
36pub struct RawTables {
37    endpoint: Box<str>,
38    blocks: HashMap<Box<str>, Box<RawValue>>,
39}
40
41impl RawTables {
42    /// Количество верхнеуровневых блоков в payload-е.
43    pub fn len(&self) -> usize {
44        self.blocks.len()
45    }
46
47    /// Проверить, что payload не содержит верхнеуровневых блоков.
48    pub fn is_empty(&self) -> bool {
49        self.blocks.is_empty()
50    }
51
52    /// Имена верхнеуровневых блоков payload-а.
53    ///
54    /// Итератор отражает текущее состояние кэша: после `take_rows(...)`
55    /// соответствующее имя больше не возвращается.
56    pub fn table_names(&self) -> impl Iterator<Item = &str> {
57        self.blocks.keys().map(Box::as_ref)
58    }
59
60    /// Декодировать строки выбранной таблицы и удалить её из кэша.
61    ///
62    /// Метод потребляет только выбранный верхнеуровневый блок и не вызывает
63    /// повторный разбор всего payload-а.
64    pub fn take_rows<T>(&mut self, table: impl AsRef<str>) -> Result<Vec<T>, MoexError>
65    where
66        T: serde::de::DeserializeOwned,
67    {
68        let table = table.as_ref();
69        let raw_table = self
70            .blocks
71            .remove(table)
72            .ok_or_else(|| MoexError::MissingRawTable {
73                endpoint: self.endpoint.clone(),
74                table: table.into(),
75            })?;
76        // Декодируем только выбранный блок, не затрагивая остальные таблицы.
77        let table_payload =
78            serde_json::from_str(raw_table.get()).map_err(|source| MoexError::Decode {
79                endpoint: format!("{} (table={})", self.endpoint, table).into_boxed_str(),
80                source,
81            })?;
82        decode_raw_table_rows_payload_with_context(table_payload, self.endpoint.as_ref(), table)
83    }
84}
85
86#[derive(Debug)]
87/// Заимствованное представление ISS-таблицы без промежуточного `Value`.
88pub struct RawTableView<'a> {
89    columns: Vec<Cow<'a, str>>,
90    data: Vec<Vec<&'a RawValue>>,
91}
92
93impl<'a> RawTableView<'a> {
94    /// Колонки таблицы в исходном порядке.
95    pub fn columns(&self) -> &[Cow<'a, str>] {
96        &self.columns
97    }
98
99    /// Все строки таблицы в виде raw JSON-значений.
100    pub fn rows(&self) -> &[Vec<&'a RawValue>] {
101        &self.data
102    }
103
104    /// Количество строк в таблице.
105    pub fn len(&self) -> usize {
106        self.data.len()
107    }
108
109    /// Проверить, пуста ли таблица.
110    pub fn is_empty(&self) -> bool {
111        self.data.is_empty()
112    }
113
114    /// Вернуть индекс колонки по имени.
115    pub fn column_index(&self, column: &str) -> Option<usize> {
116        self.columns.iter().position(|name| name.as_ref() == column)
117    }
118
119    /// Вернуть raw JSON-значение по `row` и имени `column`.
120    pub fn raw_value(&self, row: usize, column: &str) -> Option<&'a RawValue> {
121        let column_index = self.column_index(column)?;
122        self.data
123            .get(row)
124            .and_then(|values| values.get(column_index))
125            .copied()
126    }
127
128    /// Десериализовать значение колонки в пользовательский тип.
129    ///
130    /// Тип `T` может заимствовать данные из payload-а.
131    pub fn deserialize_value<T>(
132        &self,
133        row: usize,
134        column: &str,
135    ) -> Result<Option<T>, serde_json::Error>
136    where
137        T: serde::de::Deserialize<'a>,
138    {
139        self.raw_value(row, column)
140            .map(|raw| serde_json::from_str(raw.get()))
141            .transpose()
142    }
143}
144
145struct RawIssTableRowsSeed<'a> {
146    table: &'a str,
147}
148
149impl<'de> DeserializeSeed<'de> for RawIssTableRowsSeed<'_> {
150    type Value = Option<RawIssTableRowsPayload>;
151
152    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
153    where
154        D: serde::Deserializer<'de>,
155    {
156        deserializer.deserialize_map(RawIssTableRowsVisitor { table: self.table })
157    }
158}
159
160struct RawIssTableRowsVisitor<'a> {
161    table: &'a str,
162}
163
164impl<'de> Visitor<'de> for RawIssTableRowsVisitor<'_> {
165    type Value = Option<RawIssTableRowsPayload>;
166
167    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        formatter.write_str("ISS JSON object with table blocks")
169    }
170
171    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
172    where
173        A: MapAccess<'de>,
174    {
175        let mut selected = None;
176
177        while let Some(key) = map.next_key::<Cow<'de, str>>()? {
178            if key == self.table && selected.is_none() {
179                selected = Some(map.next_value::<RawIssTableRowsPayload>()?);
180            } else {
181                map.next_value::<IgnoredAny>()?;
182            }
183        }
184
185        Ok(selected)
186    }
187}
188
189#[derive(Debug, serde::Deserialize)]
190struct BorrowedRawIssTableRowsPayload<'a> {
191    #[serde(borrow)]
192    columns: Vec<Cow<'a, str>>,
193    #[serde(borrow)]
194    data: Vec<Vec<&'a RawValue>>,
195}
196
197struct BorrowedRawIssTableRowsSeed<'a> {
198    table: &'a str,
199}
200
201impl<'de> DeserializeSeed<'de> for BorrowedRawIssTableRowsSeed<'_> {
202    type Value = Option<BorrowedRawIssTableRowsPayload<'de>>;
203
204    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
205    where
206        D: serde::Deserializer<'de>,
207    {
208        deserializer.deserialize_map(BorrowedRawIssTableRowsVisitor { table: self.table })
209    }
210}
211
212struct BorrowedRawIssTableRowsVisitor<'a> {
213    table: &'a str,
214}
215
216impl<'de> Visitor<'de> for BorrowedRawIssTableRowsVisitor<'_> {
217    type Value = Option<BorrowedRawIssTableRowsPayload<'de>>;
218
219    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        formatter.write_str("ISS JSON object with table blocks")
221    }
222
223    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
224    where
225        A: MapAccess<'de>,
226    {
227        let mut selected = None;
228
229        while let Some(key) = map.next_key::<Cow<'de, str>>()? {
230            if key == self.table && selected.is_none() {
231                selected = Some(map.next_value::<BorrowedRawIssTableRowsPayload<'de>>()?);
232            } else {
233                map.next_value::<IgnoredAny>()?;
234            }
235        }
236
237        Ok(selected)
238    }
239}
240
241fn decode_single_raw_table_payload(
242    payload: &str,
243    table: &str,
244) -> Result<Option<RawIssTableRowsPayload>, serde_json::Error> {
245    let mut deserializer = serde_json::Deserializer::from_str(payload);
246    RawIssTableRowsSeed { table }.deserialize(&mut deserializer)
247}
248
249fn decode_top_level_raw_blocks(
250    payload: &str,
251) -> Result<HashMap<Box<str>, Box<RawValue>>, serde_json::Error> {
252    // Сохраняем каждый верхнеуровневый блок как `RawValue`, чтобы декодировать
253    // конкретные таблицы позже по запросу пользователя.
254    serde_json::from_str(payload)
255}
256
257fn decode_single_raw_table_payload_borrowed<'a>(
258    payload: &'a str,
259    table: &str,
260) -> Result<Option<BorrowedRawIssTableRowsPayload<'a>>, serde_json::Error> {
261    let mut deserializer = serde_json::Deserializer::from_str(payload);
262    BorrowedRawIssTableRowsSeed { table }.deserialize(&mut deserializer)
263}
264
265fn decode_raw_table_row<T>(
266    columns: &[Box<str>],
267    values: Vec<serde_json::Value>,
268) -> Result<T, serde_json::Error>
269where
270    T: serde::de::DeserializeOwned,
271{
272    let entries = columns
273        .iter()
274        .map(Box::as_ref)
275        .zip(values)
276        .map(|(column, value)| {
277            (
278                StrDeserializer::<serde_json::Error>::new(column),
279                value.into_deserializer(),
280            )
281        });
282    let deserializer = MapDeserializer::new(entries);
283    <T as serde::Deserialize>::deserialize(deserializer)
284}
285
286fn decode_raw_table_rows_payload_with_context<T>(
287    table_payload: RawIssTableRowsPayload,
288    endpoint: &str,
289    table: &str,
290) -> Result<Vec<T>, MoexError>
291where
292    T: serde::de::DeserializeOwned,
293{
294    let RawIssTableRowsPayload { columns, data } = table_payload;
295    let expected_width = columns.len();
296    let mut rows = Vec::with_capacity(data.len());
297
298    for (row, values) in data.into_iter().enumerate() {
299        let actual_width = values.len();
300        if actual_width != expected_width {
301            return Err(MoexError::InvalidRawTableRowWidth {
302                endpoint: endpoint.into(),
303                table: table.into(),
304                row,
305                expected: expected_width,
306                actual: actual_width,
307            });
308        }
309
310        let decoded = decode_raw_table_row::<T>(&columns, values).map_err(|source| {
311            MoexError::InvalidRawTableRow {
312                endpoint: endpoint.into(),
313                table: table.into(),
314                row,
315                source,
316            }
317        })?;
318        rows.push(decoded);
319    }
320
321    Ok(rows)
322}
323
324pub(super) fn decode_raw_table_rows_json_with_endpoint<T>(
325    payload: &str,
326    endpoint: &str,
327    table: &str,
328) -> Result<Vec<T>, MoexError>
329where
330    T: serde::de::DeserializeOwned,
331{
332    let table_payload = decode_single_raw_table_payload(payload, table)
333        .map_err(|source| MoexError::Decode {
334            endpoint: endpoint.into(),
335            source,
336        })?
337        .ok_or_else(|| MoexError::MissingRawTable {
338            endpoint: endpoint.into(),
339            table: table.into(),
340        })?;
341    decode_raw_table_rows_payload_with_context(table_payload, endpoint, table)
342}
343
344pub(super) fn decode_raw_tables_json_with_endpoint(
345    payload: &str,
346    endpoint: &str,
347) -> Result<RawTables, MoexError> {
348    let endpoint = endpoint.to_owned().into_boxed_str();
349    let raw_blocks = decode_top_level_raw_blocks(payload).map_err(|source| MoexError::Decode {
350        endpoint: endpoint.clone(),
351        source,
352    })?;
353    Ok(RawTables {
354        endpoint,
355        blocks: raw_blocks,
356    })
357}
358
359pub(super) fn decode_raw_table_view_json_with_endpoint<'a>(
360    payload: &'a str,
361    endpoint: &str,
362    table: &str,
363) -> Result<RawTableView<'a>, MoexError> {
364    let endpoint = endpoint.to_owned().into_boxed_str();
365    let table = table.to_owned().into_boxed_str();
366
367    let table_payload = decode_single_raw_table_payload_borrowed(payload, table.as_ref())
368        .map_err(|source| MoexError::Decode {
369            endpoint: endpoint.clone(),
370            source,
371        })?
372        .ok_or_else(|| MoexError::MissingRawTable {
373            endpoint: endpoint.clone(),
374            table: table.clone(),
375        })?;
376
377    let expected_width = table_payload.columns.len();
378    for (row, values) in table_payload.data.iter().enumerate() {
379        let actual_width = values.len();
380        if actual_width != expected_width {
381            return Err(MoexError::InvalidRawTableRowWidth {
382                endpoint: endpoint.clone(),
383                table: table.clone(),
384                row,
385                expected: expected_width,
386                actual: actual_width,
387            });
388        }
389    }
390
391    Ok(RawTableView {
392        columns: table_payload.columns,
393        data: table_payload.data,
394    })
395}
396
397pub(super) fn decode_indexes_json_payload(payload: &str) -> Result<Vec<Index>, MoexError> {
398    let payload: IndexesResponse =
399        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
400            endpoint: INDEXES_ENDPOINT.to_owned().into_boxed_str(),
401            source,
402        })?;
403    convert_index_rows(payload.indices.data, INDEXES_ENDPOINT)
404}
405
406pub(super) fn decode_engines_json_payload(payload: &str) -> Result<Vec<Engine>, MoexError> {
407    let payload: EnginesResponse =
408        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
409            endpoint: ENGINES_ENDPOINT.to_owned().into_boxed_str(),
410            source,
411        })?;
412    convert_engine_rows(payload.engines.data, ENGINES_ENDPOINT)
413}
414
415pub(super) fn decode_markets_json_with_endpoint(
416    payload: &str,
417    endpoint: &str,
418) -> Result<Vec<Market>, MoexError> {
419    let payload: MarketsResponse =
420        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
421            endpoint: endpoint.to_owned().into_boxed_str(),
422            source,
423        })?;
424    convert_market_rows(payload.markets.data, endpoint)
425}
426
427pub(super) fn decode_boards_json_with_endpoint(
428    payload: &str,
429    endpoint: &str,
430) -> Result<Vec<Board>, MoexError> {
431    let payload: BoardsResponse =
432        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
433            endpoint: endpoint.to_owned().into_boxed_str(),
434            source,
435        })?;
436    convert_board_rows(payload.boards.data, endpoint)
437}
438
439pub(super) fn decode_security_boards_json_with_endpoint(
440    payload: &str,
441    endpoint: &str,
442) -> Result<Vec<SecurityBoard>, MoexError> {
443    let payload: SecurityBoardsResponse =
444        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
445            endpoint: endpoint.to_owned().into_boxed_str(),
446            source,
447        })?;
448    convert_security_board_rows(payload.boards.data, endpoint)
449}
450
451pub(super) fn decode_securities_json_with_endpoint(
452    payload: &str,
453    endpoint: &str,
454) -> Result<Vec<Security>, MoexError> {
455    let payload: SecuritiesResponse =
456        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
457            endpoint: endpoint.to_owned().into_boxed_str(),
458            source,
459        })?;
460    convert_security_rows(payload.securities.data, endpoint)
461}
462
463pub(super) fn decode_board_security_snapshots_json_with_endpoint(
464    payload: &str,
465    endpoint: &str,
466) -> Result<Vec<SecuritySnapshot>, MoexError> {
467    let payload: BoardSecuritySnapshotsResponse =
468        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
469            endpoint: endpoint.to_owned().into_boxed_str(),
470            source,
471        })?;
472
473    let mut last_by_secid: HashMap<SecId, (usize, Option<f64>)> =
474        HashMap::with_capacity(payload.marketdata.data.len());
475    for (row, BoardMarketDataRow(secid, last)) in payload.marketdata.data.into_iter().enumerate() {
476        if let Some(last) = last
477            && !last.is_finite()
478        {
479            return Err(MoexError::InvalidSecuritySnapshot {
480                endpoint: endpoint.to_owned().into_boxed_str(),
481                table: "marketdata",
482                row,
483                source: ParseSecuritySnapshotError::NonFiniteLast(last),
484            });
485        }
486        let secid = parse_snapshot_secid(endpoint, "marketdata", row, secid)?;
487        last_by_secid.insert(secid, (row, last));
488    }
489
490    let mut snapshots = Vec::with_capacity(payload.securities.data.len().max(last_by_secid.len()));
491    for (row, BoardSecurityRow(secid, lot_size_raw)) in
492        payload.securities.data.into_iter().enumerate()
493    {
494        let secid = parse_snapshot_secid(endpoint, "securities", row, secid)?;
495        let lot_size = parse_snapshot_lot_size(endpoint, "securities", row, lot_size_raw)?;
496        let last = last_by_secid.remove(&secid).and_then(|(_, last)| last);
497
498        let snapshot =
499            SecuritySnapshot::try_from_parts(secid, lot_size, last).map_err(|source| {
500                MoexError::InvalidSecuritySnapshot {
501                    endpoint: endpoint.to_owned().into_boxed_str(),
502                    table: "securities",
503                    row,
504                    source,
505                }
506            })?;
507        snapshots.push(snapshot);
508    }
509
510    for (secid, (row, last)) in last_by_secid {
511        let snapshot = SecuritySnapshot::try_from_parts(secid, None, last).map_err(|source| {
512            MoexError::InvalidSecuritySnapshot {
513                endpoint: endpoint.to_owned().into_boxed_str(),
514                table: "marketdata",
515                row,
516                source,
517            }
518        })?;
519        snapshots.push(snapshot);
520    }
521
522    Ok(snapshots)
523}
524
525pub(super) fn decode_orderbook_json_with_endpoint(
526    payload: &str,
527    endpoint: &str,
528) -> Result<Vec<OrderbookLevel>, MoexError> {
529    let payload: OrderbookResponse =
530        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
531            endpoint: endpoint.to_owned().into_boxed_str(),
532            source,
533        })?;
534    convert_orderbook_rows(payload.orderbook.data, endpoint)
535}
536
537pub(super) fn decode_candle_borders_json_with_endpoint(
538    payload: &str,
539    endpoint: &str,
540) -> Result<Vec<CandleBorder>, MoexError> {
541    let payload: CandleBordersResponse =
542        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
543            endpoint: endpoint.to_owned().into_boxed_str(),
544            source,
545        })?;
546    convert_candle_border_rows(payload.borders.data, endpoint)
547}
548
549pub(super) fn decode_candles_json_with_endpoint(
550    payload: &str,
551    endpoint: &str,
552) -> Result<Vec<Candle>, MoexError> {
553    let payload: CandlesResponse =
554        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
555            endpoint: endpoint.to_owned().into_boxed_str(),
556            source,
557        })?;
558    convert_candle_rows(payload.candles.data, endpoint)
559}
560
561pub(super) fn decode_trades_json_with_endpoint(
562    payload: &str,
563    endpoint: &str,
564) -> Result<Vec<Trade>, MoexError> {
565    let payload: TradesResponse =
566        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
567            endpoint: endpoint.to_owned().into_boxed_str(),
568            source,
569        })?;
570    convert_trade_rows(payload.trades.data, endpoint)
571}
572
573pub(super) fn decode_index_analytics_json_with_endpoint(
574    payload: &str,
575    endpoint: &str,
576) -> Result<Vec<IndexAnalytics>, MoexError> {
577    let payload: IndexAnalyticsResponse =
578        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
579            endpoint: endpoint.to_owned().into_boxed_str(),
580            source,
581        })?;
582    convert_index_analytics_rows(payload.analytics.data, endpoint)
583}
584
585#[cfg(feature = "history")]
586pub(super) fn decode_history_dates_json_with_endpoint(
587    payload: &str,
588    endpoint: &str,
589) -> Result<Vec<HistoryDates>, MoexError> {
590    let payload: HistoryDatesResponse =
591        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
592            endpoint: endpoint.to_owned().into_boxed_str(),
593            source,
594        })?;
595    convert_history_dates_rows(payload.dates.data, endpoint)
596}
597
598#[cfg(feature = "history")]
599pub(super) fn decode_history_json_with_endpoint(
600    payload: &str,
601    endpoint: &str,
602) -> Result<Vec<HistoryRecord>, MoexError> {
603    let payload: HistoryResponse =
604        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
605            endpoint: endpoint.to_owned().into_boxed_str(),
606            source,
607        })?;
608    convert_history_rows(payload.history.data, endpoint)
609}
610
611pub(super) fn decode_turnovers_json_with_endpoint(
612    payload: &str,
613    endpoint: &str,
614) -> Result<Vec<Turnover>, MoexError> {
615    let payload: TurnoversResponse =
616        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
617            endpoint: endpoint.to_owned().into_boxed_str(),
618            source,
619        })?;
620    convert_turnover_rows(payload.turnovers.data, endpoint)
621}
622
623#[cfg(feature = "news")]
624pub(super) fn decode_sitenews_json_with_endpoint(
625    payload: &str,
626    endpoint: &str,
627) -> Result<Vec<SiteNews>, MoexError> {
628    let payload: SiteNewsResponse =
629        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
630            endpoint: endpoint.to_owned().into_boxed_str(),
631            source,
632        })?;
633    convert_sitenews_rows(payload.sitenews.data, endpoint)
634}
635
636#[cfg(feature = "news")]
637pub(super) fn decode_events_json_with_endpoint(
638    payload: &str,
639    endpoint: &str,
640) -> Result<Vec<Event>, MoexError> {
641    let payload: EventsResponse =
642        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
643            endpoint: endpoint.to_owned().into_boxed_str(),
644            source,
645        })?;
646    convert_event_rows(payload.events.data, endpoint)
647}
648
649pub(super) fn decode_secstats_json_with_endpoint(
650    payload: &str,
651    endpoint: &str,
652) -> Result<Vec<SecStat>, MoexError> {
653    let payload: SecStatsResponse =
654        serde_json::from_str(payload).map_err(|source| MoexError::Decode {
655            endpoint: endpoint.to_owned().into_boxed_str(),
656            source,
657        })?;
658    convert_secstats_rows(payload.secstats.data, endpoint)
659}
660
661fn parse_snapshot_secid(
662    endpoint: &str,
663    table: &'static str,
664    row: usize,
665    secid: String,
666) -> Result<SecId, MoexError> {
667    SecId::try_from(secid).map_err(|source| MoexError::InvalidSecuritySnapshot {
668        endpoint: endpoint.to_owned().into_boxed_str(),
669        table,
670        row,
671        source: ParseSecuritySnapshotError::InvalidSecId(source),
672    })
673}
674
675fn parse_snapshot_lot_size(
676    endpoint: &str,
677    table: &'static str,
678    row: usize,
679    lot_size: Option<i64>,
680) -> Result<Option<u32>, MoexError> {
681    match lot_size {
682        None => Ok(None),
683        Some(raw) if raw < 0 => Err(MoexError::InvalidSecuritySnapshot {
684            endpoint: endpoint.to_owned().into_boxed_str(),
685            table,
686            row,
687            source: ParseSecuritySnapshotError::NegativeLotSize(raw),
688        }),
689        Some(raw) => u32::try_from(raw)
690            .map(Some)
691            .map_err(|_| MoexError::InvalidSecuritySnapshot {
692                endpoint: endpoint.to_owned().into_boxed_str(),
693                table,
694                row,
695                source: ParseSecuritySnapshotError::LotSizeOutOfRange(raw),
696            }),
697    }
698}