Skip to main content

sqlx_data_params/
builder.rs

1//! High-level, type-safe SQL parameter builder with
2//! support for filtering, sorting and pagination
3//! across PostgreSQL, MySQL and SQLite.
4
5use crate::filter::Filter;
6use crate::sort::Sort;
7use crate::{
8    CursorDirection, CursorParams, FilterOperator, FilterParams, FilterValue,
9    IntoParams, SearchParams, SortingParams, params::Params,
10};
11use std::marker::PhantomData;
12
13//
14// ========================
15//   PARAMS ROOT BUILDER
16// ========================
17//
18#[derive(Debug)]
19pub struct ParamsBuilder {
20    params: Params,
21}
22
23impl Default for ParamsBuilder {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl ParamsBuilder {
30    pub fn new() -> Self {
31        Self {
32            params: Params::default(),
33        }
34    }
35
36    //
37    // -------------- SORT BUILDER --------------
38    //
39    pub fn sort(self) -> SortBuilder<Self, Safe> {
40        SortBuilder::with_parent(self)
41    }
42
43    //
44    // -------------- FILTER BUILDER --------------
45    //
46    pub fn filter(self) -> FilterBuilder<Self> {
47        FilterBuilder::with_parent(self)
48    }
49
50    //
51    // -------------- SEARCH BUILDER --------------
52    //
53    pub fn search(self) -> SearchBuilder<Self> {
54        SearchBuilder::with_parent(self)
55    }
56
57    //
58    // -------------- SERIAL BUILDER --------------
59    //
60    pub fn serial(self) -> SerialBuilder<Self> {
61        SerialBuilder::with_parent(self)
62    }
63
64    //
65    // -------------- SLICE BUILDER --------------
66    //
67    pub fn slice(self) -> SliceBuilder<Self> {
68        SliceBuilder::with_parent(self)
69    }
70
71    //
72    // -------------- CURSOR BUILDER --------------
73    //
74    pub fn cursor(self) -> CursorBuilder<Self, Initial> {
75        CursorBuilder::with_parent(self)
76    }
77
78    //
79    // -------------- LIMIT/OFFSET --------------
80    //
81    pub fn limit(mut self, limit: u32) -> Self {
82        self.params.limit = Some(crate::pagination::LimitParam(limit));
83        self
84    }
85
86    pub fn offset(mut self, offset: u32) -> Self {
87        self.params.offset = Some(crate::pagination::OffsetParam(offset));
88        self
89    }
90
91    //
92    // -------------- FINAL BUILD --------------
93    //
94    pub fn build(self) -> Params {
95        self.params
96    }
97}
98
99//
100// ========================
101//       SORT BUILDER
102// ========================
103//
104// Security states for typestate pattern
105pub struct Safe;
106pub struct Unsafe;
107
108pub struct SortBuilder<P = (), S = Safe> {
109    parent: Option<P>,
110    sorts: SortingParams,
111    allowed_columns: Option<&'static [&'static str]>,
112    _security: PhantomData<S>,
113}
114
115impl Default for SortBuilder<(), Safe> {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl<S> std::fmt::Debug for SortBuilder<(), S> {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("SortBuilder")
124            .field("sorts", &self.sorts)
125            .field("allowed_columns", &self.allowed_columns)
126            .finish()
127    }
128}
129
130impl SortBuilder<(), Safe> {
131    /// Create as standalone (no parent) - Safe by default
132    pub fn new() -> Self {
133        Self {
134            parent: None,
135            sorts: SortingParams::new(),
136            allowed_columns: None,
137            _security: PhantomData,
138        }
139    }
140
141    /// Finish and return only the sorting params
142    pub fn build(self) -> Option<SortingParams> {
143        if self.sorts.is_empty() {
144            None
145        } else {
146            Some(self.sorts)
147        }
148    }
149}
150
151// Safe implementation - compile-time safety with &'static str
152impl<P> SortBuilder<P, Safe> {
153    fn with_parent(parent: P) -> Self {
154        Self {
155            parent: Some(parent),
156            sorts: SortingParams::new(),
157            allowed_columns: None,
158            _security: PhantomData,
159        }
160    }
161
162    /// Safe ASC ordering - only accepts static string literals (compile-time safe)
163    pub fn asc(mut self, field: &'static str) -> Self {
164        self.sorts = self.sorts.asc(field);
165        self
166    }
167
168    /// Safe DESC ordering - only accepts static string literals (compile-time safe)
169    pub fn desc(mut self, field: &'static str) -> Self {
170        self.sorts = self.sorts.desc(field);
171        self
172    }
173
174    /// Switch to unsafe mode with column whitelist for dynamic strings
175    pub fn with_allowed_columns(self, allowed_columns: &'static [&'static str]) -> SortBuilder<P, Unsafe> {
176        SortBuilder {
177            parent: self.parent,
178            sorts: self.sorts.with_allowed_columns(allowed_columns),
179            allowed_columns: Some(allowed_columns),
180            _security: PhantomData,
181        }
182    }
183}
184
185// Unsafe implementation - runtime validation against whitelist
186impl SortBuilder<(), Unsafe> {
187    /// Finish and return only the sorting params
188    pub fn build(self) -> Option<SortingParams> {
189        if self.sorts.is_empty() {
190            None
191        } else {
192            Some(self.sorts)
193        }
194    }
195}
196
197impl<P> SortBuilder<P, Unsafe> {
198    /// Unsafe ASC ordering - accepts dynamic strings (validation deferred to runtime)
199    pub fn asc_unsafe(mut self, field: impl Into<String>) -> Self {
200        let sort = Sort::asc_unsafe(field.into());
201        self.sorts = self.sorts.push(sort);
202        self
203    }
204
205    /// Unsafe DESC ordering - accepts dynamic strings (validation deferred to runtime)
206    pub fn desc_unsafe(mut self, field: impl Into<String>) -> Self {
207        let sort = Sort::desc_unsafe(field.into());
208        self.sorts = self.sorts.push(sort);
209        self
210    }
211
212}
213
214// Generic methods available for both Safe and Unsafe modes
215impl<P, S> SortBuilder<P, S> {
216    /// Apply NULLS FIRST to the last added sort
217    pub fn nulls_first(mut self) -> Self {
218        self.sorts = self.sorts.apply_nulls_first();
219        self
220    }
221
222    /// Apply NULLS LAST to the last added sort
223    pub fn nulls_last(mut self) -> Self {
224        self.sorts = self.sorts.apply_nulls_last();
225        self
226    }
227
228    /// Apply default null ordering to the last added sort
229    pub fn nulls_default(mut self) -> Self {
230        self.sorts = self.sorts.apply_nulls_default();
231        self
232    }
233
234    /// Finish and return to parent
235    pub fn done(self) -> P
236    where
237        P: HasParams,
238    {
239        #[allow(clippy::expect_used)]
240        let mut parent = self
241            .parent
242            .expect("SortBuilder::done called without a parent (programming error)");
243
244        if !self.sorts.is_empty() {
245            if let Some(existing_sorts) = parent.params_mut().sort_by.take() {
246                parent.params_mut().sort_by = Some(existing_sorts.extend_with(self.sorts));
247            } else {
248                parent.params_mut().sort_by = Some(self.sorts);
249            }
250        }
251        parent
252    }
253}
254
255//
256// ========================
257//      FILTER BUILDER
258// ========================
259//
260
261pub struct FilterBuilder<P = ()> {
262    parent: Option<P>,
263    filters: FilterParams,
264}
265
266impl Default for FilterBuilder<()> {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl std::fmt::Debug for FilterBuilder<()> {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.debug_struct("FilterBuilder")
275            .field("filters", &self.filters)
276            .finish()
277    }
278}
279
280impl FilterBuilder<()> {
281    /// Create as standalone (no parent)
282    pub fn new() -> Self {
283        Self {
284            parent: None,
285            filters: FilterParams::default(),
286        }
287    }
288
289    /// Finish and return only the filters
290    pub fn build(self) -> FilterParams {
291        self.filters
292    }
293}
294
295impl<P> FilterBuilder<P> {
296    /// Create with parent
297    pub fn with_parent(parent: P) -> Self {
298        Self {
299            parent: Some(parent),
300            filters: FilterParams::default(),
301        }
302    }
303
304    // --- PRIMITIVES ---
305
306    fn push(
307        mut self,
308        field: impl Into<String>,
309        op: FilterOperator,
310        value: impl Into<FilterValue>,
311    ) -> Self {
312        self.filters.filters.push(Filter::new(field, op, value));
313        self
314    }
315
316    pub fn eq(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
317        self.push(field, FilterOperator::Eq, value)
318    }
319
320    pub fn ne(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
321        self.push(field, FilterOperator::Ne, value)
322    }
323
324    pub fn gt(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
325        self.push(field, FilterOperator::Gt, value)
326    }
327
328    pub fn lt(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
329        self.push(field, FilterOperator::Lt, value)
330    }
331
332    pub fn gte(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
333        self.push(field, FilterOperator::Gte, value)
334    }
335
336    pub fn lte(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
337        self.push(field, FilterOperator::Lte, value)
338    }
339
340    /// Safe LIKE operator - automatically escapes special characters (% and _).
341    /// Use this for user input to prevent wildcard injection.
342    pub fn like(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
343        self.push(field, FilterOperator::Like, pat.into())
344    }
345
346    /// Case-insensitive LIKE operator - automatically escapes special characters.
347    /// Safe for user input.
348    pub fn ilike(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
349        self.push(field, FilterOperator::ILike, pat.into())
350    }
351
352    /// Unsafe LIKE operator - allows intentional wildcards (% and _).
353    /// WARNING: Only use with controlled input, never with direct user input.
354    /// Use this when you intentionally want wildcard behavior.
355    pub fn like_pattern(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
356        self.push(field, FilterOperator::UnsafeLike, pat.into())
357    }
358
359    pub fn r#in(
360        self,
361        field: impl Into<String>,
362        values: impl IntoIterator<Item = impl Into<FilterValue>>,
363    ) -> Self {
364        let array = FilterValue::Array(values.into_iter().map(Into::into).collect());
365        self.push(field, FilterOperator::In, array)
366    }
367
368    /// Alias for r#in
369    pub fn in_values(
370        self,
371        field: impl Into<String>,
372        values: impl IntoIterator<Item = impl Into<FilterValue>>,
373    ) -> Self {
374        self.r#in(field, values)
375    }
376
377    pub fn not_in(
378        self,
379        field: impl Into<String>,
380        values: impl IntoIterator<Item = impl Into<FilterValue>>,
381    ) -> Self {
382        self.push(
383            field,
384            FilterOperator::NotIn,
385            FilterValue::Array(values.into_iter().map(Into::into).collect()),
386        )
387    }
388
389    pub fn between(
390        self,
391        field: impl Into<String>,
392        min: impl Into<FilterValue>,
393        max: impl Into<FilterValue>,
394    ) -> Self {
395        self.push(
396            field,
397            FilterOperator::Between,
398            FilterValue::Array(vec![min.into(), max.into()]),
399        )
400    }
401
402    pub fn is_null(self, field: impl Into<String>) -> Self {
403        self.push(field, FilterOperator::IsNull, FilterValue::Null)
404    }
405
406    pub fn is_not_null(self, field: impl Into<String>) -> Self {
407        self.push(field, FilterOperator::IsNotNull, FilterValue::Null)
408    }
409
410    pub fn contains(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
411        self.push(field, FilterOperator::Contains, value)
412    }
413
414    /// Negates the last applied filter operation.
415    /// Supports: Like, LikePattern, ILike, In, Between.
416    /// Usage: .like("name", "pattern").not() // Creates NOT LIKE
417    #[allow(clippy::should_implement_trait)]
418    pub fn not(mut self) -> Self {
419        if let Some(last_filter) = self.filters.filters.last_mut() {
420            last_filter.not = true;
421        }
422        self
423    }
424
425    /// Finish and return to parent
426    pub fn done(self) -> P
427    where
428        P: HasParams,
429    {
430        #[allow(clippy::expect_used)]
431        let mut parent = self
432            .parent
433            .expect("FilterBuilder::done called without a parent (programming error)");
434        if let Some(ref mut existing_filters) = parent.params_mut().filters {
435            existing_filters.filters.extend(self.filters.filters);
436        } else {
437            parent.params_mut().filters = Some(self.filters);
438        }
439        parent
440    }
441}
442
443//
444// ========================
445//      SEARCH BUILDER
446// ========================
447//
448
449pub struct SearchBuilder<P = ()> {
450    parent: Option<P>,
451    search: SearchParams,
452}
453
454impl Default for SearchBuilder<()> {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl std::fmt::Debug for SearchBuilder<()> {
461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462        f.debug_struct("SearchBuilder")
463            .field("search", &self.search)
464            .finish()
465    }
466}
467
468impl SearchBuilder<()> {
469    pub fn new() -> Self {
470        Self {
471            parent: None,
472            search: SearchParams::new("", vec![]),
473        }
474    }
475
476    pub fn build(self) -> SearchParams {
477        self.search
478    }
479}
480
481impl<P> SearchBuilder<P> {
482    pub fn with_parent(parent: P) -> Self {
483        Self {
484            parent: Some(parent),
485            search: SearchParams::new("", vec![]),
486        }
487    }
488
489    pub fn query(mut self, q: impl Into<String>) -> Self {
490        self.search.query = q.into();
491        self
492    }
493
494    /// Convenience method - sets query and fields in one call
495    pub fn search<I, S>(mut self, query: impl Into<String>, fields: I) -> Self
496    where
497        I: IntoIterator<Item = S>,
498        S: Into<String>,
499    {
500        self.search.query = query.into();
501        self.search.fields = fields.into_iter().map(|s| s.into()).collect();
502        self
503    }
504
505    pub fn fields<I, S>(mut self, fields: I) -> Self
506    where
507        I: IntoIterator<Item = S>,
508        S: Into<String>,
509    {
510        self.search.fields = fields.into_iter().map(Into::into).collect();
511        self
512    }
513
514    pub fn exact(mut self, yes: bool) -> Self {
515        self.search = self.search.with_exact_match(yes);
516        self
517    }
518
519    pub fn case_sensitive(mut self, yes: bool) -> Self {
520        self.search = self.search.with_case_sensitive(yes);
521        self
522    }
523
524    pub fn done(self) -> P
525    where
526        P: HasParams,
527    {
528        #[allow(clippy::expect_used)]
529        let mut parent = self
530            .parent
531            .expect("SearchBuilder::done called without a parent (programming error)");
532
533        if !self.search.query.is_empty() {
534            parent.params_mut().search = Some(self.search);
535        }
536
537        parent
538    }
539}
540
541//
542// ========================
543//     SERIAL BUILDER
544// ========================
545//
546
547pub struct SerialBuilder<P = ()> {
548    parent: Option<P>,
549    serial: crate::serial::SerialParams,
550}
551
552impl Default for SerialBuilder<()> {
553    fn default() -> Self {
554        Self::new()
555    }
556}
557
558impl std::fmt::Debug for SerialBuilder<()> {
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        f.debug_struct("SerialBuilder")
561            .field("serial", &self.serial)
562            .finish()
563    }
564}
565
566impl SerialBuilder<()> {
567    pub fn new() -> Self {
568        Self {
569            parent: None,
570            serial: crate::serial::SerialParams::default(),
571        }
572    }
573
574    pub fn with_page(page: u32, per_page: u32) -> Self {
575        Self {
576            parent: None,
577            serial: crate::serial::SerialParams::new(page, per_page),
578        }
579    }
580
581    pub fn build(self) -> crate::serial::SerialParams {
582        self.serial
583    }
584}
585
586impl<P> SerialBuilder<P> {
587    pub fn with_parent(parent: P) -> Self {
588        Self {
589            parent: Some(parent),
590            serial: crate::serial::SerialParams::default(),
591        }
592    }
593
594    pub fn page(mut self, page: u32, per_page: u32) -> Self {
595        self.serial = crate::serial::SerialParams::new(page, per_page);
596        self
597    }
598
599    pub fn done(self) -> P
600    where
601        P: HasParams,
602    {
603        #[allow(clippy::expect_used)]
604        let mut parent = self
605            .parent
606            .expect("SerialBuilder::done called without a parent (programming error)");
607        let params = parent.params_mut();
608        let limit = self.serial.limit();
609        let offset = self.serial.offset();
610        params.pagination = Some(crate::pagination::Pagination::Serial(self.serial));
611        params.limit = Some(crate::pagination::LimitParam(limit));
612        params.offset = Some(crate::pagination::OffsetParam(offset));
613        parent
614    }
615}
616
617//
618// ========================
619//     SLICE BUILDER
620// ========================
621//
622
623pub struct SliceBuilder<P = ()> {
624    parent: Option<P>,
625    slice: crate::slice::SliceParams,
626}
627
628impl Default for SliceBuilder<()> {
629    fn default() -> Self {
630        Self::new()
631    }
632}
633
634impl std::fmt::Debug for SliceBuilder<()> {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        f.debug_struct("SliceBuilder")
637            .field("slice", &self.slice)
638            .finish()
639    }
640}
641
642impl SliceBuilder<()> {
643    pub fn new() -> Self {
644        Self {
645            parent: None,
646            slice: crate::slice::SliceParams::default(),
647        }
648    }
649
650    pub fn with_page(page: u32, per_page: u32) -> Self {
651        Self {
652            parent: None,
653            slice: crate::slice::SliceParams::new(page, per_page),
654        }
655    }
656
657    pub fn build(self) -> crate::slice::SliceParams {
658        self.slice
659    }
660}
661
662impl<P> SliceBuilder<P> {
663    pub fn with_parent(parent: P) -> Self {
664        Self {
665            parent: Some(parent),
666            slice: crate::slice::SliceParams::default(),
667        }
668    }
669
670    pub fn page(mut self, page: u32, per_page: u32) -> Self {
671        self.slice = crate::slice::SliceParams::new(page, per_page);
672        self
673    }
674
675    pub fn enable_total_count(mut self) -> Self {
676        self.slice = self.slice.with_disable_total_count(false);
677        self
678    }
679
680    pub fn done(self) -> P
681    where
682        P: HasParams,
683    {
684        #[allow(clippy::expect_used)]
685        let mut parent = self
686            .parent
687            .expect("SliceBuilder::done called without a parent (programming error)");
688        let params = parent.params_mut();
689        let limit = self.slice.limit();
690        let offset = self.slice.offset();
691        params.pagination = Some(crate::pagination::Pagination::Slice(self.slice));
692        params.limit = Some(crate::pagination::LimitParam(limit));
693        params.offset = Some(crate::pagination::OffsetParam(offset));
694        parent
695    }
696}
697
698//
699// ========================
700//      CURSOR BUILDER
701// ========================
702// Using typestate builder
703//
704
705/// Represents the initial state where no direction has been defined.
706/// At this stage, it can transition to `After`, `Before`, `FirstPage`, or decoded cursor.
707pub struct Initial;
708
709/// Represents the state for first page with empty cursor.
710/// This is explicitly chosen by the developer to indicate initial pagination state.
711pub struct FirstPage;
712
713/// Represents the state where the `After` direction (Next Page) has been explicitly defined.
714pub struct After;
715
716/// Represents the state where the `Before` direction (Previous Page) has been explicitly defined.
717pub struct Before;
718
719
720pub struct CursorBuilder<P = (), S = Initial> {
721    parent: Option<P>,
722    cursor: CursorParams,
723    _state: PhantomData<S>,
724}
725
726impl Default for CursorBuilder<(), Initial> {
727    fn default() -> Self {
728        Self::new()
729    }
730}
731
732impl<S> std::fmt::Debug for CursorBuilder<(), S> {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        f.debug_struct("CursorBuilder")
735            .field("cursor", &self.cursor)
736            .finish()
737    }
738}
739
740impl CursorBuilder<(), Initial> {
741    pub fn new() -> Self {
742        Self {
743            parent: None,
744            cursor: CursorParams::default(),
745            _state: PhantomData,
746        }
747    }
748
749    pub fn build(self) -> CursorParams {
750        self.cursor
751    }
752}
753
754// -----------------------------
755// Estado INITIAL - Initial state with no direction defined
756impl<P> CursorBuilder<P, Initial> {
757    pub fn with_parent(parent: P) -> Self {
758        Self {
759            parent: Some(parent),
760            cursor: CursorParams::default(),
761            _state: PhantomData,
762        }
763    }
764
765    // Transition: Initial -> FirstPage (explicit first page)
766    pub fn first_page(self) -> CursorBuilder<P, FirstPage> {
767        CursorBuilder {
768            parent: self.parent,
769            cursor: CursorParams::default(), // Empty cursor for first page
770            _state: PhantomData,
771        }
772    }
773
774    // Transition: Initial -> After
775    pub fn after(
776        self,
777        value: impl Into<FilterValue>
778    ) -> CursorBuilder<P, After> {
779        let mut cursor = CursorParams::default();
780        cursor = cursor.and_field(value.into());
781        cursor.direction = Some(CursorDirection::After);
782
783        CursorBuilder {
784            parent: self.parent,
785            cursor,
786            _state: PhantomData,
787        }
788    }
789
790    // Transition: Initial -> Before
791    pub fn before(
792        self,
793        value: impl Into<FilterValue>
794    ) -> CursorBuilder<P, Before> {
795        let mut cursor = CursorParams::default();
796        cursor = cursor.and_field(value.into());
797        cursor.direction = Some(CursorDirection::Before);
798
799        CursorBuilder {
800            parent: self.parent,
801            cursor,
802            _state: PhantomData,
803        }
804    }
805
806    // Transition: Initial -> After (with encoded token - user must decode externally)
807    pub fn next_cursor<T: crate::CursorSecureExtract>(self, token: impl Into<String>) -> CursorBuilder<P, After> {
808        self.decoded::<T, After>(token, CursorDirection::After)
809    }
810
811    // Transition: Initial -> Before (with encoded token - user must decode externally)
812    pub fn prev_cursor<T: crate::CursorSecureExtract>(self, token: impl Into<String>) -> CursorBuilder<P, Before> {
813        self.decoded::<T, Before>(token, CursorDirection::Before)
814    }
815
816    // Helper method to decode token with direction
817    fn decoded<T: crate::CursorSecureExtract, S>(
818        self,
819        token: impl Into<String>,
820        direction: CursorDirection,
821    ) -> CursorBuilder<P, S> {
822        let token = token.into();
823
824        let cursor = match T::decode(&token) {
825            Ok(decoded_values) => {
826                CursorParams::from_values(decoded_values, direction)
827            }
828            Err(e) => {
829                CursorParams::with_error(direction, e.to_string())
830            }
831        };
832
833        CursorBuilder {
834            parent: self.parent,
835            cursor,
836            _state: PhantomData,
837        }
838    }
839}
840
841// -----------------------------
842// Estado FIRSTPAGE - Explicit first page with empty cursor
843impl<P> CursorBuilder<P, FirstPage> {
844
845    pub fn done(self) -> P
846    where
847        P: HasParams,
848    {
849        #[allow(clippy::expect_used)]
850        let mut parent = self
851            .parent
852            .expect("CursorBuilder::done called without a parent (programming error)");
853        let params = parent.params_mut();
854        // Set cursor pagination with empty cursor - this enables next_cursor/prev_cursor generation
855        params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
856        parent
857    }
858}
859
860// -----------------------------
861// Estado AFTER - After direction (next page) defined
862impl<P> CursorBuilder<P, After> {
863
864    /// Add more fields to create composite cursor
865    pub fn and_field(mut self, value: impl Into<FilterValue>) -> Self {
866        self.cursor = self.cursor.and_field(value.into());
867        self
868    }
869
870    pub fn done(self) -> P
871    where
872        P: HasParams,
873    {
874        #[allow(clippy::expect_used)]
875        let mut parent = self
876            .parent
877            .expect("CursorBuilder::done called without a parent (programming error)");
878        let params = parent.params_mut();
879        params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
880        parent
881    }
882}
883
884// -----------------------------
885// Estado BEFORE - Before direction (previous page) defined
886impl<P> CursorBuilder<P, Before> {
887
888    /// Add more fields to create composite cursor
889    pub fn and_field(mut self, value: impl Into<FilterValue>) -> Self {
890        self.cursor = self.cursor.and_field(value.into());
891        self
892    }
893
894    pub fn done(self) -> P
895    where
896        P: HasParams,
897    {
898        #[allow(clippy::expect_used)]
899        let mut parent = self
900            .parent
901            .expect("CursorBuilder::done called without a parent (programming error)");
902        let params = parent.params_mut();
903        params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
904        parent
905    }
906}
907
908// -----------------------------
909
910/// Trait for types that have params
911pub trait HasParams {
912    fn params_mut(&mut self) -> &mut Params;
913}
914
915impl HasParams for ParamsBuilder {
916    fn params_mut(&mut self) -> &mut Params {
917        &mut self.params
918    }
919}
920
921impl IntoParams for ParamsBuilder {
922    fn into_params(self) -> Params {
923        self.params
924    }
925}
926
927impl IntoParams for FilterBuilder<()> {
928    fn into_params(self) -> Params {
929        Params {
930            filters: if self.filters.filters.is_empty() {
931                None
932            } else {
933                Some(self.filters)
934            },
935            ..Default::default()
936        }
937    }
938}
939
940impl IntoParams for SearchBuilder<()> {
941    fn into_params(self) -> Params {
942        Params {
943            search: if self.search.query.is_empty() {
944                None
945            } else {
946                Some(self.search)
947            },
948            ..Default::default()
949        }
950    }
951}
952
953impl IntoParams for SerialBuilder<()> {
954    fn into_params(self) -> Params {
955        let limit = self.serial.limit();
956        let offset = self.serial.offset();
957        Params {
958            pagination: Some(crate::pagination::Pagination::Serial(self.serial)),
959            limit: Some(crate::pagination::LimitParam(limit)),
960            offset: Some(crate::pagination::OffsetParam(offset)),
961            ..Default::default()
962        }
963    }
964}
965
966impl IntoParams for SliceBuilder<()> {
967    fn into_params(self) -> Params {
968        let limit = self.slice.limit();
969        let offset = self.slice.offset();
970        Params {
971            pagination: Some(crate::pagination::Pagination::Slice(self.slice)),
972            limit: Some(crate::pagination::LimitParam(limit)),
973            offset: Some(crate::pagination::OffsetParam(offset)),
974            ..Default::default()
975        }
976    }
977}
978
979// IntoParams implementations for different cursor states
980impl IntoParams for CursorBuilder<(), Initial> {
981    fn into_params(self) -> Params {
982        Params {
983            // Initial state without cursor - no pagination set
984            ..Default::default()
985        }
986    }
987}
988
989impl IntoParams for CursorBuilder<(), FirstPage> {
990    fn into_params(self) -> Params {
991        Params {
992            pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
993            ..Default::default()
994        }
995    }
996}
997
998impl IntoParams for CursorBuilder<(), After> {
999    fn into_params(self) -> Params {
1000        Params {
1001            pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
1002            ..Default::default()
1003        }
1004    }
1005}
1006
1007impl IntoParams for CursorBuilder<(), Before> {
1008    fn into_params(self) -> Params {
1009        Params {
1010            pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
1011            ..Default::default()
1012        }
1013    }
1014}
1015
1016
1017impl<S> IntoParams for SortBuilder<(), S> {
1018    fn into_params(self) -> Params {
1019        Params {
1020            sort_by: if self.sorts.is_empty() {
1021                None
1022            } else {
1023                Some(self.sorts)
1024            },
1025            ..Default::default()
1026        }
1027    }
1028}
1029#[cfg(test)]
1030mod security_tests {
1031    use super::*;
1032
1033    #[test]
1034    fn test_safe_sort_builder_compile_time_safety() {
1035        // This should compile - using static string literals
1036        let params = ParamsBuilder::new()
1037            .sort()
1038            .asc("id")           // &'static str - safe
1039            .desc("created_at")  // &'static str - safe
1040            .done()
1041            .build();
1042
1043        assert!(params.sort_by.is_some());
1044        let sort_by = params.sort_by.unwrap();
1045        assert_eq!(sort_by.sorts().len(), 2);
1046        assert_eq!(sort_by.sorts()[0].field, "id");
1047        assert!(sort_by.sorts()[0].is_asc());
1048        assert_eq!(sort_by.sorts()[1].field, "created_at");
1049        assert!(!sort_by.sorts()[1].is_asc());
1050    }
1051
1052    #[test]
1053    fn test_unsafe_sort_builder_with_runtime_validation() {
1054        // Test using dynamic strings - validation happens at runtime
1055        let dynamic_field = "name".to_string();
1056        let invalid_field = "malicious_field".to_string();
1057
1058        let params = ParamsBuilder::new()
1059            .sort()
1060            .with_allowed_columns(&["id", "name", "created_at", "age"])
1061            .asc_unsafe(dynamic_field)  // Valid field - preserved
1062            .desc_unsafe(invalid_field) // Invalid field - also preserved
1063            .done()
1064            .build();
1065
1066        assert!(params.sort_by.is_some());
1067        let sort_by = params.sort_by.unwrap();
1068        assert_eq!(sort_by.sorts().len(), 2);
1069
1070        // Fields are preserved as-is during build
1071        assert_eq!(sort_by.sorts()[0].field, "name");
1072        assert!(sort_by.sorts()[0].is_asc());
1073        assert_eq!(sort_by.sorts()[1].field, "malicious_field");
1074        assert!(!sort_by.sorts()[1].is_asc());
1075
1076        // Should have unsafe fields
1077        assert!(sort_by.has_unsafe_fields());
1078
1079        // Runtime validation should fail due to "malicious_field"
1080        assert!(sort_by.validate_fields().is_err());
1081    }
1082
1083    #[test]
1084    fn test_standalone_sort_builder() {
1085        // Test standalone usage (without ParamsBuilder)
1086        let safe_sort = SortBuilder::new()
1087            .asc("id")
1088            .desc("created_at")
1089            .build();
1090
1091        assert!(safe_sort.is_some());
1092        let sort_params = safe_sort.unwrap();
1093        assert_eq!(sort_params.sorts().len(), 2);
1094
1095        // Test unsafe standalone - no validation in builder, field is preserved
1096        let unsafe_sort = SortBuilder::new()
1097            .with_allowed_columns(&["id", "name"])
1098            .asc_unsafe("invalid_field".to_string()) // Field is preserved, validation deferred
1099            .build();
1100
1101        assert!(unsafe_sort.is_some());
1102        let sort_params = unsafe_sort.unwrap();
1103        assert_eq!(sort_params.sorts().len(), 1);
1104        assert_eq!(sort_params.sorts()[0].field, "invalid_field"); // Field preserved
1105        assert!(sort_params.has_unsafe_fields()); // Should have unsafe fields
1106
1107        // Validation should fail at runtime
1108        assert!(sort_params.validate_fields().is_err());
1109    }
1110
1111    #[test]
1112    fn test_runtime_validation_success() {
1113        // Test successful runtime validation
1114        let params = ParamsBuilder::new()
1115            .sort()
1116            .with_allowed_columns(&["id", "name", "email"])
1117            .asc_unsafe("name".to_string())  // Valid field
1118            .desc_unsafe("id".to_string())   // Valid field
1119            .done()
1120            .build();
1121
1122        let sort_by = params.sort_by.unwrap();
1123        assert!(sort_by.has_unsafe_fields());
1124
1125        // Runtime validation should succeed
1126        assert!(sort_by.validate_fields().is_ok());
1127    }
1128}