Skip to main content

cbr_client/
types.rs

1use std::{marker::PhantomData, num::NonZeroI32};
2
3#[cfg(feature = "chrono")]
4use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use time::{Date, PrimitiveDateTime};
8
9const ISO_DATETIME_FORMAT: &[time::format_description::FormatItem<'static>] =
10    time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
11const DMY_DATE_FORMAT: &[time::format_description::FormatItem<'static>] =
12    time::macros::format_description!("[day].[month].[year]");
13
14/// Строго типизированный идентификатор публикации.
15pub type PublicationId = Id<PublicationIdKind>;
16
17/// Строго типизированный идентификатор показателя (`dataset`).
18pub type DatasetId = Id<DatasetIdKind>;
19
20/// Строго типизированный идентификатор категории.
21pub type CategoryId = Id<CategoryIdKind>;
22
23/// Строго типизированный идентификатор индикатора.
24pub type IndicatorId = Id<IndicatorIdKind>;
25
26/// Строго типизированный идентификатор разреза (`measure`).
27pub type MeasureId = Id<MeasureIdKind>;
28
29/// Строго типизированный идентификатор единицы измерения.
30pub type UnitId = Id<UnitIdKind>;
31
32/// Строго типизированный идентификатор строки данных.
33pub type RowId = Id<RowIdKind>;
34
35/// Строго типизированный идентификатор периода.
36pub type PeriodId = Id<PeriodIdKind>;
37
38/// Строго типизированный идентификатор колонки.
39pub type ColumnId = Id<ColumnIdKind>;
40
41/// Строго типизированный идентификатор элемента.
42pub type ElementId = Id<ElementIdKind>;
43
44/// Ошибки валидации входных параметров.
45#[derive(Debug, Error, Clone, PartialEq, Eq)]
46pub enum InputError {
47    /// Идентификатор должен быть строго положительным.
48    #[error("{kind} must be strictly positive, got {value}")]
49    NonPositiveId { kind: &'static str, value: i32 },
50    /// Левая граница периода больше правой.
51    #[error("year span start {start} must be less than or equal to end {end}")]
52    InvalidYearSpan { start: i32, end: i32 },
53    /// Родительская ссылка должна быть `-1` (корень) или положительным id.
54    #[error("parent reference must be -1 (root) or positive id, got {value}")]
55    InvalidParentRef { value: i32 },
56    /// Значение chrono выходит за поддерживаемый диапазон.
57    #[cfg(feature = "chrono")]
58    #[error("chrono value is out of range for {kind}")]
59    ChronoOutOfRange { kind: &'static str },
60    /// Для ISO-формата поддерживается только точность до секунд.
61    #[cfg(feature = "chrono")]
62    #[error("sub-second precision is not supported for chrono conversion")]
63    ChronoSubsecondPrecision,
64}
65
66#[doc(hidden)]
67pub enum PublicationIdKind {}
68impl IdKind for PublicationIdKind {
69    const NAME: &'static str = "publication_id";
70}
71#[doc(hidden)]
72pub enum DatasetIdKind {}
73impl IdKind for DatasetIdKind {
74    const NAME: &'static str = "dataset_id";
75}
76#[doc(hidden)]
77pub enum CategoryIdKind {}
78impl IdKind for CategoryIdKind {
79    const NAME: &'static str = "category_id";
80}
81#[doc(hidden)]
82pub enum IndicatorIdKind {}
83impl IdKind for IndicatorIdKind {
84    const NAME: &'static str = "indicator_id";
85}
86#[doc(hidden)]
87pub enum MeasureIdKind {}
88impl IdKind for MeasureIdKind {
89    const NAME: &'static str = "measure_id";
90}
91#[doc(hidden)]
92pub enum UnitIdKind {}
93impl IdKind for UnitIdKind {
94    const NAME: &'static str = "unit_id";
95}
96#[doc(hidden)]
97pub enum RowIdKind {}
98impl IdKind for RowIdKind {
99    const NAME: &'static str = "row_id";
100}
101#[doc(hidden)]
102pub enum PeriodIdKind {}
103impl IdKind for PeriodIdKind {
104    const NAME: &'static str = "period_id";
105}
106#[doc(hidden)]
107pub enum ColumnIdKind {}
108impl IdKind for ColumnIdKind {
109    const NAME: &'static str = "column_id";
110}
111#[doc(hidden)]
112pub enum ElementIdKind {}
113impl IdKind for ElementIdKind {
114    const NAME: &'static str = "element_id";
115}
116/// Родительская ссылка с явным корнем (`-1`) или валидным id.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum ParentRef<T> {
119    /// Корневой элемент (`-1` в API).
120    Root,
121    /// Ссылка на родительский элемент.
122    Id(T),
123}
124
125impl<T> ParentRef<T>
126where
127    T: TryFrom<i32, Error = InputError> + Copy,
128{
129    /// Создаёт ссылку из сырого значения API.
130    pub fn new(value: i32) -> Result<Self, InputError> {
131        match value {
132            -1 => Ok(Self::Root),
133            value if value <= 0 => Err(InputError::InvalidParentRef { value }),
134            _ => T::try_from(value).map(Self::Id),
135        }
136    }
137
138    /// Возвращает `true`, если это корневая ссылка.
139    #[must_use]
140    #[inline]
141    pub fn is_root(self) -> bool {
142        matches!(self, Self::Root)
143    }
144}
145
146impl<T: Copy> ParentRef<T> {
147    /// Возвращает id родителя, если ссылка не корневая.
148    #[must_use]
149    #[inline]
150    pub fn id(self) -> Option<T> {
151        match self {
152            Self::Root => None,
153            Self::Id(value) => Some(value),
154        }
155    }
156}
157
158impl<T> Serialize for ParentRef<T>
159where
160    T: Copy + Into<i32>,
161{
162    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
163    where
164        S: serde::Serializer,
165    {
166        let raw = match self {
167            Self::Root => -1,
168            Self::Id(value) => (*value).into(),
169        };
170        serializer.serialize_i32(raw)
171    }
172}
173
174impl<'de, T> Deserialize<'de> for ParentRef<T>
175where
176    T: TryFrom<i32, Error = InputError> + Copy,
177{
178    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179    where
180        D: serde::Deserializer<'de>,
181    {
182        let raw = i32::deserialize(deserializer)?;
183        Self::new(raw).map_err(serde::de::Error::custom)
184    }
185}
186
187/// Периодичность ряда.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(rename_all = "lowercase")]
190pub enum Periodicity {
191    /// Месячная периодичность.
192    Month,
193    /// Квартальная периодичность.
194    Quarter,
195    /// Годовая периодичность.
196    Year,
197}
198
199/// Обобщённый строго типизированный идентификатор.
200pub struct Id<K>(NonZeroI32, PhantomData<K>);
201
202impl<K: IdKind> Id<K> {
203    /// Создаёт идентификатор из целого числа.
204    pub fn new(value: i32) -> Result<Self, InputError> {
205        if value <= 0 {
206            return Err(InputError::NonPositiveId {
207                kind: K::NAME,
208                value,
209            });
210        }
211
212        let raw =
213            NonZeroI32::new(value).expect("strictly positive value always produces NonZeroI32");
214        Ok(Self(raw, PhantomData))
215    }
216
217    /// Создаёт идентификатор в `const`-контексте.
218    ///
219    /// Паникует на этапе компиляции, если `value <= 0`.
220    #[must_use]
221    #[inline]
222    pub const fn new_const(value: i32) -> Self {
223        if value <= 0 {
224            panic!("identifier must be strictly positive");
225        }
226
227        match NonZeroI32::new(value) {
228            Some(raw) => Self(raw, PhantomData),
229            None => panic!("identifier must be strictly positive"),
230        }
231    }
232
233    /// Возвращает исходное числовое значение идентификатора.
234    #[must_use]
235    #[inline]
236    pub fn get(self) -> i32 {
237        self.0.get()
238    }
239}
240
241impl<K> Copy for Id<K> {}
242
243impl<K> Clone for Id<K> {
244    fn clone(&self) -> Self {
245        *self
246    }
247}
248
249impl<K> PartialEq for Id<K> {
250    fn eq(&self, other: &Self) -> bool {
251        self.0 == other.0
252    }
253}
254
255impl<K> Eq for Id<K> {}
256
257impl<K> PartialOrd for Id<K> {
258    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
259        Some(self.cmp(other))
260    }
261}
262
263impl<K> Ord for Id<K> {
264    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
265        self.0.cmp(&other.0)
266    }
267}
268
269impl<K: IdKind> TryFrom<i32> for Id<K> {
270    type Error = InputError;
271
272    fn try_from(value: i32) -> Result<Self, Self::Error> {
273        Self::new(value)
274    }
275}
276
277impl<K> std::fmt::Debug for Id<K> {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        f.debug_tuple("Id").field(&self.0.get()).finish()
280    }
281}
282
283impl<K> std::hash::Hash for Id<K> {
284    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
285        self.0.hash(state);
286    }
287}
288
289impl<K: IdKind> Serialize for Id<K> {
290    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
291    where
292        S: serde::Serializer,
293    {
294        serializer.serialize_i32(self.get())
295    }
296}
297
298impl<'de, K: IdKind> Deserialize<'de> for Id<K> {
299    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300    where
301        D: serde::Deserializer<'de>,
302    {
303        let raw = i32::deserialize(deserializer)?;
304        Self::new(raw).map_err(serde::de::Error::custom)
305    }
306}
307
308/// Календарный год.
309#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
310#[serde(transparent)]
311pub struct Year(i32);
312
313impl Year {
314    /// Создаёт значение года.
315    #[must_use]
316    #[inline]
317    pub const fn new(value: i32) -> Self {
318        Self(value)
319    }
320
321    /// Возвращает исходное значение года.
322    #[must_use]
323    #[inline]
324    pub fn get(self) -> i32 {
325        self.0
326    }
327}
328
329/// Дата и время в формате `YYYY-MM-DDTHH:MM:SS`.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
331pub struct IsoDateTime(PrimitiveDateTime);
332
333impl IsoDateTime {
334    /// Создаёт значение из `time::PrimitiveDateTime`.
335    #[must_use]
336    #[inline]
337    pub const fn new(value: PrimitiveDateTime) -> Self {
338        Self(value)
339    }
340
341    /// Парсит строку формата `YYYY-MM-DDTHH:MM:SS`.
342    pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
343        PrimitiveDateTime::parse(value, ISO_DATETIME_FORMAT).map(Self)
344    }
345
346    /// Возвращает внутреннее представление.
347    #[must_use]
348    #[inline]
349    pub const fn get(self) -> PrimitiveDateTime {
350        self.0
351    }
352
353    /// Конвертирует значение `chrono::NaiveDateTime` в `IsoDateTime`.
354    #[cfg(feature = "chrono")]
355    pub fn try_from_chrono(value: NaiveDateTime) -> Result<Self, InputError> {
356        Self::try_from(value)
357    }
358
359    /// Конвертирует в `chrono::NaiveDateTime`.
360    #[cfg(feature = "chrono")]
361    pub fn try_to_chrono(self) -> Result<NaiveDateTime, InputError> {
362        let date = self.0.date();
363        let chrono_date = NaiveDate::from_ymd_opt(
364            date.year(),
365            u32::from(u8::from(date.month())),
366            u32::from(date.day()),
367        )
368        .ok_or(InputError::ChronoOutOfRange { kind: "date" })?;
369
370        let time = self.0.time();
371        chrono_date
372            .and_hms_nano_opt(
373                u32::from(time.hour()),
374                u32::from(time.minute()),
375                u32::from(time.second()),
376                time.nanosecond(),
377            )
378            .ok_or(InputError::ChronoOutOfRange { kind: "datetime" })
379    }
380}
381
382#[cfg(feature = "chrono")]
383impl TryFrom<NaiveDateTime> for IsoDateTime {
384    type Error = InputError;
385
386    fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
387        if value.nanosecond() != 0 {
388            return Err(InputError::ChronoSubsecondPrecision);
389        }
390
391        let month = chrono_month_to_time(value.month())?;
392        let day =
393            u8::try_from(value.day()).map_err(|_| InputError::ChronoOutOfRange { kind: "day" })?;
394        let date = Date::from_calendar_date(value.year(), month, day)
395            .map_err(|_| InputError::ChronoOutOfRange { kind: "date" })?;
396        let hour = u8::try_from(value.hour())
397            .map_err(|_| InputError::ChronoOutOfRange { kind: "hour" })?;
398        let minute = u8::try_from(value.minute())
399            .map_err(|_| InputError::ChronoOutOfRange { kind: "minute" })?;
400        let second = u8::try_from(value.second())
401            .map_err(|_| InputError::ChronoOutOfRange { kind: "second" })?;
402        let time = time::Time::from_hms(hour, minute, second)
403            .map_err(|_| InputError::ChronoOutOfRange { kind: "time" })?;
404
405        Ok(Self::new(PrimitiveDateTime::new(date, time)))
406    }
407}
408
409impl std::fmt::Display for IsoDateTime {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        let value = self
412            .0
413            .format(ISO_DATETIME_FORMAT)
414            .map_err(|_| std::fmt::Error)?;
415        f.write_str(&value)
416    }
417}
418
419impl Serialize for IsoDateTime {
420    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
421    where
422        S: serde::Serializer,
423    {
424        let value = self
425            .0
426            .format(ISO_DATETIME_FORMAT)
427            .map_err(serde::ser::Error::custom)?;
428        serializer.serialize_str(&value)
429    }
430}
431
432impl<'de> Deserialize<'de> for IsoDateTime {
433    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434    where
435        D: serde::Deserializer<'de>,
436    {
437        let value = <&str>::deserialize(deserializer)?;
438        Self::parse(value).map_err(serde::de::Error::custom)
439    }
440}
441
442/// Календарная дата в формате `DD.MM.YYYY`.
443#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
444pub struct DmyDate(Date);
445
446impl DmyDate {
447    /// Создаёт значение из `time::Date`.
448    #[must_use]
449    #[inline]
450    pub const fn new(value: Date) -> Self {
451        Self(value)
452    }
453
454    /// Парсит строку формата `DD.MM.YYYY`.
455    pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
456        Date::parse(value, DMY_DATE_FORMAT).map(Self)
457    }
458
459    /// Возвращает внутреннее представление.
460    #[must_use]
461    #[inline]
462    pub const fn get(self) -> Date {
463        self.0
464    }
465
466    /// Конвертирует значение `chrono::NaiveDate` в `DmyDate`.
467    #[cfg(feature = "chrono")]
468    pub fn try_from_chrono(value: NaiveDate) -> Result<Self, InputError> {
469        Self::try_from(value)
470    }
471
472    /// Конвертирует в `chrono::NaiveDate`.
473    #[cfg(feature = "chrono")]
474    pub fn try_to_chrono(self) -> Result<NaiveDate, InputError> {
475        let date = self.0;
476        NaiveDate::from_ymd_opt(
477            date.year(),
478            u32::from(u8::from(date.month())),
479            u32::from(date.day()),
480        )
481        .ok_or(InputError::ChronoOutOfRange { kind: "date" })
482    }
483}
484
485#[cfg(feature = "chrono")]
486impl TryFrom<NaiveDate> for DmyDate {
487    type Error = InputError;
488
489    fn try_from(value: NaiveDate) -> Result<Self, Self::Error> {
490        let month = chrono_month_to_time(value.month())?;
491        let day =
492            u8::try_from(value.day()).map_err(|_| InputError::ChronoOutOfRange { kind: "day" })?;
493        Date::from_calendar_date(value.year(), month, day)
494            .map(Self::new)
495            .map_err(|_| InputError::ChronoOutOfRange { kind: "date" })
496    }
497}
498
499impl std::fmt::Display for DmyDate {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        let value = self
502            .0
503            .format(DMY_DATE_FORMAT)
504            .map_err(|_| std::fmt::Error)?;
505        f.write_str(&value)
506    }
507}
508
509impl Serialize for DmyDate {
510    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
511    where
512        S: serde::Serializer,
513    {
514        let value = self
515            .0
516            .format(DMY_DATE_FORMAT)
517            .map_err(serde::ser::Error::custom)?;
518        serializer.serialize_str(&value)
519    }
520}
521
522impl<'de> Deserialize<'de> for DmyDate {
523    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
524    where
525        D: serde::Deserializer<'de>,
526    {
527        let value = <&str>::deserialize(deserializer)?;
528        Self::parse(value).map_err(serde::de::Error::custom)
529    }
530}
531
532/// Диапазон годов включительно.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
534pub struct YearSpan {
535    start: Year,
536    end: Year,
537}
538
539impl YearSpan {
540    /// Создаёт диапазон годов с проверкой `start <= end`.
541    pub fn new(start: Year, end: Year) -> Result<Self, InputError> {
542        if start > end {
543            return Err(InputError::InvalidYearSpan {
544                start: start.get(),
545                end: end.get(),
546            });
547        }
548
549        Ok(Self { start, end })
550    }
551
552    /// Левая граница диапазона.
553    #[must_use]
554    #[inline]
555    pub fn start(self) -> Year {
556        self.start
557    }
558
559    /// Правая граница диапазона.
560    #[must_use]
561    #[inline]
562    pub fn end(self) -> Year {
563        self.end
564    }
565}
566/// Маркер домена идентификатора.
567pub trait IdKind {
568    /// Имя поля для текста ошибки.
569    const NAME: &'static str;
570}
571
572impl<K: IdKind> From<Id<K>> for i32 {
573    fn from(value: Id<K>) -> Self {
574        value.get()
575    }
576}
577
578impl From<Year> for i32 {
579    fn from(value: Year) -> Self {
580        value.get()
581    }
582}
583
584#[cfg(feature = "chrono")]
585fn chrono_month_to_time(value: u32) -> Result<time::Month, InputError> {
586    let month = u8::try_from(value).map_err(|_| InputError::ChronoOutOfRange { kind: "month" })?;
587    time::Month::try_from(month).map_err(|_| InputError::ChronoOutOfRange { kind: "month" })
588}