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