shared/
domain.rs

1//! Types that travel over the wire.
2
3macro_rules! into_i16_index {
4    ( $( $t:ty ),+ $(,)? ) => {
5        $(
6            impl From<$t> for i16 {
7                fn from(t: $t) -> Self {
8                    t.0
9                }
10            }
11
12            /// Needed to cast i16 into i64 range for algolia indexing
13            impl From<$t> for i64 {
14                fn from(t: $t) -> Self {
15                    t.0 as i64
16                }
17            }
18        )+
19    };
20}
21
22/// Helper macro to generate a Newtype that wraps a [uuid::Uuid], derives relevant macros
23/// and sets it up to be stored in the database.
24///
25/// Example:
26///
27/// ```
28/// wrap_uuid! {
29///   /// Represents a My ID
30///   #[serde(rename_all = "camelCase")]
31///   pub struct MyId
32/// }
33/// ```
34macro_rules! wrap_uuid {
35    (
36        $(#[$outer:meta])*
37        $vis:vis struct $t:ident
38    ) => {
39        #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, PathPart, Hash)]
40        $(#[$outer])*
41        #[cfg_attr(feature = "backend", derive(sqlx::Type))]
42        #[cfg_attr(feature = "backend", sqlx(transparent))]
43        $vis struct $t(pub uuid::Uuid);
44
45        impl From<$t> for uuid::Uuid {
46            fn from(t: $t) -> Self {
47                t.0
48            }
49        }
50
51        impl std::fmt::Display for $t {
52            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53                write!(f, "{}", self.0)
54            }
55        }
56
57        impl std::str::FromStr for $t {
58            type Err = uuid::Error;
59
60            #[inline]
61            fn from_str(value: &str) -> Result<Self, Self::Err> {
62                Ok(Self(uuid::Uuid::from_str(value)?))
63            }
64        }
65
66        impl $t {
67            /// Creates a wrapped UUID from a 128 bit value
68            #[inline]
69            #[must_use]
70            $vis const fn from_u128(v: u128) -> Self {
71                Self(uuid::Uuid::from_u128(v))
72            }
73        }
74    }
75}
76
77pub mod additional_resource;
78pub mod admin;
79pub mod animation;
80pub mod asset;
81pub mod audio;
82pub mod billing;
83pub mod category;
84pub mod circle;
85pub mod course;
86pub mod image;
87pub mod jig;
88pub mod locale;
89pub mod media;
90pub mod meta;
91pub mod module;
92pub mod pdf;
93pub mod playlist;
94pub mod resource;
95pub mod search;
96pub mod ser;
97pub mod session;
98pub mod user;
99
100#[deprecated]
101/// auth types (deprecated)
102pub mod auth {
103
104    #[deprecated]
105    pub use super::session::AUTH_COOKIE_NAME;
106
107    #[deprecated]
108    pub use super::session::CSRF_HEADER_NAME;
109
110    #[deprecated]
111    pub use super::user::CreateProfileRequest as RegisterRequest;
112}
113
114use chrono::Utc;
115use ser::{csv_encode_i16_indices, csv_encode_uuids, deserialize_optional_field, from_csv, to_csv};
116use serde::{Deserialize, Serialize};
117use std::fmt::{Display, Formatter};
118use uuid::Uuid;
119
120/// Serialize/Deserialize wrapper for Base64 encoded content.
121#[derive(Debug)]
122pub struct Base64<T>(pub T);
123
124impl<T: Display> serde::Serialize for Base64<T> {
125    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126    where
127        S: serde::Serializer,
128    {
129        serializer.serialize_str(&base64::encode(&self.0.to_string()))
130    }
131}
132
133impl<'de, E: std::fmt::Debug, T: std::str::FromStr<Err = E>> serde::Deserialize<'de> for Base64<T> {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: serde::Deserializer<'de>,
137    {
138        Ok(Self(deserializer.deserialize_str(ser::FromStrVisitor(
139            std::marker::PhantomData,
140        ))?))
141    }
142}
143/// Response for successfuly creating a Resource.
144#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
145pub struct CreateResponse<T: Into<Uuid>> {
146    /// The newly created resource's ID.
147    pub id: T,
148}
149
150/// Represents when to publish an image.
151#[derive(Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, Debug)]
152pub enum Publish {
153    /// Publish the image *at* the given time.
154    At(chrono::DateTime<Utc>),
155    /// Publish the image *in* the given amount of time from now.
156    In(std::time::Duration),
157}
158
159impl Publish {
160    /// creates an instance of `Self` that will publish "right now"
161    #[must_use]
162    #[allow(clippy::missing_const_for_fn)]
163    pub fn now() -> Self {
164        // Duration::new is const unstable
165        Self::In(std::time::Duration::new(0, 0))
166    }
167}
168
169impl From<Publish> for chrono::DateTime<Utc> {
170    fn from(publish: Publish) -> Self {
171        match publish {
172            Publish::At(t) => t,
173            Publish::In(d) => {
174                // todo: error instead of panicking
175                Utc::now() + chrono::Duration::from_std(d).expect("Really really big duration?")
176            }
177        }
178    }
179}
180
181/// Clearer representation of an optional nullable field.
182///
183/// Requires `#[serde(default, skip_serializing_if = "Update::is_keep")]` to be applied to
184/// fields which use this type.
185#[derive(Clone, Debug, Serialize, Default)]
186#[serde(untagged)]
187pub enum UpdateNullable<T> {
188    /// Use the current value stored in the database. Equivalent of `undefined` in JS.
189    #[default]
190    Keep,
191    /// Set the value to `null` or the equivalent.
192    Unset,
193    /// Use the given value.
194    Change(T),
195}
196
197impl<T> From<Option<T>> for UpdateNullable<T> {
198    fn from(value: Option<T>) -> Self {
199        match value {
200            Some(value) => Self::Change(value),
201            None => Self::Unset,
202        }
203    }
204}
205
206impl<'de, T> Deserialize<'de> for UpdateNullable<T>
207where
208    T: Deserialize<'de>,
209{
210    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211    where
212        D: serde::Deserializer<'de>,
213    {
214        #[derive(Debug, Deserialize)]
215        #[serde(untagged)]
216        enum UpdateMap<T> {
217            Unset,
218            Change(T),
219        }
220
221        let mapping = UpdateMap::deserialize(deserializer)?;
222
223        let update = match mapping {
224            UpdateMap::Unset => Self::Unset,
225            UpdateMap::Change(val) => Self::Change(val),
226        };
227
228        Ok(update)
229    }
230}
231
232impl<T> UpdateNullable<T> {
233    /// Whether this is the `Keep` variant
234    pub const fn is_keep(&self) -> bool {
235        matches!(self, Self::Keep)
236    }
237
238    /// Whether this is the `Keep` variant
239    pub const fn is_unset(&self) -> bool {
240        matches!(self, Self::Unset)
241    }
242
243    /// Whether this is the `Change` variant
244    pub const fn is_change(&self) -> bool {
245        matches!(self, Self::Change(_))
246    }
247
248    /// Similar to `Option<Option<T>>::flatten()`, this converts the variant into an `Option<T>`.
249    ///
250    /// Useful for coalesce updates.
251    pub fn into_option(self) -> Option<T> {
252        match self {
253            Self::Keep | Self::Unset => None,
254            Self::Change(v) => Some(v),
255        }
256    }
257}
258
259/// Clearer representation of an optional non-nullable field.
260///
261/// Requires `#[serde(default, skip_serializing_if = "Update::is_keep")]` to be applied to
262/// fields which use this type.
263#[derive(Clone, Debug, Serialize, Default)]
264#[serde(untagged)]
265pub enum UpdateNonNullable<T> {
266    /// Use the current value stored in the database. Equivalent of `undefined` in JS.
267    #[default]
268    Keep,
269    /// Use the given value.
270    Change(T),
271}
272
273impl<'de, T> Deserialize<'de> for UpdateNonNullable<T>
274where
275    T: Deserialize<'de>,
276{
277    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278    where
279        D: serde::Deserializer<'de>,
280    {
281        Ok(Self::Change(T::deserialize(deserializer)?))
282    }
283}
284
285impl<T> UpdateNonNullable<T> {
286    /// Whether this is the `Keep` variant
287    pub const fn is_keep(&self) -> bool {
288        matches!(self, Self::Keep)
289    }
290
291    /// Whether this is the `Change` variant
292    pub const fn is_change(&self) -> bool {
293        matches!(self, Self::Change(_))
294    }
295
296    /// Similar to `Option<Option<T>>::flatten()`, this converts the variant into an `Option<T>`.
297    ///
298    /// Useful for coalesce updates.
299    pub fn into_option(self) -> Option<T> {
300        match self {
301            Self::Keep => None,
302            Self::Change(v) => Some(v),
303        }
304    }
305}
306
307impl<T> From<Option<T>> for UpdateNonNullable<T> {
308    fn from(value: Option<T>) -> Self {
309        value.map(UpdateNonNullable::Change).unwrap_or_default()
310    }
311}
312
313/// New-type representing the current page of a list of items
314#[derive(Copy, Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
315pub struct Page(usize);
316
317impl From<usize> for Page {
318    fn from(value: usize) -> Self {
319        Self(value)
320    }
321}
322
323impl From<Page> for usize {
324    fn from(value: Page) -> Self {
325        value.0
326    }
327}
328
329#[cfg(feature = "backend")]
330impl From<Page> for i64 {
331    fn from(value: Page) -> Self {
332        value.0 as i64
333    }
334}
335
336impl Display for Page {
337    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
338        write!(f, "{}", self.0)
339    }
340}
341
342impl Page {
343    /// Get an instance of the next page
344    pub fn next_page(self) -> Self {
345        Self(self.0.saturating_add(1))
346    }
347
348    /// Get an instance of the previous page
349    pub fn prev_page(self) -> Self {
350        Self(self.0.saturating_sub(1))
351    }
352}
353
354const DEFAULT_PAGE_LIMIT: usize = 20;
355
356/// New-type representing the item limit for a page of items
357#[derive(Serialize, Deserialize, Copy, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
358pub struct PageLimit(usize);
359
360impl Default for PageLimit {
361    fn default() -> Self {
362        Self(DEFAULT_PAGE_LIMIT)
363    }
364}
365
366impl From<usize> for PageLimit {
367    fn from(value: usize) -> Self {
368        Self(value)
369    }
370}
371
372impl From<PageLimit> for usize {
373    fn from(value: PageLimit) -> Self {
374        value.0
375    }
376}
377
378#[cfg(feature = "backend")]
379impl From<PageLimit> for i64 {
380    fn from(value: PageLimit) -> Self {
381        value.0 as i64
382    }
383}
384
385impl PageLimit {
386    /// Calculate the offset of items given the current page
387    #[cfg(feature = "backend")]
388    pub fn offset(&self, page: Page) -> i64 {
389        (self.0 * page.0) as i64
390    }
391}
392
393/// New-type representing the total count of items
394#[derive(Serialize, Deserialize, Copy, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
395pub struct ItemCount(usize);
396
397impl From<usize> for ItemCount {
398    fn from(value: usize) -> Self {
399        Self(value)
400    }
401}
402
403impl From<ItemCount> for usize {
404    fn from(value: ItemCount) -> Self {
405        value.0
406    }
407}
408
409#[cfg(feature = "backend")]
410impl From<ItemCount> for i64 {
411    fn from(value: ItemCount) -> Self {
412        value.0 as i64
413    }
414}
415
416impl ItemCount {
417    /// Calculate the page count for a list of items
418    pub fn paged(&self, limit: PageLimit) -> Self {
419        // let pages = (total_count / (page_limit as u64)
420        //     + (total_count % (page_limit as u64) != 0) as u64) as u32;
421        let page_count = self.0 / limit.0 + (self.0 % limit.0 != 0) as usize;
422        page_count.into()
423    }
424}
425
426/// Representation of a percentage
427#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
428pub struct Percent(f64);
429
430impl Display for Percent {
431    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
432        write!(f, "{}%", self.0 * 100.0)
433    }
434}
435
436impl From<f64> for Percent {
437    fn from(value: f64) -> Self {
438        Self(value)
439    }
440}
441
442impl From<Percent> for f64 {
443    fn from(value: Percent) -> Self {
444        value.0
445    }
446}
447
448#[cfg(feature = "backend")]
449impl From<sqlx::types::BigDecimal> for Percent {
450    fn from(value: sqlx::types::BigDecimal) -> Self {
451        use bigdecimal::ToPrimitive;
452        Self(value.to_f64().unwrap_or_default())
453    }
454}
455
456#[cfg(feature = "backend")]
457impl From<Percent> for sqlx::types::BigDecimal {
458    fn from(value: Percent) -> Self {
459        Self::try_from(value.0).ok().unwrap_or_default()
460    }
461}
462
463// use actix_web::{
464//     http::{header::IntoHeaderPair, StatusCode},
465//     HttpRequest, HttpResponse,
466// };
467
468// FIXME
469// #[cfg(feature = "backend")]
470// impl actix_web::Responder for CreateResponse<T: Into<Uuid>> {
471//     type Future = futures::ready::Ready<HttpResponse>;
472//
473//     fn respond_to(self, _: &HttpRequest) -> Self::Future {
474//         ready(Ok(HttpResponse::build(StatusCode::NO_CONTENT)
475//             .content_type("application/json")
476//             .finish()))
477//     }
478// }