opentalk_types_common/pagination/
page.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use snafu::{Snafu, ensure};
6
7use crate::utils::ExampleData;
8
9const DEFAULT_VALUE: i64 = 1;
10const FIRST_VALUE: i64 = 1;
11const ONE_VALUE: i64 = 1;
12const MIN_VALUE: i64 = 1;
13const MAX_VALUE: i64 = i64::MAX;
14
15/// Error when parsing a [`Page`].
16#[derive(Debug, Snafu)]
17pub enum TryFromPageError {
18    /// Page number is outside of the allowed range.
19    #[snafu(display("Page number is outside the allowed range ({min}..={max})"))]
20    OutOfRange {
21        /// The minimum page number
22        min: i64,
23        /// The maximum page number
24        max: i64,
25    },
26}
27
28/// A page number used for paging in requests.
29///
30/// This is `1`-based, so a `Page` with a value of `0` can not be created.
31#[derive(
32    Debug,
33    Clone,
34    Copy,
35    PartialEq,
36    Eq,
37    PartialOrd,
38    Ord,
39    Hash,
40    derive_more::Display,
41    derive_more::AsRef,
42    derive_more::Into,
43)]
44#[cfg_attr(
45    feature = "diesel",
46    derive(
47        opentalk_diesel_newtype::DieselNewtype,
48        diesel::expression::AsExpression,
49        diesel::deserialize::FromSqlRow
50    )
51)]
52#[cfg_attr(feature="diesel", diesel(sql_type = diesel::sql_types::BigInt))]
53#[cfg_attr(
54    feature = "serde",
55    derive(serde::Serialize, serde::Deserialize),
56    serde(try_from = "i64")
57)]
58#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(example = json!(Page::example_data())))]
59pub struct Page(i64);
60
61impl Page {
62    /// The minimum value of the type.
63    pub const MIN: Self = Self(MIN_VALUE);
64
65    /// The maximum value of the type.
66    pub const MAX: Self = Self(MAX_VALUE);
67
68    /// The default value of the type.
69    pub const DEFAULT: Self = Self(DEFAULT_VALUE);
70
71    /// The first page.
72    pub const FIRST: Self = Self(FIRST_VALUE);
73
74    /// The page with number 1.
75    pub const ONE: Self = Self(ONE_VALUE);
76
77    /// Create a new [`Page`] from an i64 value, clamped to the valid range.
78    pub const fn from_i64_clamped(value: i64) -> Self {
79        let value = if value < MIN_VALUE { MIN_VALUE } else { value };
80        Self(value)
81    }
82
83    /// Add two [`Page`] values, saturating.
84    pub const fn saturating_add(self, rhs: Self) -> Self {
85        Self::from_i64_clamped(self.0.saturating_add(rhs.0))
86    }
87
88    /// Subtract a [`Page`] value from another, saturating.
89    pub const fn saturating_sub(self, rhs: Self) -> Self {
90        Self::from_i64_clamped(self.0.saturating_sub(rhs.0))
91    }
92
93    /// Get the next page number. Will return itself if this is already the highest possible value.
94    pub fn saturating_next(&self) -> Self {
95        self.saturating_add(Self::ONE)
96    }
97
98    /// Get the previous page number. Will return itself if this is already the lowest possible value.
99    pub fn saturating_previous(&self) -> Self {
100        self.saturating_sub(Self::ONE)
101    }
102
103    /// Get the zero-based index value as i64.
104    pub fn as_zero_based_i64(&self) -> i64 {
105        self.0.saturating_sub(1).clamp(0, i64::MAX)
106    }
107
108    /// Get the zero-based index value as usize.
109    pub fn as_zero_based_usize(&self) -> usize {
110        self.0.saturating_sub(1) as usize
111    }
112}
113
114impl Default for Page {
115    fn default() -> Self {
116        Self::DEFAULT
117    }
118}
119
120impl TryFrom<u64> for Page {
121    type Error = TryFromPageError;
122
123    fn try_from(value: u64) -> Result<Self, Self::Error> {
124        let value = i64::try_from(value).map_err(|_e| TryFromPageError::OutOfRange {
125            min: MIN_VALUE,
126            max: MAX_VALUE,
127        })?;
128        Ok(Self(value))
129    }
130}
131
132impl TryFrom<i32> for Page {
133    type Error = TryFromPageError;
134
135    fn try_from(value: i32) -> Result<Self, Self::Error> {
136        Self::try_from(i64::from(value))
137    }
138}
139
140impl TryFrom<usize> for Page {
141    type Error = TryFromPageError;
142
143    fn try_from(value: usize) -> Result<Self, Self::Error> {
144        let value = i64::try_from(value).map_err(|_e| TryFromPageError::OutOfRange {
145            min: MIN_VALUE,
146            max: MAX_VALUE,
147        })?;
148        Ok(Self(value))
149    }
150}
151
152impl TryFrom<i64> for Page {
153    type Error = TryFromPageError;
154
155    fn try_from(value: i64) -> Result<Self, Self::Error> {
156        ensure!(
157            (MIN_VALUE..=MAX_VALUE).contains(&value),
158            OutOfRangeSnafu {
159                min: MIN_VALUE,
160                max: MAX_VALUE
161            }
162        );
163        Ok(Self(value))
164    }
165}
166
167impl From<Page> for usize {
168    fn from(Page(value): Page) -> Self {
169        usize::try_from(value).unwrap()
170    }
171}
172
173impl ExampleData for Page {
174    fn example_data() -> Self {
175        Self(5)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use pretty_assertions::{assert_eq, assert_matches};
182
183    use super::{MAX_VALUE, MIN_VALUE, Page, TryFromPageError};
184
185    #[test]
186    fn saturating_add() {
187        assert_eq!(
188            Page::try_from(5423)
189                .unwrap()
190                .saturating_add(Page::try_from(3).unwrap()),
191            Page::try_from(5426).unwrap()
192        );
193        assert_eq!(
194            Page::MAX.saturating_add(Page::try_from(3).unwrap()),
195            Page::MAX
196        );
197    }
198
199    #[test]
200    fn try_from() {
201        assert_eq!(Page::try_from(1usize).unwrap(), Page::FIRST);
202        assert_eq!(Page::try_from(1i32).unwrap(), Page::FIRST);
203        assert_eq!(Page::try_from(1i64).unwrap(), Page::FIRST);
204        assert_eq!(Page::try_from(1u64).unwrap(), Page::FIRST);
205
206        assert_eq!(Page::try_from(14usize).unwrap(), Page(14));
207        assert_eq!(Page::try_from(15i32).unwrap(), Page(15));
208        assert_eq!(Page::try_from(16i64).unwrap(), Page(16));
209        assert_eq!(Page::try_from(18u64).unwrap(), Page(18));
210
211        assert_matches!(
212            Page::try_from(-42i64),
213            Err(TryFromPageError::OutOfRange {
214                min: MIN_VALUE,
215                max: MAX_VALUE
216            })
217        );
218        assert_matches!(
219            Page::try_from((i64::MAX as usize) + 1),
220            Err(TryFromPageError::OutOfRange {
221                min: MIN_VALUE,
222                max: MAX_VALUE
223            })
224        );
225        assert_matches!(
226            Page::try_from((i64::MAX as u64) + 1),
227            Err(TryFromPageError::OutOfRange {
228                min: MIN_VALUE,
229                max: MAX_VALUE
230            })
231        );
232    }
233}
234
235#[cfg(all(test, feature = "serde"))]
236mod serde_tests {
237    use pretty_assertions::assert_eq;
238    use serde_json::json;
239
240    use super::Page;
241
242    #[test]
243    fn serialize_default() {
244        let example = Page::default();
245        assert_eq!(json!(example), json!(1));
246    }
247
248    #[test]
249    fn serialize() {
250        let example = Page::try_from(423).unwrap();
251        assert_eq!(json!(example), json!(423));
252    }
253
254    #[test]
255    fn deserialize_invalid_zero() {
256        assert!(serde_json::from_value::<Page>(json!(0)).is_err());
257    }
258
259    #[test]
260    fn deserialize_default() {
261        let example = Page::default();
262        assert_eq!(example, serde_json::from_value(json!(1)).unwrap());
263    }
264
265    #[test]
266    fn deserialize() {
267        let example = Page::try_from(64).unwrap();
268        assert_eq!(example, serde_json::from_value(json!(64)).unwrap());
269    }
270}