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