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
14pub type PublicationId = Id<PublicationIdKind>;
16
17pub type DatasetId = Id<DatasetIdKind>;
19
20pub type CategoryId = Id<CategoryIdKind>;
22
23pub type IndicatorId = Id<IndicatorIdKind>;
25
26pub type MeasureId = Id<MeasureIdKind>;
28
29pub type UnitId = Id<UnitIdKind>;
31
32pub type RowId = Id<RowIdKind>;
34
35pub type PeriodId = Id<PeriodIdKind>;
37
38pub type ColumnId = Id<ColumnIdKind>;
40
41pub type ElementId = Id<ElementIdKind>;
43
44#[derive(Debug, Error, Clone, PartialEq, Eq)]
46pub enum InputError {
47 #[error("{kind} must be strictly positive, got {value}")]
49 NonPositiveId { kind: &'static str, value: i32 },
50 #[error("year span start {start} must be less than or equal to end {end}")]
52 InvalidYearSpan { start: i32, end: i32 },
53 #[error("parent reference must be -1 (root) or positive id, got {value}")]
55 InvalidParentRef { value: i32 },
56 #[cfg(feature = "chrono")]
58 #[error("chrono value is out of range for {kind}")]
59 ChronoOutOfRange { kind: &'static str },
60 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum ParentRef<T> {
119 Root,
121 Id(T),
123}
124
125impl<T> ParentRef<T>
126where
127 T: TryFrom<i32, Error = InputError> + Copy,
128{
129 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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(rename_all = "lowercase")]
190pub enum Periodicity {
191 Month,
193 Quarter,
195 Year,
197}
198
199pub struct Id<K>(NonZeroI32, PhantomData<K>);
201
202impl<K: IdKind> Id<K> {
203 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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
310#[serde(transparent)]
311pub struct Year(i32);
312
313impl Year {
314 #[must_use]
316 #[inline]
317 pub const fn new(value: i32) -> Self {
318 Self(value)
319 }
320
321 #[must_use]
323 #[inline]
324 pub fn get(self) -> i32 {
325 self.0
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
331pub struct IsoDateTime(PrimitiveDateTime);
332
333impl IsoDateTime {
334 #[must_use]
336 #[inline]
337 pub const fn new(value: PrimitiveDateTime) -> Self {
338 Self(value)
339 }
340
341 pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
343 PrimitiveDateTime::parse(value, ISO_DATETIME_FORMAT).map(Self)
344 }
345
346 #[must_use]
348 #[inline]
349 pub const fn get(self) -> PrimitiveDateTime {
350 self.0
351 }
352
353 #[cfg(feature = "chrono")]
355 pub fn try_from_chrono(value: NaiveDateTime) -> Result<Self, InputError> {
356 Self::try_from(value)
357 }
358
359 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
444pub struct DmyDate(Date);
445
446impl DmyDate {
447 #[must_use]
449 #[inline]
450 pub const fn new(value: Date) -> Self {
451 Self(value)
452 }
453
454 pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
456 Date::parse(value, DMY_DATE_FORMAT).map(Self)
457 }
458
459 #[must_use]
461 #[inline]
462 pub const fn get(self) -> Date {
463 self.0
464 }
465
466 #[cfg(feature = "chrono")]
468 pub fn try_from_chrono(value: NaiveDate) -> Result<Self, InputError> {
469 Self::try_from(value)
470 }
471
472 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
534pub struct YearSpan {
535 start: Year,
536 end: Year,
537}
538
539impl YearSpan {
540 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 #[must_use]
554 #[inline]
555 pub fn start(self) -> Year {
556 self.start
557 }
558
559 #[must_use]
561 #[inline]
562 pub fn end(self) -> Year {
563 self.end
564 }
565}
566pub trait IdKind {
568 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}