Skip to content

Commit 77eb338

Browse files
committed
controllers/helpers/pagination: Add Page::SeekBackward() variant
1 parent d1dc40b commit 77eb338

File tree

3 files changed

+87
-33
lines changed

3 files changed

+87
-33
lines changed

src/controllers/crate_owner_invitation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ async fn prepare_list(
212212
.load(conn)
213213
.await?
214214
}
215+
Page::SeekBackward(_) => unreachable!("seek-backward is disabled"),
215216
Page::Numeric(_) => unreachable!("page-based pagination is disabled"),
216217
};
217218
let next_page = if raw_invitations.len() > pagination.per_page as usize {

src/controllers/helpers/pagination.rs

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const MAX_PER_PAGE: i64 = 100;
2929
pub(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)]

src/controllers/krate/versions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ where
432432
let seek = f(records.last().unwrap());
433433
opts.insert("seek".into(), encode_seek(seek)?);
434434
}
435-
Page::Numeric(_) => unreachable!(),
435+
Page::Numeric(_) | Page::SeekBackward(_) => unreachable!(),
436436
};
437437
Ok(Some(opts))
438438
}

0 commit comments

Comments
 (0)