@@ -29,6 +29,7 @@ const MAX_PER_PAGE: i64 = 100;
2929pub ( crate ) enum Page {
3030 Numeric ( u32 ) ,
3131 Seek ( RawSeekPayload ) ,
32+ SeekBackward ( RawSeekPayload ) ,
3233 Unspecified ,
3334}
3435
@@ -157,8 +158,7 @@ impl PaginationOptionsBuilder {
157158 "seek backward ?seek=- is not supported for this request" ,
158159 ) ) ;
159160 }
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 ( ) ) ) ,
162162 false if !self . enable_seek => {
163163 return Err ( bad_request ( "?seek= is not supported for this request" ) ) ;
164164 }
@@ -225,15 +225,17 @@ impl<T> Paginated<T> {
225225 match self . options . page {
226226 Page :: Numeric ( n) => opts. insert ( "page" . into ( ) , ( n + 1 ) . to_string ( ) ) ,
227227 Page :: Unspecified => opts. insert ( "page" . into ( ) , 2 . to_string ( ) ) ,
228- Page :: Seek ( _) => return None ,
228+ Page :: Seek ( _) | Page :: SeekBackward ( _ ) => return None ,
229229 } ;
230230 Some ( opts)
231231 }
232232
233233 pub ( crate ) fn prev_page_params ( & self ) -> Option < IndexMap < String , String > > {
234234 let mut opts = IndexMap :: new ( ) ;
235235 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+ }
237239 Page :: Numeric ( n) => opts. insert ( "page" . into ( ) , ( n - 1 ) . to_string ( ) ) ,
238240 } ;
239241 Some ( opts)
@@ -244,14 +246,15 @@ impl<T> Paginated<T> {
244246 F : Fn ( & T ) -> S ,
245247 S : Serialize ,
246248 {
249+ // TODO: handle this more properly when seek backward comes into play.
247250 if self . is_explicit_page ( ) || self . records_and_total . len ( ) < self . options . per_page as usize
248251 {
249252 return Ok ( None ) ;
250253 }
251254
252255 let mut opts = IndexMap :: new ( ) ;
253256 match self . options . page {
254- Page :: Unspecified | Page :: Seek ( _) => {
257+ Page :: Unspecified | Page :: Seek ( _) | Page :: SeekBackward ( _ ) => {
255258 let seek = f ( & self . records_and_total . last ( ) . unwrap ( ) . record ) ;
256259 opts. insert ( "seek" . into ( ) , encode_seek ( seek) ?) ;
257260 }
@@ -302,6 +305,8 @@ impl<T> PaginatedQuery<T> {
302305 async move {
303306 let records_and_total = future. await ?. try_collect ( ) . await ?;
304307
308+ // TODO: maintain consistent ordering from page to pagen
309+
305310 Ok ( Paginated {
306311 records_and_total,
307312 options,
@@ -450,6 +455,8 @@ impl<T, C> PaginatedQueryWithCountSubq<T, C> {
450455 async move {
451456 let records_and_total = future. await ?. try_collect ( ) . await ?;
452457
458+ // TODO: maintain consistent ordering from page to pagen
459+
453460 Ok ( Paginated {
454461 records_and_total,
455462 options,
@@ -539,8 +546,10 @@ macro_rules! seek {
539546 use crate :: controllers:: helpers:: pagination:: Page ;
540547 impl $name {
541548 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 ) ,
544553 } ;
545554
546555 Ok ( Some ( match self {
@@ -619,6 +628,24 @@ mod tests {
619628 pagination. page
620629 ) ;
621630 }
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+ }
622649 }
623650
624651 #[ test]
@@ -631,6 +658,13 @@ mod tests {
631658 "page=1&seek=OTg" ,
632659 ) ;
633660 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" ) ;
634668 }
635669
636670 #[ test]
@@ -681,20 +715,27 @@ mod tests {
681715 use chrono:: serde:: ts_microseconds;
682716 use seek:: * ;
683717
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+ }
691733 } ;
692734
693735 let id = 1234 ;
694736 let seek = Seek :: Id ;
695737 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) ) ;
698739
699740 let dt = NaiveDate :: from_ymd_opt ( 2016 , 7 , 8 )
700741 . unwrap ( )
@@ -703,29 +744,41 @@ mod tests {
703744 . and_utc ( ) ;
704745 let seek = Seek :: New ;
705746 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) ) ;
708748
709749 let downloads = Some ( 5678 ) ;
710750 let seek = Seek :: RecentDownloads ;
711751 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) ) ;
714753
715754 let seek = Seek :: Id ;
716- assert_decode_after ( seek, "" , None ) ;
755+ assert_decode ( seek, None ) ;
717756
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+ }
729782
730783 // Ensures it still encodes compactly with a field struct
731784 #[ derive( Debug , Default , Serialize , PartialEq ) ]
0 commit comments