From 9f57dd49067ff707d53c5cd7d3d5fc110ea1cbf2 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 7 Mar 2024 04:48:59 +0800 Subject: [PATCH] controllers/helpers/pagination: Add named fields struct support for `seek!` --- src/controllers/helpers/pagination.rs | 127 ++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/src/controllers/helpers/pagination.rs b/src/controllers/helpers/pagination.rs index 0a396589af8..2c895d384f6 100644 --- a/src/controllers/helpers/pagination.rs +++ b/src/controllers/helpers/pagination.rs @@ -401,20 +401,56 @@ impl PaginatedQueryWithCountSubq { } } +#[allow(unused_macro_rules)] macro_rules! seek { + // Tuple struct + (@variant_struct $vis:vis $variant:ident($($(#[$field_meta:meta])? $ty:ty),*)) => { + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + $vis struct $variant($($(#[$field_meta])? pub(super) $ty),*); + }; + // Field struct + (@variant_struct $vis:vis $variant:ident { + $($(#[$field_meta:meta])? $field:ident: $ty:ty),* + }) => { + paste::item! { + #[derive(Debug, Default, Deserialize, PartialEq)] + #[serde(from = $variant "Helper")] + $vis struct $variant { + $($(#[$field_meta])? pub(super) $field: $ty),* + } + + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] + struct [<$variant Helper>]($($(#[$field_meta])? pub(super) $ty),*); + + impl From<[<$variant Helper>]> for $variant { + fn from(helper: [<$variant Helper>]) -> Self { + let [<$variant Helper>]($($field,)*) = helper; + Self { $($field,)* } + } + } + + impl serde::Serialize for $variant { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let helper = [<$variant Helper>]($(self.$field,)*); + serde::Serialize::serialize(&helper, serializer) + } + } + } + }; ( $vis:vis enum $name:ident { $( - $variant:ident($($(#[$field_meta:meta])? $ty:ty),*) + $variant:ident $fields:tt )* } ) => { + $( + seek!(@variant_struct $vis $variant $fields); + )* paste::item! { - $( - #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] - $vis struct $variant($($(#[$field_meta])? pub(super) $ty),*); - )* - #[derive(Debug, Deserialize, Serialize, PartialEq)] #[serde(untagged)] $vis enum [<$name Payload>] { @@ -583,12 +619,16 @@ mod tests { Id(i32) New(#[serde(with="ts_microseconds")] chrono::NaiveDateTime, i32) RecentDownloads(Option, i32) + NamedId{id: i32} + NamedNew{#[serde(with="ts_microseconds")] dt: chrono::NaiveDateTime, id: i32} + NamedRecentDownloads{ downloads: Option, id: i32 } } } } #[test] fn test_seek_macro_encode_and_decode() { + use chrono::naive::serde::ts_microseconds; use chrono::{NaiveDate, NaiveDateTime}; use seek::*; @@ -601,6 +641,7 @@ mod tests { assert_eq!(decoded, expect); }; + // Tuple struct let seek = Seek::Id; let payload = SeekPayload::Id(Id(1234)); let query = format!("seek={}", encode_seek(&payload).unwrap()); @@ -634,6 +675,54 @@ mod tests { assert_eq!(error.to_string(), "invalid seek parameter"); let response = error.response(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Field struct + let id = 1234; + let seek = Seek::NamedId; + let payload = SeekPayload::NamedId(NamedId { id }); + let query = format!("seek={}", encode_seek(&payload).unwrap()); + assert_decode_after(seek, &query, Some(payload)); + + let dt: NaiveDateTime = NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(); + let seek = Seek::NamedNew; + let payload = SeekPayload::NamedNew(NamedNew { dt, id }); + let query = format!("seek={}", encode_seek(&payload).unwrap()); + assert_decode_after(seek, &query, Some(payload)); + + let downloads = Some(5678); + let seek = Seek::NamedRecentDownloads; + let payload = SeekPayload::NamedRecentDownloads(NamedRecentDownloads { downloads, id }); + let query = format!("seek={}", encode_seek(&payload).unwrap()); + assert_decode_after(seek, &query, Some(payload)); + + let seek = Seek::Id; + assert_decode_after(seek, "", None); + + let seek = Seek::Id; + let payload = SeekPayload::NamedRecentDownloads(NamedRecentDownloads { downloads, id }); + let query = format!("seek={}", encode_seek(payload).unwrap()); + let pagination = PaginationOptions::builder() + .enable_seek(true) + .gather(&mock(&query)) + .unwrap(); + let error = seek.after(&pagination.page).unwrap_err(); + assert_eq!(error.to_string(), "invalid seek parameter"); + let response = error.response(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Ensures it still encodes compactly with a field struct + #[derive(Debug, Default, Serialize, PartialEq)] + struct NewTuple( + #[serde(with = "ts_microseconds")] chrono::NaiveDateTime, + i32, + ); + assert_eq!( + encode_seek(NewTuple(dt, id)).unwrap(), + encode_seek(SeekPayload::NamedNew(NamedNew { dt, id })).unwrap() + ); } #[test] @@ -641,6 +730,7 @@ mod tests { use chrono::{NaiveDate, NaiveDateTime}; use seek::*; + // Tuple struct assert_eq!(Seek::from(SeekPayload::Id(Id(1234))), Seek::Id); let dt: NaiveDateTime = NaiveDate::from_ymd_opt(2016, 7, 8) @@ -653,6 +743,31 @@ mod tests { Seek::from(SeekPayload::RecentDownloads(RecentDownloads(None, 1234))), Seek::RecentDownloads ); + + // Field struct + let id = 1234; + assert_eq!( + Seek::from(SeekPayload::NamedId(NamedId { id })), + Seek::NamedId + ); + + let dt: NaiveDateTime = NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(); + assert_eq!( + Seek::from(SeekPayload::NamedNew(NamedNew { dt, id })), + Seek::NamedNew + ); + + let downloads = None; + assert_eq!( + Seek::from(SeekPayload::NamedRecentDownloads(NamedRecentDownloads { + downloads, + id + })), + Seek::NamedRecentDownloads + ); } fn mock(query: &str) -> Request<()> {