@@ -29,6 +29,7 @@ const MAX_PER_PAGE: i64 = 100;
29
29
pub ( crate ) enum Page {
30
30
Numeric ( u32 ) ,
31
31
Seek ( RawSeekPayload ) ,
32
+ SeekBackward ( RawSeekPayload ) ,
32
33
Unspecified ,
33
34
}
34
35
@@ -157,8 +158,7 @@ impl PaginationOptionsBuilder {
157
158
"seek backward ?seek=- is not supported for this request" ,
158
159
) ) ;
159
160
}
160
- // TODO: add a varaint for seek backward
161
- true => unimplemented ! ( "seek backward is not yet implemented" ) ,
161
+ true => Page :: SeekBackward ( RawSeekPayload ( s. trim_start_matches ( '-' ) . into ( ) ) ) ,
162
162
false if !self . enable_seek => {
163
163
return Err ( bad_request ( "?seek= is not supported for this request" ) ) ;
164
164
}
@@ -225,15 +225,17 @@ impl<T> Paginated<T> {
225
225
match self . options . page {
226
226
Page :: Numeric ( n) => opts. insert ( "page" . into ( ) , ( n + 1 ) . to_string ( ) ) ,
227
227
Page :: Unspecified => opts. insert ( "page" . into ( ) , 2 . to_string ( ) ) ,
228
- Page :: Seek ( _) => return None ,
228
+ Page :: Seek ( _) | Page :: SeekBackward ( _ ) => return None ,
229
229
} ;
230
230
Some ( opts)
231
231
}
232
232
233
233
pub ( crate ) fn prev_page_params ( & self ) -> Option < IndexMap < String , String > > {
234
234
let mut opts = IndexMap :: new ( ) ;
235
235
match self . options . page {
236
- Page :: Numeric ( 1 ) | Page :: Unspecified | Page :: Seek ( _) => return None ,
236
+ Page :: Numeric ( 1 ) | Page :: Unspecified | Page :: Seek ( _) | Page :: SeekBackward ( _) => {
237
+ return None ;
238
+ }
237
239
Page :: Numeric ( n) => opts. insert ( "page" . into ( ) , ( n - 1 ) . to_string ( ) ) ,
238
240
} ;
239
241
Some ( opts)
@@ -244,14 +246,15 @@ impl<T> Paginated<T> {
244
246
F : Fn ( & T ) -> S ,
245
247
S : Serialize ,
246
248
{
249
+ // TODO: handle this more properly when seek backward comes into play.
247
250
if self . is_explicit_page ( ) || self . records_and_total . len ( ) < self . options . per_page as usize
248
251
{
249
252
return Ok ( None ) ;
250
253
}
251
254
252
255
let mut opts = IndexMap :: new ( ) ;
253
256
match self . options . page {
254
- Page :: Unspecified | Page :: Seek ( _) => {
257
+ Page :: Unspecified | Page :: Seek ( _) | Page :: SeekBackward ( _ ) => {
255
258
let seek = f ( & self . records_and_total . last ( ) . unwrap ( ) . record ) ;
256
259
opts. insert ( "seek" . into ( ) , encode_seek ( seek) ?) ;
257
260
}
@@ -302,6 +305,8 @@ impl<T> PaginatedQuery<T> {
302
305
async move {
303
306
let records_and_total = future. await ?. try_collect ( ) . await ?;
304
307
308
+ // TODO: maintain consistent ordering from page to pagen
309
+
305
310
Ok ( Paginated {
306
311
records_and_total,
307
312
options,
@@ -450,6 +455,8 @@ impl<T, C> PaginatedQueryWithCountSubq<T, C> {
450
455
async move {
451
456
let records_and_total = future. await ?. try_collect ( ) . await ?;
452
457
458
+ // TODO: maintain consistent ordering from page to pagen
459
+
453
460
Ok ( Paginated {
454
461
records_and_total,
455
462
options,
@@ -539,8 +546,10 @@ macro_rules! seek {
539
546
use crate :: controllers:: helpers:: pagination:: Page ;
540
547
impl $name {
541
548
pub fn decode( & self , page: & Page ) -> AppResult <Option <[ <$name Payload >] >> {
542
- let Page :: Seek ( ref encoded) = * page else {
543
- return Ok ( None ) ;
549
+ let encoded = match page {
550
+ Page :: Seek ( encoded) => encoded,
551
+ Page :: SeekBackward ( encoded) => encoded,
552
+ _ => return Ok ( None ) ,
544
553
} ;
545
554
546
555
Ok ( Some ( match self {
@@ -619,6 +628,24 @@ mod tests {
619
628
pagination. page
620
629
) ;
621
630
}
631
+
632
+ // for backward
633
+ let error = pagination_error ( PaginationOptions :: builder ( ) , "seek=-OTg" ) ;
634
+ assert_snapshot ! ( error, @"seek backward ?seek=- is not supported for this request" ) ;
635
+
636
+ let pagination = PaginationOptions :: builder ( )
637
+ . enable_seek_backward ( true )
638
+ . gather ( & mock ( "seek=-OTg" ) )
639
+ . unwrap ( ) ;
640
+
641
+ if let Page :: SeekBackward ( raw) = pagination. page {
642
+ assert_ok_eq ! ( raw. decode:: <i32 >( ) , 98 ) ;
643
+ } else {
644
+ panic ! (
645
+ "did not parse a seek page, parsed {:?} instead" ,
646
+ pagination. page
647
+ ) ;
648
+ }
622
649
}
623
650
624
651
#[ test]
@@ -631,6 +658,13 @@ mod tests {
631
658
"page=1&seek=OTg" ,
632
659
) ;
633
660
assert_snapshot ! ( error, @"providing both ?page= and ?seek= is unsupported" ) ;
661
+
662
+ // for backward
663
+ let error = pagination_error (
664
+ PaginationOptions :: builder ( ) . enable_seek_backward ( true ) ,
665
+ "page=1&seek=-OTg" ,
666
+ ) ;
667
+ assert_snapshot ! ( error, @"providing both ?page= and ?seek= is unsupported" ) ;
634
668
}
635
669
636
670
#[ test]
@@ -681,20 +715,27 @@ mod tests {
681
715
use chrono:: serde:: ts_microseconds;
682
716
use seek:: * ;
683
717
684
- let assert_decode_after = |seek : Seek , query : & str , expect| {
685
- let pagination = PaginationOptions :: builder ( )
686
- . enable_seek ( true )
687
- . gather ( & mock ( query) )
688
- . unwrap ( ) ;
689
- let decoded = seek. decode ( & pagination. page ) . unwrap ( ) ;
690
- assert_eq ! ( decoded, expect) ;
718
+ let assert_decode = |seek : Seek , payload : Option < _ > | {
719
+ for ( param, prefix) in [ ( "seek" , "" ) , ( "seek" , "-" ) ] {
720
+ let query = if let Some ( ref s) = payload {
721
+ & format ! ( "{param}={prefix}{}" , encode_seek( s) . unwrap( ) )
722
+ } else {
723
+ ""
724
+ } ;
725
+ let pagination = PaginationOptions :: builder ( )
726
+ . enable_seek ( true )
727
+ . enable_seek_backward ( true )
728
+ . gather ( & mock ( query) )
729
+ . unwrap ( ) ;
730
+ let decoded = seek. decode ( & pagination. page ) . unwrap ( ) ;
731
+ assert_eq ! ( decoded. as_ref( ) , payload. as_ref( ) ) ;
732
+ }
691
733
} ;
692
734
693
735
let id = 1234 ;
694
736
let seek = Seek :: Id ;
695
737
let payload = SeekPayload :: Id ( Id { id } ) ;
696
- let query = format ! ( "seek={}" , encode_seek( & payload) . unwrap( ) ) ;
697
- assert_decode_after ( seek, & query, Some ( payload) ) ;
738
+ assert_decode ( seek, Some ( payload) ) ;
698
739
699
740
let dt = NaiveDate :: from_ymd_opt ( 2016 , 7 , 8 )
700
741
. unwrap ( )
@@ -703,29 +744,41 @@ mod tests {
703
744
. and_utc ( ) ;
704
745
let seek = Seek :: New ;
705
746
let payload = SeekPayload :: New ( New { dt, id } ) ;
706
- let query = format ! ( "seek={}" , encode_seek( & payload) . unwrap( ) ) ;
707
- assert_decode_after ( seek, & query, Some ( payload) ) ;
747
+ assert_decode ( seek, Some ( payload) ) ;
708
748
709
749
let downloads = Some ( 5678 ) ;
710
750
let seek = Seek :: RecentDownloads ;
711
751
let payload = SeekPayload :: RecentDownloads ( RecentDownloads { downloads, id } ) ;
712
- let query = format ! ( "seek={}" , encode_seek( & payload) . unwrap( ) ) ;
713
- assert_decode_after ( seek, & query, Some ( payload) ) ;
752
+ assert_decode ( seek, Some ( payload) ) ;
714
753
715
754
let seek = Seek :: Id ;
716
- assert_decode_after ( seek, "" , None ) ;
755
+ assert_decode ( seek, None ) ;
717
756
718
- let seek = Seek :: Id ;
719
- let payload = SeekPayload :: RecentDownloads ( RecentDownloads { downloads, id } ) ;
720
- let query = format ! ( "seek={}" , encode_seek( payload) . unwrap( ) ) ;
721
- let pagination = PaginationOptions :: builder ( )
722
- . enable_seek ( true )
723
- . gather ( & mock ( & query) )
724
- . unwrap ( ) ;
725
- let error = seek. decode ( & pagination. page ) . unwrap_err ( ) ;
726
- assert_eq ! ( error. to_string( ) , "invalid seek parameter" ) ;
727
- let response = error. response ( ) ;
728
- assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
757
+ // invalid seek payload
758
+ {
759
+ let seek = Seek :: Id ;
760
+ let payload = SeekPayload :: RecentDownloads ( RecentDownloads { downloads, id } ) ;
761
+ let query = format ! ( "seek={}" , encode_seek( & payload) . unwrap( ) ) ;
762
+ let pagination = PaginationOptions :: builder ( )
763
+ . enable_seek ( true )
764
+ . gather ( & mock ( & query) )
765
+ . unwrap ( ) ;
766
+ let error = seek. decode ( & pagination. page ) . unwrap_err ( ) ;
767
+ assert_eq ! ( error. to_string( ) , "invalid seek parameter" ) ;
768
+ let response = error. response ( ) ;
769
+ assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
770
+
771
+ // for backward
772
+ let query = format ! ( "seek=-{}" , encode_seek( & payload) . unwrap( ) ) ;
773
+ let pagination = PaginationOptions :: builder ( )
774
+ . enable_seek_backward ( true )
775
+ . gather ( & mock ( & query) )
776
+ . unwrap ( ) ;
777
+ let error = seek. decode ( & pagination. page ) . unwrap_err ( ) ;
778
+ assert_eq ! ( error. to_string( ) , "invalid seek parameter" ) ;
779
+ let response = error. response ( ) ;
780
+ assert_eq ! ( response. status( ) , StatusCode :: BAD_REQUEST ) ;
781
+ }
729
782
730
783
// Ensures it still encodes compactly with a field struct
731
784
#[ derive( Debug , Default , Serialize , PartialEq ) ]
0 commit comments