Skip to main content

eternaltwin_core/
core.rs

1use crate::digest::DigestSha2_256;
2#[cfg(feature = "sqlx-postgres")]
3use crate::pg_num::PgU8;
4use crate::user::{ShortUser, UserIdRef};
5use async_trait::async_trait;
6use chrono::{DateTime, TimeZone, Timelike, Utc};
7use core::any::type_name;
8use core::fmt;
9use core::ops;
10#[cfg(feature = "sqlx-postgres")]
11use sqlx::postgres::types::PgRange;
12#[cfg(feature = "sqlx-postgres")]
13use sqlx::{postgres, Postgres};
14use std::cmp::Ordering;
15use std::ops::{Range, RangeFrom};
16use thiserror::Error;
17use uuid::Uuid;
18
19declare_new_uuid! {
20  /// UUID used as an idempotency key.
21  ///
22  /// Eternaltwin requires the use of UUIDv7 for idempotency.
23  ///
24  /// For REST requests, it should be passed through the `Idempotency-Key` header
25  ///
26  /// See <https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/>
27  pub struct IdempotencyKey(Uuid);
28  pub type ParseError = IdempotencyKeyParseError;
29  const SQL_NAME = "idempotency_key";
30}
31
32pub struct Cursor {
33  /// time for the query
34  pub time: Instant,
35  /// query fingerprint (to ensure the same options are passed)
36  pub fingerprint: DigestSha2_256,
37  /// stream position
38  pub position: Uuid,
39  /// direction
40  /// - Ascending: return items with an id strictly higher than `position`
41  /// - Descending: return items with an id strictly lower than `position`
42  pub dir: CursorDir,
43}
44
45pub enum CursorDir {
46  Ascending,
47  Descending,
48}
49
50pub type HtmlFragment = String;
51
52/// Protocol-relative URL
53///
54/// Should be avoided nowadays (in favor of HTTPS only) but Twinoid uses
55/// them.
56///
57/// See <https://en.wikipedia.org/wiki/URL#prurl>
58#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct PrUrl {
60  full: url::Url,
61  // Was the scheme added automatically?
62  auto_scheme: bool,
63}
64
65impl fmt::Debug for PrUrl {
66  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67    f.debug_struct(type_name::<Self>())
68      .field("full", &self.full.as_str())
69      .field("auto_scheme", &self.auto_scheme)
70      .finish()
71  }
72}
73
74impl PrUrl {
75  pub fn as_url(&self) -> &url::Url {
76    &self.full
77  }
78
79  pub fn as_prurl(&self) -> &str {
80    let url_str = self.full.as_str();
81    let scheme = self.full.scheme();
82    let after_scheme = &url_str[scheme.len()..];
83    if after_scheme.starts_with("://") {
84      &after_scheme[1..]
85    } else {
86      after_scheme.strip_prefix(':').unwrap_or(after_scheme)
87    }
88  }
89
90  pub fn parse(input: &str) -> Result<Self, url::ParseError> {
91    if input.starts_with("//") {
92      url::Url::parse(&format!("https:{input}")).map(|full| Self {
93        full,
94        auto_scheme: true,
95      })
96    } else {
97      url::Url::parse(input).map(|full| Self {
98        full,
99        auto_scheme: false,
100      })
101    }
102  }
103}
104
105#[cfg(feature = "serde")]
106impl serde::Serialize for PrUrl {
107  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108  where
109    S: serde::Serializer,
110  {
111    self.full.serialize(serializer)
112  }
113}
114
115#[cfg(feature = "serde")]
116impl<'de> serde::Deserialize<'de> for PrUrl {
117  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118  where
119    D: serde::Deserializer<'de>,
120  {
121    use serde::de::Error;
122    let input = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
123    Self::parse(input.as_ref()).map_err(|e| D::Error::custom(format!("{}", crate::types::DisplayErrorChain(&e))))
124  }
125}
126
127declare_new_enum!(
128  pub enum LocaleId {
129    #[str("de-DE")]
130    DeDe,
131    #[str("en-US")]
132    EnUs,
133    #[str("eo")]
134    Eo,
135    #[str("es-SP")]
136    EsSp,
137    #[str("fr-FR")]
138    FrFr,
139    #[str("it-IT")]
140    ItIt,
141    #[str("pt-PT")]
142    PtPt,
143  }
144  pub type ParseError = LocaleIdParseError;
145  const SQL_NAME = "locale_id";
146);
147
148/// Year, month and date (no timezone attached)
149#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
150pub struct Date(chrono::NaiveDate);
151
152#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Error)]
153#[error("invalid date ymd components: year = {year}, month = {month}, day = {day}")]
154pub struct InvalidDateYmd {
155  pub year: i32,
156  pub month: u32,
157  pub day: u32,
158}
159
160impl Date {
161  pub fn into_chrono(self) -> chrono::NaiveDate {
162    self.0
163  }
164
165  /// Create a new date from its ymd components.
166  ///
167  /// Returns an error if the components do not represent a valid date.
168  ///
169  /// ```
170  /// use eternaltwin_core::core::Date;
171  ///
172  /// let d = Date::ymd(2020, 1, 1).unwrap();
173  /// assert_eq!(&d.to_string(), "2020-01-01");
174  /// ```
175  pub fn ymd(year: i32, month: u32, day: u32) -> Result<Self, InvalidDateYmd> {
176    chrono::NaiveDate::from_ymd_opt(year, month, day)
177      .map(Self)
178      .ok_or(InvalidDateYmd { year, month, day })
179  }
180}
181
182impl fmt::Display for Date {
183  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184    const DATE_FORMAT: &str = "%F";
185    write!(f, "{}", self.0.format(DATE_FORMAT))
186  }
187}
188
189#[cfg(feature = "serde")]
190impl serde::Serialize for Date {
191  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
192  where
193    S: serde::Serializer,
194  {
195    self.to_string().serialize(serializer)
196  }
197}
198
199#[cfg(feature = "serde")]
200impl<'de> serde::Deserialize<'de> for Date {
201  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202  where
203    D: serde::Deserializer<'de>,
204  {
205    let inner = chrono::NaiveDate::deserialize(deserializer)?;
206    Ok(Self(inner))
207  }
208}
209
210/// A point in time, with a millisecond precision.
211#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
212pub struct Instant(DateTime<Utc>);
213
214const NANOSECOND_PER_MILLISECOND: u32 = 1_000_000;
215const MILLISECOND_PER_SECOND: u32 = 1_000;
216
217impl Instant {
218  /// Round down nanosecond precision Chrono DateTime to millisecond precision Instant.
219  pub fn new_round_down(inner: DateTime<Utc>) -> Self {
220    let nanos = inner.nanosecond();
221    Self(
222      inner
223        .with_nanosecond(nanos - (nanos % NANOSECOND_PER_MILLISECOND))
224        .expect("invalid time"),
225    )
226  }
227
228  // TODO: Return result (error on invalid data)
229  // TODO: Avoid deprecated functions in implementation
230  pub fn ymd_hms(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Self {
231    #[allow(deprecated)]
232    Self(Utc.ymd(year, month, day).and_hms(hour, min, sec))
233  }
234
235  // TODO: Return result (error on invalid data)
236  // TODO: Avoid deprecated functions in implementation
237  pub fn ymd_hms_milli(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32, milli: u32) -> Self {
238    #[allow(deprecated)]
239    Self(Utc.ymd(year, month, day).and_hms_milli(hour, min, sec, milli))
240  }
241
242  /// Create an Instant from the number of POSIX seconds since 1970-01-01T00:00:00Z.
243  ///
244  /// Note: POSIX seconds are defined as 1/86400 of a day (they ignore leap seconds).
245  ///
246  /// TODO: Rename to UNIX epoch timestamp / unix timestamp
247  pub fn from_posix_timestamp(secs: i64) -> Self {
248    Self(Utc.timestamp_opt(secs, 0).single().expect("invalid timestamp"))
249  }
250
251  pub fn into_chrono(self) -> DateTime<Utc> {
252    self.0
253  }
254
255  pub const fn from_chrono(t: DateTime<Utc>) -> Self {
256    Self(t)
257  }
258
259  /// Converts to an [`std::time::Instant`]
260  pub fn into_std(self) -> ::std::time::Instant {
261    let chrono_now = Utc::now();
262    let chrono_duration = self.0.signed_duration_since(chrono_now);
263    let (is_neg, std_duration) = if chrono_duration < ::chrono::Duration::zero() {
264      (true, (-chrono_duration).to_std().expect("duration out of range"))
265    } else {
266      (false, chrono_duration.to_std().expect("duration out of range"))
267    };
268    let std_now = ::std::time::Instant::now();
269    if is_neg {
270      std_now - std_duration
271    } else {
272      std_now + std_duration
273    }
274  }
275
276  /// Converts to an [`std::time::SystemTime`]
277  pub fn into_system(self) -> ::std::time::SystemTime {
278    let (secs, nanos) = (self.0.timestamp(), self.0.timestamp_subsec_nanos());
279    let (is_neg, secs) = if secs < 0 {
280      (true, secs.checked_neg().expect("impossible to turn `secs` positive"))
281    } else {
282      (false, secs)
283    };
284    debug_assert!(secs >= 0);
285    let secs = u64::try_from(secs).expect("failed to convert positive i64 to u64");
286    let std_since_epoch = std::time::Duration::new(secs, nanos);
287    if is_neg {
288      std::time::SystemTime::UNIX_EPOCH - std_since_epoch
289    } else {
290      std::time::SystemTime::UNIX_EPOCH + std_since_epoch
291    }
292  }
293
294  pub fn from_system(t: ::std::time::SystemTime) -> Self {
295    Self::from_chrono(DateTime::<Utc>::from(t))
296  }
297
298  pub const fn into_posix_timestamp(self) -> i64 {
299    self.0.timestamp()
300  }
301
302  pub fn duration_since(&self, earlier: Instant) -> Duration {
303    Duration(self.0.signed_duration_since(earlier.0))
304  }
305
306  pub const fn const_cmp(self, other: Self) -> Ordering {
307    let left: i64 = self.into_posix_timestamp();
308    let right: i64 = other.into_posix_timestamp();
309    if left == right {
310      Ordering::Equal
311    } else if left < right {
312      Ordering::Less
313    } else {
314      Ordering::Greater
315    }
316  }
317}
318
319impl fmt::Display for Instant {
320  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321    fmt::Display::fmt(&self.0, f)
322  }
323}
324
325#[cfg(feature = "serde")]
326impl serde::Serialize for Instant {
327  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
328  where
329    S: serde::Serializer,
330  {
331    const INSTANT_FORMAT: &str = "%FT%T%.3fZ";
332
333    self.0.format(INSTANT_FORMAT).to_string().serialize(serializer)
334  }
335}
336
337#[cfg(feature = "serde")]
338impl<'de> serde::Deserialize<'de> for Instant {
339  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
340  where
341    D: serde::Deserializer<'de>,
342  {
343    let inner = DateTime::<Utc>::deserialize(deserializer)?;
344    Ok(Self::new_round_down(inner))
345  }
346}
347
348#[cfg(feature = "sqlx-postgres")]
349impl sqlx::Type<Postgres> for Instant {
350  fn type_info() -> postgres::PgTypeInfo {
351    postgres::PgTypeInfo::with_name("instant")
352  }
353
354  fn compatible(ty: &postgres::PgTypeInfo) -> bool {
355    *ty == <Self as ::sqlx::Type<Postgres>>::type_info()
356      || *ty == <DateTime<Utc> as ::sqlx::Type<Postgres>>::type_info()
357  }
358}
359
360#[cfg(feature = "sqlx-sqlite")]
361impl ::sqlx::Type<sqlx::Sqlite> for Instant {
362  fn type_info() -> ::sqlx::sqlite::SqliteTypeInfo {
363    <DateTime<Utc> as ::sqlx::Type<::sqlx::Sqlite>>::type_info()
364  }
365
366  fn compatible(ty: &::sqlx::sqlite::SqliteTypeInfo) -> bool {
367    <DateTime<Utc> as ::sqlx::Type<::sqlx::Sqlite>>::compatible(ty)
368  }
369}
370
371#[cfg(feature = "sqlx")]
372impl<'r, Db: ::sqlx::Database> ::sqlx::Decode<'r, Db> for Instant
373where
374  DateTime<Utc>: ::sqlx::Decode<'r, Db>,
375{
376  fn decode(value: <Db as ::sqlx::database::HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
377    let value: DateTime<Utc> = <DateTime<Utc> as ::sqlx::Decode<Db>>::decode(value)?;
378    Ok(Self::new_round_down(value))
379  }
380}
381
382#[cfg(feature = "sqlx-postgres")]
383impl sqlx::Encode<'_, Postgres> for Instant {
384  fn encode_by_ref(&self, buf: &mut postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
385    let v: DateTime<Utc> = self.into_chrono();
386    <DateTime<Utc> as sqlx::Encode<'_, Postgres>>::encode(v, buf)
387  }
388}
389
390#[cfg(feature = "sqlx-sqlite")]
391impl ::sqlx::Encode<'_, ::sqlx::Sqlite> for Instant {
392  fn encode_by_ref(&self, args: &mut Vec<::sqlx::sqlite::SqliteArgumentValue<'_>>) -> ::sqlx::encode::IsNull {
393    <DateTime<Utc> as sqlx::Encode<'_, ::sqlx::Sqlite>>::encode(self.0, args)
394  }
395}
396
397impl ops::Add<Duration> for Instant {
398  type Output = Instant;
399
400  fn add(self, rhs: Duration) -> Self::Output {
401    Self::new_round_down(self.into_chrono() + rhs.0)
402  }
403}
404
405impl ops::Sub<Duration> for Instant {
406  type Output = Instant;
407
408  fn sub(self, rhs: Duration) -> Self::Output {
409    Self::new_round_down(self.into_chrono() - rhs.0)
410  }
411}
412
413impl ops::Sub<Instant> for Instant {
414  type Output = Duration;
415
416  fn sub(self, rhs: Instant) -> Self::Output {
417    Duration::from_chrono(self.into_chrono().signed_duration_since(rhs.into_chrono()))
418  }
419}
420
421/// A difference between two `Instant`
422#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
423pub struct Duration(::chrono::Duration);
424
425impl Duration {
426  #[inline(always)]
427  pub const fn into_std(self) -> std::time::Duration {
428    match self.0.to_std() {
429      Ok(d) => d,
430      Err(_) => panic!("invalid `Durattion` value"),
431    }
432  }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
436#[error("standard Duration does not fit in Eternaltwin duration range: {0:?}")]
437pub struct StdDurationRangeError(pub std::time::Duration);
438
439impl Duration {
440  pub const ZERO: Self = Self(::chrono::Duration::zero());
441
442  pub fn from_std(d: std::time::Duration) -> Result<Self, StdDurationRangeError> {
443    ::chrono::Duration::from_std(d)
444      .map(Self)
445      .map_err(|_| StdDurationRangeError(d))
446  }
447
448  pub const fn from_chrono(d: ::chrono::Duration) -> Self {
449    Self(d)
450  }
451
452  pub const fn from_days(days: i64) -> Self {
453    Self::from_seconds(days * 86400)
454  }
455
456  pub const fn from_minutes(minutes: i64) -> Self {
457    Self::from_seconds(minutes * 60)
458  }
459
460  pub const fn from_seconds(seconds: i64) -> Self {
461    Self::from_millis(seconds * 1000)
462  }
463
464  pub const fn from_hours(hours: i64) -> Self {
465    Self::from_seconds(hours * 3600)
466  }
467
468  pub const fn from_millis(millis: i64) -> Self {
469    Self(match ::chrono::Duration::try_milliseconds(millis) {
470      Some(d) => d,
471      None => panic!("Duration::from_millis out of bounds"),
472    })
473  }
474
475  pub const fn seconds(&self) -> i64 {
476    self.0.num_seconds()
477  }
478
479  pub const fn milliseconds(&self) -> i64 {
480    self.0.num_milliseconds()
481  }
482
483  pub const fn nanoseconds(&self) -> i128 {
484    // todo: avoid `as` casts once `i128::from` is supported in const context
485    (self.0.num_seconds() as i128) * 1_000_000_000i128 + (self.0.subsec_nanos() as i128)
486  }
487
488  pub fn div_floor(&self, rhs: Self) -> i64 {
489    f64::floor(self.seconds_f64() / rhs.seconds_f64()) as i64
490  }
491
492  // TODO: Avoid using `num_seconds` as it does not support leap seconds.
493  pub fn seconds_f64(&self) -> f64 {
494    let secs = self.0.num_seconds() as f64;
495    let nanos = self.0.subsec_nanos() as f64;
496    secs + (nanos / 1e9)
497  }
498
499  pub fn from_seconds_f64_lossy(seconds: f64) -> Self {
500    let millis = (seconds * (MILLISECOND_PER_SECOND as f64)) as i64;
501    Self::from_millis(millis)
502  }
503}
504
505#[cfg(feature = "serde")]
506impl serde::Serialize for Duration {
507  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
508  where
509    S: serde::Serializer,
510  {
511    use serde::ser::Error;
512    let duration = self.0;
513    let millis = duration.num_milliseconds();
514    let seconds = (millis as f64) / (MILLISECOND_PER_SECOND as f64);
515    if ((seconds * (MILLISECOND_PER_SECOND as f64)) as i64) != millis {
516      return Err(S::Error::custom("lossy duration serialization"));
517    }
518    serializer.serialize_f64(seconds)
519  }
520}
521
522#[cfg(feature = "serde")]
523impl<'de> serde::Deserialize<'de> for Duration {
524  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
525  where
526    D: serde::Deserializer<'de>,
527  {
528    use crate::types::DisplayErrorChain;
529    use serde::de::Error;
530
531    #[derive(serde::Deserialize)]
532    #[serde(untagged)]
533    enum StrOrNum<'s> {
534      Str(std::borrow::Cow<'s, str>),
535      Num(f64),
536    }
537
538    match StrOrNum::deserialize(deserializer)? {
539      StrOrNum::Str(s) => {
540        let std_duration = humantime::parse_duration(s.as_ref())
541          .map_err(|e| D::Error::custom(format!("invalid duration format: {}", DisplayErrorChain(&e))))?;
542        Self::from_std(std_duration)
543          .map_err(|e| D::Error::custom(format!("invalid duration: {}", DisplayErrorChain(&e))))
544      }
545      StrOrNum::Num(n) => Ok(Self::from_seconds_f64_lossy(n)),
546    }
547  }
548}
549
550impl ops::Div<i32> for Duration {
551  type Output = Self;
552
553  fn div(self, rhs: i32) -> Self::Output {
554    Self(self.0 / rhs)
555  }
556}
557
558impl ops::Mul<i32> for Duration {
559  type Output = Self;
560
561  fn mul(self, rhs: i32) -> Self::Output {
562    Self(self.0 * rhs)
563  }
564}
565
566/// Private type used to serialize PeriodLower and its variants.
567#[cfg(feature = "serde")]
568#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
569struct SerializablePeriodLower {
570  pub start: Instant,
571  pub end: Option<Instant>,
572}
573
574#[cfg(feature = "serde")]
575impl From<PeriodFrom> for SerializablePeriodLower {
576  fn from(period: PeriodFrom) -> Self {
577    Self {
578      start: period.start,
579      end: None,
580    }
581  }
582}
583
584#[cfg(feature = "serde")]
585impl From<FinitePeriod> for SerializablePeriodLower {
586  fn from(period: FinitePeriod) -> Self {
587    Self {
588      start: period.start,
589      end: Some(period.end),
590    }
591  }
592}
593
594#[cfg(feature = "serde")]
595impl From<PeriodLower> for SerializablePeriodLower {
596  fn from(period: PeriodLower) -> Self {
597    match period {
598      PeriodLower::From(p) => p.into(),
599      PeriodLower::Finite(p) => p.into(),
600    }
601  }
602}
603
604#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
605#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
606pub struct PeriodFrom {
607  pub start: Instant,
608}
609
610#[cfg(feature = "serde")]
611impl serde::Serialize for PeriodFrom {
612  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
613  where
614    S: serde::Serializer,
615  {
616    SerializablePeriodLower::from(*self).serialize(serializer)
617  }
618}
619
620impl From<RangeFrom<Instant>> for PeriodFrom {
621  fn from(r: RangeFrom<Instant>) -> Self {
622    Self { start: r.start }
623  }
624}
625
626impl From<PeriodFrom> for RangeFrom<Instant> {
627  fn from(r: PeriodFrom) -> Self {
628    r.start..
629  }
630}
631
632/// Represents the finite time period between `start` and `end`.
633///
634/// More specifically, it represents instants `t` such that `start <= t < end`.
635///
636/// Note that `end` may be lesser than or equal to `start`, in such case there
637/// is no valid instant contained by the period.
638#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
639#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
640pub struct FinitePeriod {
641  pub start: Instant,
642  pub end: Instant,
643}
644
645impl FinitePeriod {
646  pub const fn new(start: Instant, end: Instant) -> Self {
647    Self { start, end }
648  }
649
650  /// Check if this period contains the instant `time`.
651  pub const fn contains(self, time: Instant) -> bool {
652    matches!(self.start.const_cmp(time), Ordering::Less | Ordering::Equal)
653      && matches!(time.const_cmp(self.end), Ordering::Less)
654  }
655}
656
657#[cfg(feature = "serde")]
658impl serde::Serialize for FinitePeriod {
659  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
660  where
661    S: serde::Serializer,
662  {
663    SerializablePeriodLower::from(*self).serialize(serializer)
664  }
665}
666
667impl From<Range<Instant>> for FinitePeriod {
668  fn from(r: Range<Instant>) -> Self {
669    Self {
670      start: r.start,
671      end: r.end,
672    }
673  }
674}
675
676impl From<FinitePeriod> for Range<Instant> {
677  fn from(r: FinitePeriod) -> Self {
678    r.start..r.end
679  }
680}
681
682#[cfg(feature = "sqlx-postgres")]
683impl sqlx::Type<Postgres> for FinitePeriod {
684  fn type_info() -> postgres::PgTypeInfo {
685    PeriodLower::type_info()
686  }
687
688  fn compatible(ty: &postgres::PgTypeInfo) -> bool {
689    PeriodLower::compatible(ty)
690  }
691}
692
693#[cfg(feature = "sqlx-postgres")]
694#[derive(Debug, Error)]
695#[error("decoded invalid Postgres finite PERIOD_LOWER as {:?}", .0)]
696struct InvalidFinitePeriod(PgRange<Instant>);
697
698#[cfg(feature = "sqlx-postgres")]
699impl<'r> sqlx::Decode<'r, Postgres> for FinitePeriod {
700  fn decode(value: postgres::PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
701    let range = PgRange::<Instant>::decode(value)?;
702    match (range.start, range.end) {
703      (std::ops::Bound::Included(start), std::ops::Bound::Excluded(end)) => Ok(FinitePeriod { start, end }),
704      _ => Err(Box::new(InvalidFinitePeriod(range))),
705    }
706  }
707}
708
709#[cfg(feature = "sqlx-postgres")]
710impl sqlx::Encode<'_, Postgres> for FinitePeriod {
711  fn encode_by_ref(&self, buf: &mut postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
712    let range: PgRange<Instant> = (self.start..self.end).into();
713    range.encode_by_ref(buf)
714  }
715}
716
717/// Represents the raw `PERIOD` Postgres type. It should not be used directly.
718#[cfg(feature = "sqlx-postgres")]
719struct PgPeriod;
720
721#[cfg(feature = "sqlx-postgres")]
722impl sqlx::Type<Postgres> for PgPeriod {
723  fn type_info() -> postgres::PgTypeInfo {
724    postgres::PgTypeInfo::with_name("period")
725  }
726
727  fn compatible(ty: &postgres::PgTypeInfo) -> bool {
728    *ty == Self::type_info()
729  }
730}
731
732/// Represents any period with a lower bound
733#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))]
734#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
735pub enum PeriodLower {
736  Finite(FinitePeriod),
737  From(PeriodFrom),
738}
739
740impl PeriodLower {
741  pub const fn new(start: Instant, end: Option<Instant>) -> Self {
742    match end {
743      Some(end) => Self::bounded(start, end),
744      None => Self::unbounded(start),
745    }
746  }
747
748  pub const fn unbounded(start: Instant) -> Self {
749    Self::From(PeriodFrom { start })
750  }
751
752  pub const fn bounded(start: Instant, end: Instant) -> Self {
753    Self::Finite(FinitePeriod { start, end })
754  }
755
756  pub const fn start(self) -> Instant {
757    match self {
758      Self::Finite(p) => p.start,
759      Self::From(p) => p.start,
760    }
761  }
762
763  pub const fn end(self) -> Option<Instant> {
764    match self {
765      Self::Finite(p) => Some(p.end),
766      Self::From(_) => None,
767    }
768  }
769
770  /// Updates the end instant to be the minimum of the current end and provided value
771  pub fn end_min(self, other_end: Option<Instant>) -> Self {
772    if let Some(end) = other_end {
773      Self::Finite(self.bounded_end_min(end))
774    } else {
775      self
776    }
777  }
778
779  /// Updates the end instant to be the minimum of the current end and provided value
780  pub fn bounded_end_min(self, other_end: Instant) -> FinitePeriod {
781    match self {
782      Self::From(PeriodFrom { start }) => FinitePeriod { start, end: other_end },
783      Self::Finite(FinitePeriod { start, end }) => FinitePeriod {
784        start,
785        end: Instant::min(end, other_end),
786      },
787    }
788  }
789}
790
791#[cfg(feature = "serde")]
792impl serde::Serialize for PeriodLower {
793  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
794  where
795    S: serde::Serializer,
796  {
797    SerializablePeriodLower::from(*self).serialize(serializer)
798  }
799}
800
801#[cfg(feature = "sqlx-postgres")]
802impl sqlx::Type<Postgres> for PeriodLower {
803  fn type_info() -> postgres::PgTypeInfo {
804    postgres::PgTypeInfo::with_name("period_lower")
805  }
806
807  fn compatible(ty: &postgres::PgTypeInfo) -> bool {
808    *ty == Self::type_info() || *ty == PgPeriod::type_info() || *ty == (PgRange::<DateTime<Utc>>::type_info())
809  }
810}
811
812#[cfg(feature = "sqlx-postgres")]
813#[derive(Debug, Error)]
814#[error("decoded invalid Postgres PERIOD_LOWER as {:?}", .0)]
815struct InvalidPeriodLower(PgRange<Instant>);
816
817#[cfg(feature = "sqlx-postgres")]
818impl<'r> sqlx::Decode<'r, Postgres> for PeriodLower {
819  fn decode(value: postgres::PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
820    let range = PgRange::<Instant>::decode(value)?;
821    match (range.start, range.end) {
822      (std::ops::Bound::Included(start), std::ops::Bound::Unbounded) => Ok(PeriodLower::From(PeriodFrom { start })),
823      (std::ops::Bound::Included(start), std::ops::Bound::Excluded(end)) => {
824        Ok(PeriodLower::Finite(FinitePeriod { start, end }))
825      }
826      _ => Err(Box::new(InvalidPeriodLower(range))),
827    }
828  }
829}
830
831#[cfg(feature = "sqlx-postgres")]
832impl sqlx::Encode<'_, Postgres> for PeriodLower {
833  fn encode_by_ref(&self, buf: &mut postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
834    let range: PgRange<Instant> = match *self {
835      Self::Finite(FinitePeriod { start, end }) => (start..end).into(),
836      Self::From(PeriodFrom { start }) => (start..).into(),
837    };
838    range.encode_by_ref(buf)
839  }
840}
841
842#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
843#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
844pub struct UserDot {
845  pub time: Instant,
846  pub user: ShortUser,
847}
848
849/// Revision at two points in time: now and then
850#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
851#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
852pub struct BiRev<T> {
853  pub now: T,
854  pub then: T,
855}
856
857/// Wrapper for a value (allows future extensions without breakage)
858#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
859#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
860pub struct Value<T> {
861  pub value: T,
862}
863
864pub type BiValue<T> = BiRev<Value<T>>;
865
866#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
867#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
868pub struct RawUserDot {
869  pub time: Instant,
870  pub user: UserIdRef,
871}
872
873/// Wrapper around a string that should be kept secret
874#[derive(Clone)]
875pub struct SecretString(Box<str>);
876
877impl SecretString {
878  pub fn new(str: String) -> Self {
879    Self(str.into_boxed_str())
880  }
881
882  pub fn as_str(&self) -> &str {
883    &self.0
884  }
885}
886
887/// Wrapper around a byte buffer that should be kept secret
888#[derive(Clone)]
889pub struct SecretBytes(Box<[u8]>);
890
891impl SecretBytes {
892  pub fn new(bytes: Vec<u8>) -> Self {
893    Self(bytes.into_boxed_slice())
894  }
895
896  pub fn as_slice(&self) -> &[u8] {
897    &self.0
898  }
899}
900
901declare_new_int! {
902  /// A percentage value between 0 and 100 (inclusive) supporting only integer
903  /// values.
904  pub struct IntPercentage(u8);
905  pub type RangeError = IntPercentageRangeError;
906  const BOUNDS = 0..=100;
907  type SqlType = PgU8;
908  const SQL_NAME = "int_percentage";
909}
910
911#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
912#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
913pub struct Listing<T> {
914  pub offset: u32,
915  pub limit: u32,
916  pub count: u32,
917  pub items: Vec<T>,
918}
919
920impl<T> Listing<T> {
921  pub fn map<B, F>(self, f: F) -> Listing<B>
922  where
923    F: FnMut(T) -> B,
924  {
925    Listing {
926      offset: self.offset,
927      limit: self.limit,
928      count: self.count,
929      items: self.items.into_iter().map(f).collect(),
930    }
931  }
932}
933
934#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
935#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
936pub struct ListingCount {
937  pub count: u32,
938}
939
940pub trait Request<Brand = ()> {
941  type Response;
942}
943
944#[async_trait]
945pub trait Handler<Req, Brand = ()>
946where
947  Req: Request<Brand>,
948{
949  async fn handle(&self, req: Req) -> Req::Response;
950}
951
952#[cfg(test)]
953mod test {
954  #[cfg(feature = "serde")]
955  use crate::core::{FinitePeriod, Instant, PeriodFrom, PeriodLower};
956  #[cfg(feature = "serde")]
957  use std::fs;
958
959  #[cfg(feature = "serde")]
960  #[allow(clippy::unnecessary_wraps)]
961  fn get_finite_period_one_millisecond() -> FinitePeriod {
962    FinitePeriod {
963      start: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
964      end: Instant::ymd_hms_milli(2021, 1, 1, 0, 0, 0, 1),
965    }
966  }
967
968  #[cfg(feature = "serde")]
969  #[test]
970  fn read_finite_period_one_millisecond() {
971    let s = fs::read_to_string("../../test-resources/core/core/finite-period/one-millisecond/value.json").unwrap();
972    let actual: FinitePeriod = serde_json::from_str(&s).unwrap();
973    let expected = get_finite_period_one_millisecond();
974    assert_eq!(actual, expected);
975  }
976
977  #[cfg(feature = "serde")]
978  #[test]
979  fn write_finite_period_one_millisecond() {
980    let value = get_finite_period_one_millisecond();
981    let actual: String = serde_json::to_string_pretty(&value).unwrap();
982    let expected =
983      fs::read_to_string("../../test-resources/core/core/finite-period/one-millisecond/value.json").unwrap();
984    assert_eq!(&actual, expected.trim());
985  }
986
987  #[cfg(feature = "serde")]
988  #[allow(clippy::unnecessary_wraps)]
989  fn get_finite_period_one_second() -> FinitePeriod {
990    FinitePeriod {
991      start: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
992      end: Instant::ymd_hms(2021, 1, 1, 0, 0, 1),
993    }
994  }
995
996  #[cfg(feature = "serde")]
997  #[test]
998  fn read_finite_period_one_second() {
999    let s = fs::read_to_string("../../test-resources/core/core/finite-period/one-second/value.json").unwrap();
1000    let actual: FinitePeriod = serde_json::from_str(&s).unwrap();
1001    let expected = get_finite_period_one_second();
1002    assert_eq!(actual, expected);
1003  }
1004
1005  #[cfg(feature = "serde")]
1006  #[test]
1007  fn write_finite_period_one_second() {
1008    let value = get_finite_period_one_second();
1009    let actual: String = serde_json::to_string_pretty(&value).unwrap();
1010    let expected = fs::read_to_string("../../test-resources/core/core/finite-period/one-second/value.json").unwrap();
1011    assert_eq!(&actual, expected.trim());
1012  }
1013
1014  #[cfg(feature = "serde")]
1015  #[allow(clippy::unnecessary_wraps)]
1016  fn get_period_from_unbounded() -> PeriodFrom {
1017    PeriodFrom {
1018      start: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
1019    }
1020  }
1021
1022  #[cfg(feature = "serde")]
1023  #[test]
1024  fn read_period_from_unbounded() {
1025    let s = fs::read_to_string("../../test-resources/core/core/period-from/unbounded/value.json").unwrap();
1026    let actual: PeriodFrom = serde_json::from_str(&s).unwrap();
1027    let expected = get_period_from_unbounded();
1028    assert_eq!(actual, expected);
1029  }
1030
1031  #[cfg(feature = "serde")]
1032  #[test]
1033  fn write_period_from_unbounded() {
1034    let value = get_period_from_unbounded();
1035    let actual: String = serde_json::to_string_pretty(&value).unwrap();
1036    let expected = fs::read_to_string("../../test-resources/core/core/period-from/unbounded/value.json").unwrap();
1037    assert_eq!(&actual, expected.trim());
1038  }
1039
1040  #[cfg(feature = "serde")]
1041  #[allow(clippy::unnecessary_wraps)]
1042  fn get_period_lower_one_millisecond() -> PeriodLower {
1043    PeriodLower::Finite(FinitePeriod {
1044      start: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
1045      end: Instant::ymd_hms_milli(2021, 1, 1, 0, 0, 0, 1),
1046    })
1047  }
1048
1049  #[cfg(feature = "serde")]
1050  #[test]
1051  fn read_period_lower_one_millisecond() {
1052    let s = fs::read_to_string("../../test-resources/core/core/period-lower/one-millisecond/value.json").unwrap();
1053    let actual: PeriodLower = serde_json::from_str(&s).unwrap();
1054    let expected = get_period_lower_one_millisecond();
1055    assert_eq!(actual, expected);
1056  }
1057
1058  #[cfg(feature = "serde")]
1059  #[test]
1060  fn write_period_lower_one_millisecond() {
1061    let value = get_period_lower_one_millisecond();
1062    let actual: String = serde_json::to_string_pretty(&value).unwrap();
1063    let expected =
1064      fs::read_to_string("../../test-resources/core/core/period-lower/one-millisecond/value.json").unwrap();
1065    assert_eq!(&actual, expected.trim());
1066  }
1067
1068  #[cfg(feature = "serde")]
1069  #[allow(clippy::unnecessary_wraps)]
1070  fn get_period_lower_one_second() -> PeriodLower {
1071    PeriodLower::Finite(FinitePeriod {
1072      start: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
1073      end: Instant::ymd_hms(2021, 1, 1, 0, 0, 1),
1074    })
1075  }
1076
1077  #[cfg(feature = "serde")]
1078  #[test]
1079  fn read_period_lower_one_second() {
1080    let s = fs::read_to_string("../../test-resources/core/core/period-lower/one-second/value.json").unwrap();
1081    let actual: PeriodLower = serde_json::from_str(&s).unwrap();
1082    let expected = get_period_lower_one_second();
1083    assert_eq!(actual, expected);
1084  }
1085
1086  #[cfg(feature = "serde")]
1087  #[test]
1088  fn write_period_lower_one_second() {
1089    let value = get_period_lower_one_second();
1090    let actual: String = serde_json::to_string_pretty(&value).unwrap();
1091    let expected = fs::read_to_string("../../test-resources/core/core/period-lower/one-second/value.json").unwrap();
1092    assert_eq!(&actual, expected.trim());
1093  }
1094
1095  #[cfg(feature = "serde")]
1096  #[allow(clippy::unnecessary_wraps)]
1097  fn get_period_lower_unbounded() -> PeriodLower {
1098    PeriodLower::unbounded(Instant::ymd_hms(2021, 1, 1, 0, 0, 0))
1099  }
1100
1101  #[cfg(feature = "serde")]
1102  #[test]
1103  fn read_period_lower_unbounded() {
1104    let s = fs::read_to_string("../../test-resources/core/core/period-lower/unbounded/value.json").unwrap();
1105    let actual: PeriodLower = serde_json::from_str(&s).unwrap();
1106    let expected = get_period_lower_unbounded();
1107    assert_eq!(actual, expected);
1108  }
1109
1110  #[cfg(feature = "serde")]
1111  #[test]
1112  fn write_period_lower_unbounded() {
1113    let value = get_period_lower_unbounded();
1114    let actual: String = serde_json::to_string_pretty(&value).unwrap();
1115    let expected = fs::read_to_string("../../test-resources/core/core/period-lower/unbounded/value.json").unwrap();
1116    assert_eq!(&actual, expected.trim());
1117  }
1118}