Skip to content

Commit fd79e73

Browse files
authored
Merge pull request #7983 from eth3lbert/search-seek
Expand the scope of seek-based pagination on search
2 parents 93e5a53 + 548be21 commit fd79e73

File tree

5 files changed

+920
-332
lines changed

5 files changed

+920
-332
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ object_store = { version = "=0.9.0", features = ["aws"] }
9696
once_cell = "=1.19.0"
9797
p256 = "=0.13.2"
9898
parking_lot = "=0.12.1"
99+
paste = "=1.0.14"
99100
prometheus = { version = "=0.13.3", default-features = false }
100101
rand = "=0.8.5"
101102
reqwest = { version = "=0.11.24", features = ["gzip", "json"] }

src/controllers/helpers/pagination.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,31 @@ impl<T> Paginated<T> {
198198
Some(opts)
199199
}
200200

201+
pub(crate) fn next_seek_params<S, F>(&self, f: F) -> AppResult<Option<IndexMap<String, String>>>
202+
where
203+
F: Fn(&T) -> S,
204+
S: Serialize,
205+
{
206+
if self.is_explicit_page() || self.records_and_total.len() < self.options.per_page as usize
207+
{
208+
return Ok(None);
209+
}
210+
211+
let mut opts = IndexMap::new();
212+
match self.options.page {
213+
Page::Unspecified | Page::Seek(_) => {
214+
let seek = f(&self.records_and_total.last().unwrap().record);
215+
opts.insert("seek".into(), encode_seek(seek)?);
216+
}
217+
Page::Numeric(_) => unreachable!(),
218+
};
219+
Ok(Some(opts))
220+
}
221+
222+
fn is_explicit_page(&self) -> bool {
223+
matches!(&self.options.page, Page::Numeric(_))
224+
}
225+
201226
pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
202227
self.records_and_total.iter().map(|row| &row.record)
203228
}
@@ -372,6 +397,73 @@ impl<T, C> PaginatedQueryWithCountSubq<T, C> {
372397
}
373398
}
374399

400+
macro_rules! seek {
401+
(
402+
$vis:vis enum $name:ident {
403+
$(
404+
$variant:ident($($(#[$field_meta:meta])? $ty:ty),*)
405+
)*
406+
}
407+
) => {
408+
paste::item! {
409+
$(
410+
#[derive(Debug, Default, Deserialize, Serialize, PartialEq)]
411+
$vis struct $variant($($(#[$field_meta])? pub(super) $ty),*);
412+
)*
413+
414+
#[derive(Debug, Deserialize, Serialize, PartialEq)]
415+
#[serde(untagged)]
416+
$vis enum [<$name Payload>] {
417+
$(
418+
$variant($variant),
419+
)*
420+
}
421+
422+
#[derive(Debug, PartialEq)]
423+
$vis enum $name {
424+
$(
425+
$variant,
426+
)*
427+
}
428+
429+
$(
430+
impl From<$variant> for [<$name Payload>] {
431+
fn from(value: $variant) -> Self {
432+
[<$name Payload>]::$variant(value)
433+
}
434+
}
435+
)*
436+
impl From<[<$name Payload>]> for $name {
437+
fn from(value: [<$name Payload>]) -> Self {
438+
match value {
439+
$(
440+
[<$name Payload>]::$variant(_) => $name::$variant,
441+
)*
442+
}
443+
}
444+
}
445+
446+
use crate::util::errors::AppResult;
447+
use crate::controllers::helpers::pagination::Page;
448+
impl $name {
449+
pub fn after(&self, page: &Page) -> AppResult<Option<[<$name Payload>]>> {
450+
let Page::Seek(ref encoded) = *page else {
451+
return Ok(None);
452+
};
453+
454+
Ok(Some(match self {
455+
$(
456+
$name::$variant => encoded.decode::<$variant>()?.into(),
457+
)*
458+
}))
459+
}
460+
}
461+
}
462+
};
463+
}
464+
465+
pub(crate) use seek;
466+
375467
#[cfg(test)]
376468
mod tests {
377469
use super::*;
@@ -476,6 +568,85 @@ mod tests {
476568
);
477569
}
478570

571+
mod seek {
572+
use chrono::naive::serde::ts_microseconds;
573+
seek! {
574+
pub(super) enum Seek {
575+
Id(i32)
576+
New(#[serde(with="ts_microseconds")] chrono::NaiveDateTime, i32)
577+
RecentDownloads(Option<i64>, i32)
578+
}
579+
}
580+
}
581+
582+
#[test]
583+
fn test_seek_macro_encode_and_decode() {
584+
use chrono::{NaiveDate, NaiveDateTime};
585+
use seek::*;
586+
587+
let assert_decode_after = |seek: Seek, query: &str, expect| {
588+
let pagination = PaginationOptions::builder()
589+
.enable_seek(true)
590+
.gather(&mock(query))
591+
.unwrap();
592+
let decoded = seek.after(&pagination.page).unwrap();
593+
assert_eq!(decoded, expect);
594+
};
595+
596+
let seek = Seek::Id;
597+
let payload = SeekPayload::Id(Id(1234));
598+
let query = format!("seek={}", encode_seek(&payload).unwrap());
599+
assert_decode_after(seek, &query, Some(payload));
600+
601+
let dt: NaiveDateTime = NaiveDate::from_ymd_opt(2016, 7, 8)
602+
.unwrap()
603+
.and_hms_opt(9, 10, 11)
604+
.unwrap();
605+
let seek = Seek::New;
606+
let payload = SeekPayload::New(New(dt, 1234));
607+
let query = format!("seek={}", encode_seek(&payload).unwrap());
608+
assert_decode_after(seek, &query, Some(payload));
609+
610+
let seek = Seek::RecentDownloads;
611+
let payload = SeekPayload::RecentDownloads(RecentDownloads(Some(5678), 1234));
612+
let query = format!("seek={}", encode_seek(&payload).unwrap());
613+
assert_decode_after(seek, &query, Some(payload));
614+
615+
let seek = Seek::Id;
616+
assert_decode_after(seek, "", None);
617+
618+
let seek = Seek::Id;
619+
let payload = SeekPayload::RecentDownloads(RecentDownloads(Some(5678), 1234));
620+
let query = format!("seek={}", encode_seek(payload).unwrap());
621+
let pagination = PaginationOptions::builder()
622+
.enable_seek(true)
623+
.gather(&mock(&query))
624+
.unwrap();
625+
let error = seek.after(&pagination.page).unwrap_err();
626+
assert_eq!(error.to_string(), "invalid seek parameter");
627+
let response = error.response();
628+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
629+
}
630+
631+
#[test]
632+
fn test_seek_macro_conv() {
633+
use chrono::{NaiveDate, NaiveDateTime};
634+
use seek::*;
635+
636+
assert_eq!(Seek::from(SeekPayload::Id(Id(1234))), Seek::Id);
637+
638+
let dt: NaiveDateTime = NaiveDate::from_ymd_opt(2016, 7, 8)
639+
.unwrap()
640+
.and_hms_opt(9, 10, 11)
641+
.unwrap();
642+
assert_eq!(Seek::from(SeekPayload::New(New(dt, 1234))), Seek::New);
643+
644+
assert_eq!(
645+
Seek::from(SeekPayload::RecentDownloads(RecentDownloads(None, 1234))),
646+
Seek::RecentDownloads
647+
);
648+
}
649+
479650
fn mock(query: &str) -> Request<()> {
480651
Request::builder()
481652
.method(Method::GET)

0 commit comments

Comments
 (0)