Skip to main content

postrust_graphql/input/
order.rs

1//! Order and pagination input types for GraphQL queries.
2//!
3//! Provides order by direction and pagination types for limiting and offsetting results.
4
5use postrust_core::api_request::{
6    Field, OrderDirection as CoreOrderDirection, OrderNulls, OrderTerm,
7};
8use serde::{Deserialize, Serialize};
9
10/// Sort direction for ordering.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12pub enum OrderDirection {
13    /// Ascending order (smallest first)
14    #[default]
15    Asc,
16    /// Descending order (largest first)
17    Desc,
18}
19
20impl From<OrderDirection> for CoreOrderDirection {
21    fn from(dir: OrderDirection) -> Self {
22        match dir {
23            OrderDirection::Asc => CoreOrderDirection::Asc,
24            OrderDirection::Desc => CoreOrderDirection::Desc,
25        }
26    }
27}
28
29/// Null ordering preference.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum NullsOrder {
32    /// Nulls first
33    First,
34    /// Nulls last
35    Last,
36}
37
38impl From<NullsOrder> for OrderNulls {
39    fn from(nulls: NullsOrder) -> Self {
40        match nulls {
41            NullsOrder::First => OrderNulls::First,
42            NullsOrder::Last => OrderNulls::Last,
43        }
44    }
45}
46
47/// An order by field specification.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct OrderByField {
50    /// Field name to order by
51    pub field: String,
52    /// Direction of the sort
53    pub direction: OrderDirection,
54    /// Where to place nulls
55    pub nulls: Option<NullsOrder>,
56}
57
58impl OrderByField {
59    /// Create a new ascending order by field.
60    pub fn asc(field: impl Into<String>) -> Self {
61        Self {
62            field: field.into(),
63            direction: OrderDirection::Asc,
64            nulls: None,
65        }
66    }
67
68    /// Create a new descending order by field.
69    pub fn desc(field: impl Into<String>) -> Self {
70        Self {
71            field: field.into(),
72            direction: OrderDirection::Desc,
73            nulls: None,
74        }
75    }
76
77    /// Set nulls ordering.
78    pub fn with_nulls(mut self, nulls: NullsOrder) -> Self {
79        self.nulls = Some(nulls);
80        self
81    }
82
83    /// Convert to an OrderTerm.
84    pub fn to_order_term(&self) -> OrderTerm {
85        OrderTerm::Field {
86            field: Field::simple(&self.field),
87            direction: Some(self.direction.into()),
88            nulls: self.nulls.map(|n| n.into()),
89        }
90    }
91}
92
93/// Pagination input for limiting and offsetting results.
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct PaginationInput {
96    /// Maximum number of results to return
97    pub limit: Option<i64>,
98    /// Number of results to skip
99    pub offset: Option<i64>,
100}
101
102impl PaginationInput {
103    /// Create pagination with a limit.
104    pub fn new(limit: Option<i64>, offset: Option<i64>) -> Self {
105        Self { limit, offset }
106    }
107
108    /// Create pagination with just a limit.
109    pub fn with_limit(limit: i64) -> Self {
110        Self {
111            limit: Some(limit),
112            offset: None,
113        }
114    }
115
116    /// Create pagination with limit and offset.
117    pub fn with_offset(limit: i64, offset: i64) -> Self {
118        Self {
119            limit: Some(limit),
120            offset: Some(offset),
121        }
122    }
123
124    /// Check if pagination is set.
125    pub fn is_empty(&self) -> bool {
126        self.limit.is_none() && self.offset.is_none()
127    }
128
129    /// Get the offset or 0 if not set.
130    pub fn offset_or_default(&self) -> i64 {
131        self.offset.unwrap_or(0)
132    }
133}
134
135/// Combined order and pagination for a query.
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137pub struct OrderAndPagination {
138    /// Fields to order by
139    pub order_by: Vec<OrderByField>,
140    /// Pagination
141    pub pagination: PaginationInput,
142}
143
144impl OrderAndPagination {
145    /// Create new order and pagination settings.
146    pub fn new(order_by: Vec<OrderByField>, pagination: PaginationInput) -> Self {
147        Self {
148            order_by,
149            pagination,
150        }
151    }
152
153    /// Convert order_by fields to OrderTerms.
154    pub fn to_order_terms(&self) -> Vec<OrderTerm> {
155        self.order_by.iter().map(|f| f.to_order_term()).collect()
156    }
157}
158
159/// Helper to parse GraphQL order enum values like "id_ASC", "name_DESC".
160pub fn parse_order_enum(value: &str) -> Option<OrderByField> {
161    // Split by last underscore to handle field names with underscores
162    if let Some(pos) = value.rfind('_') {
163        let (field, direction) = value.split_at(pos);
164        let direction = &direction[1..]; // Skip the underscore
165
166        let dir = match direction {
167            "ASC" => OrderDirection::Asc,
168            "DESC" => OrderDirection::Desc,
169            _ => return None,
170        };
171
172        Some(OrderByField {
173            field: field.to_string(),
174            direction: dir,
175            nulls: None,
176        })
177    } else {
178        None
179    }
180}
181
182/// Generate order enum value from field and direction.
183pub fn make_order_enum(field: &str, direction: OrderDirection) -> String {
184    let dir_str = match direction {
185        OrderDirection::Asc => "ASC",
186        OrderDirection::Desc => "DESC",
187    };
188    format!("{}_{}", field, dir_str)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use pretty_assertions::assert_eq;
195
196    // ============================================================================
197    // OrderDirection Tests
198    // ============================================================================
199
200    #[test]
201    fn test_order_direction_default() {
202        let dir = OrderDirection::default();
203        assert_eq!(dir, OrderDirection::Asc);
204    }
205
206    #[test]
207    fn test_order_direction_to_core() {
208        let asc: CoreOrderDirection = OrderDirection::Asc.into();
209        assert!(matches!(asc, CoreOrderDirection::Asc));
210
211        let desc: CoreOrderDirection = OrderDirection::Desc.into();
212        assert!(matches!(desc, CoreOrderDirection::Desc));
213    }
214
215    // ============================================================================
216    // NullsOrder Tests
217    // ============================================================================
218
219    #[test]
220    fn test_nulls_order_to_core() {
221        let first: OrderNulls = NullsOrder::First.into();
222        assert!(matches!(first, OrderNulls::First));
223
224        let last: OrderNulls = NullsOrder::Last.into();
225        assert!(matches!(last, OrderNulls::Last));
226    }
227
228    // ============================================================================
229    // OrderByField Tests
230    // ============================================================================
231
232    #[test]
233    fn test_order_by_field_asc() {
234        let field = OrderByField::asc("name");
235        assert_eq!(field.field, "name");
236        assert_eq!(field.direction, OrderDirection::Asc);
237        assert!(field.nulls.is_none());
238    }
239
240    #[test]
241    fn test_order_by_field_desc() {
242        let field = OrderByField::desc("created_at");
243        assert_eq!(field.field, "created_at");
244        assert_eq!(field.direction, OrderDirection::Desc);
245    }
246
247    #[test]
248    fn test_order_by_field_with_nulls() {
249        let field = OrderByField::desc("name").with_nulls(NullsOrder::Last);
250        assert_eq!(field.nulls, Some(NullsOrder::Last));
251    }
252
253    #[test]
254    fn test_order_by_field_to_order_term() {
255        let field = OrderByField::desc("name").with_nulls(NullsOrder::First);
256        let term = field.to_order_term();
257
258        match term {
259            OrderTerm::Field {
260                field,
261                direction,
262                nulls,
263            } => {
264                assert_eq!(field.name, "name");
265                assert!(matches!(direction, Some(CoreOrderDirection::Desc)));
266                assert!(matches!(nulls, Some(OrderNulls::First)));
267            }
268            _ => panic!("Expected Field order term"),
269        }
270    }
271
272    // ============================================================================
273    // PaginationInput Tests
274    // ============================================================================
275
276    #[test]
277    fn test_pagination_default() {
278        let pagination = PaginationInput::default();
279        assert!(pagination.limit.is_none());
280        assert!(pagination.offset.is_none());
281        assert!(pagination.is_empty());
282    }
283
284    #[test]
285    fn test_pagination_with_limit() {
286        let pagination = PaginationInput::with_limit(10);
287        assert_eq!(pagination.limit, Some(10));
288        assert!(pagination.offset.is_none());
289        assert!(!pagination.is_empty());
290    }
291
292    #[test]
293    fn test_pagination_with_offset() {
294        let pagination = PaginationInput::with_offset(10, 20);
295        assert_eq!(pagination.limit, Some(10));
296        assert_eq!(pagination.offset, Some(20));
297        assert!(!pagination.is_empty());
298    }
299
300    #[test]
301    fn test_pagination_offset_or_default() {
302        let pagination = PaginationInput::default();
303        assert_eq!(pagination.offset_or_default(), 0);
304
305        let pagination = PaginationInput::with_offset(10, 5);
306        assert_eq!(pagination.offset_or_default(), 5);
307    }
308
309    // ============================================================================
310    // OrderAndPagination Tests
311    // ============================================================================
312
313    #[test]
314    fn test_order_and_pagination_default() {
315        let oap = OrderAndPagination::default();
316        assert!(oap.order_by.is_empty());
317        assert!(oap.pagination.is_empty());
318    }
319
320    #[test]
321    fn test_order_and_pagination_new() {
322        let oap = OrderAndPagination::new(
323            vec![OrderByField::desc("created_at")],
324            PaginationInput::with_limit(10),
325        );
326
327        assert_eq!(oap.order_by.len(), 1);
328        assert_eq!(oap.pagination.limit, Some(10));
329    }
330
331    #[test]
332    fn test_order_and_pagination_to_order_terms() {
333        let oap = OrderAndPagination::new(
334            vec![OrderByField::desc("created_at"), OrderByField::asc("name")],
335            PaginationInput::default(),
336        );
337
338        let terms = oap.to_order_terms();
339        assert_eq!(terms.len(), 2);
340    }
341
342    // ============================================================================
343    // Order Enum Parsing Tests
344    // ============================================================================
345
346    #[test]
347    fn test_parse_order_enum_asc() {
348        let field = parse_order_enum("name_ASC").unwrap();
349        assert_eq!(field.field, "name");
350        assert_eq!(field.direction, OrderDirection::Asc);
351    }
352
353    #[test]
354    fn test_parse_order_enum_desc() {
355        let field = parse_order_enum("created_at_DESC").unwrap();
356        assert_eq!(field.field, "created_at");
357        assert_eq!(field.direction, OrderDirection::Desc);
358    }
359
360    #[test]
361    fn test_parse_order_enum_underscore_field() {
362        let field = parse_order_enum("created_at_ASC").unwrap();
363        assert_eq!(field.field, "created_at");
364        assert_eq!(field.direction, OrderDirection::Asc);
365    }
366
367    #[test]
368    fn test_parse_order_enum_invalid() {
369        assert!(parse_order_enum("name").is_none());
370        assert!(parse_order_enum("name_INVALID").is_none());
371    }
372
373    #[test]
374    fn test_make_order_enum() {
375        assert_eq!(make_order_enum("id", OrderDirection::Asc), "id_ASC");
376        assert_eq!(make_order_enum("name", OrderDirection::Desc), "name_DESC");
377        assert_eq!(
378            make_order_enum("created_at", OrderDirection::Asc),
379            "created_at_ASC"
380        );
381    }
382
383    #[test]
384    fn test_order_enum_roundtrip() {
385        let original = OrderByField::desc("user_id");
386        let enum_value = make_order_enum(&original.field, original.direction);
387        let parsed = parse_order_enum(&enum_value).unwrap();
388
389        assert_eq!(parsed.field, original.field);
390        assert_eq!(parsed.direction, original.direction);
391    }
392}