@@ -45,6 +45,8 @@ use crate::api::external::Name;
45
45
use crate :: api:: external:: NameOrId ;
46
46
use crate :: api:: external:: ObjectIdentity ;
47
47
use crate :: api:: external:: PaginationOrder ;
48
+ use chrono:: DateTime ;
49
+ use chrono:: Utc ;
48
50
use dropshot:: HttpError ;
49
51
use dropshot:: PaginationParams ;
50
52
use dropshot:: RequestContext ;
@@ -421,6 +423,57 @@ impl<T: Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize>
421
423
}
422
424
}
423
425
426
+ /// Query parameters for pagination by timestamp and ID
427
+ pub type PaginatedByTimeAndId < Selector = ( ) > = PaginationParams <
428
+ ScanByTimeAndId < Selector > ,
429
+ PageSelectorByTimeAndId < Selector > ,
430
+ > ;
431
+ /// Page selector for pagination by timestamp and ID
432
+ pub type PageSelectorByTimeAndId < Selector = ( ) > =
433
+ PageSelector < ScanByTimeAndId < Selector > , ( DateTime < Utc > , Uuid ) > ;
434
+
435
+ /// Scan parameters for resources that support scanning by (timestamp, id)
436
+ #[ derive( Clone , Debug , Deserialize , JsonSchema , PartialEq , Serialize ) ]
437
+ pub struct ScanByTimeAndId < Selector = ( ) > {
438
+ #[ serde( default = "default_ts_id_sort_mode" ) ]
439
+ sort_by : TimeAndIdSortMode ,
440
+
441
+ #[ serde( flatten) ]
442
+ pub selector : Selector ,
443
+ }
444
+
445
+ /// Supported set of sort modes for scanning by timestamp and ID
446
+ #[ derive( Copy , Clone , Debug , Deserialize , JsonSchema , PartialEq , Serialize ) ]
447
+ #[ serde( rename_all = "snake_case" ) ]
448
+ pub enum TimeAndIdSortMode {
449
+ /// sort in increasing order of timestamp and ID, i.e., earliest first
450
+ Ascending ,
451
+ /// sort in increasing order of timestamp and ID, i.e., most recent first
452
+ Descending ,
453
+ }
454
+
455
+ fn default_ts_id_sort_mode ( ) -> TimeAndIdSortMode {
456
+ TimeAndIdSortMode :: Ascending
457
+ }
458
+
459
+ impl < T : Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize >
460
+ ScanParams for ScanByTimeAndId < T >
461
+ {
462
+ type MarkerValue = ( DateTime < Utc > , Uuid ) ;
463
+ fn direction ( & self ) -> PaginationOrder {
464
+ match self . sort_by {
465
+ TimeAndIdSortMode :: Ascending => PaginationOrder :: Ascending ,
466
+ TimeAndIdSortMode :: Descending => PaginationOrder :: Descending ,
467
+ }
468
+ }
469
+ fn from_query ( p : & PaginatedByTimeAndId < T > ) -> Result < & Self , HttpError > {
470
+ Ok ( match p. page {
471
+ WhichPage :: First ( ref scan_params) => scan_params,
472
+ WhichPage :: Next ( PageSelector { ref scan, .. } ) => scan,
473
+ } )
474
+ }
475
+ }
476
+
424
477
#[ cfg( test) ]
425
478
mod test {
426
479
use super :: IdSortMode ;
@@ -432,14 +485,18 @@ mod test {
432
485
use super :: PageSelectorById ;
433
486
use super :: PageSelectorByName ;
434
487
use super :: PageSelectorByNameOrId ;
488
+ use super :: PageSelectorByTimeAndId ;
435
489
use super :: PaginatedBy ;
436
490
use super :: PaginatedById ;
437
491
use super :: PaginatedByName ;
438
492
use super :: PaginatedByNameOrId ;
493
+ use super :: PaginatedByTimeAndId ;
439
494
use super :: ScanById ;
440
495
use super :: ScanByName ;
441
496
use super :: ScanByNameOrId ;
497
+ use super :: ScanByTimeAndId ;
442
498
use super :: ScanParams ;
499
+ use super :: TimeAndIdSortMode ;
443
500
use super :: data_page_params_with_limit;
444
501
use super :: marker_for_id;
445
502
use super :: marker_for_name;
@@ -448,6 +505,8 @@ mod test {
448
505
use crate :: api:: external:: IdentityMetadata ;
449
506
use crate :: api:: external:: ObjectIdentity ;
450
507
use crate :: api:: external:: http_pagination:: name_or_id_pagination;
508
+ use chrono:: DateTime ;
509
+ use chrono:: TimeZone ;
451
510
use chrono:: Utc ;
452
511
use dropshot:: PaginationOrder ;
453
512
use dropshot:: PaginationParams ;
@@ -486,6 +545,10 @@ mod test {
486
545
"page selector, scan by name or id" ,
487
546
schema_for!( PageSelectorByNameOrId ) ,
488
547
) ,
548
+ (
549
+ "page selector, scan by time and id" ,
550
+ schema_for!( PageSelectorByTimeAndId ) ,
551
+ ) ,
489
552
] ;
490
553
491
554
let mut found_output = String :: new ( ) ;
@@ -515,8 +578,14 @@ mod test {
515
578
sort_by : NameOrIdSortMode :: IdAscending ,
516
579
selector : ( ) ,
517
580
} ;
581
+ let scan_by_time_and_id = ScanByTimeAndId :: < ( ) > {
582
+ sort_by : TimeAndIdSortMode :: Ascending ,
583
+ selector : ( ) ,
584
+ } ;
518
585
let id: Uuid = "61a78113-d3c6-4b35-a410-23e9eae64328" . parse ( ) . unwrap ( ) ;
519
586
let name: Name = "bort" . parse ( ) . unwrap ( ) ;
587
+ let time: DateTime < Utc > =
588
+ Utc . with_ymd_and_hms ( 2025 , 3 , 20 , 10 , 30 , 45 ) . unwrap ( ) ;
520
589
let examples = vec ! [
521
590
// scan parameters only
522
591
( "scan by id ascending" , to_string_pretty( & scan_by_id) . unwrap( ) ) ,
@@ -532,6 +601,14 @@ mod test {
532
601
"scan by name or id, using name ascending" ,
533
602
to_string_pretty( & scan_by_nameid_name) . unwrap( ) ,
534
603
) ,
604
+ (
605
+ "scan by name or id, using name ascending" ,
606
+ to_string_pretty( & scan_by_nameid_name) . unwrap( ) ,
607
+ ) ,
608
+ (
609
+ "scan by time and id, ascending" ,
610
+ to_string_pretty( & scan_by_time_and_id) . unwrap( ) ,
611
+ ) ,
535
612
// page selectors
536
613
(
537
614
"page selector: by id ascending" ,
@@ -565,6 +642,14 @@ mod test {
565
642
} )
566
643
. unwrap( ) ,
567
644
) ,
645
+ (
646
+ "page selector: by time and id, ascending" ,
647
+ to_string_pretty( & PageSelectorByTimeAndId {
648
+ scan: scan_by_time_and_id,
649
+ last_seen: ( time, id) ,
650
+ } )
651
+ . unwrap( ) ,
652
+ ) ,
568
653
] ;
569
654
570
655
let mut found_output = String :: new ( ) ;
@@ -834,6 +919,7 @@ mod test {
834
919
let thing0_marker = NameOrId :: Id ( list[ 0 ] . identity . id ) ;
835
920
let thinglast_id = list[ list. len ( ) - 1 ] . identity . id ;
836
921
let thinglast_marker = NameOrId :: Id ( list[ list. len ( ) - 1 ] . identity . id ) ;
922
+
837
923
let ( p0, p1) = test_scan_param_common (
838
924
& list,
839
925
& scan,
@@ -871,4 +957,89 @@ mod test {
871
957
assert_eq ! ( data_page. direction, PaginationOrder :: Ascending ) ;
872
958
assert_eq ! ( data_page. limit, limit) ;
873
959
}
960
+
961
+ #[ test]
962
+ fn test_scan_by_time_and_id ( ) {
963
+ let scan = ScanByTimeAndId {
964
+ sort_by : TimeAndIdSortMode :: Ascending ,
965
+ selector : ( ) ,
966
+ } ;
967
+
968
+ let list = list_of_things ( ) ;
969
+ let item0_time = list[ 0 ] . identity . time_created ;
970
+ let item0_id = list[ 0 ] . identity . id ;
971
+ let item0_marker = ( item0_time, item0_id) ;
972
+
973
+ let last_idx = list. len ( ) - 1 ;
974
+ let item_last_time = list[ last_idx] . identity . time_created ;
975
+ let item_last_id = list[ last_idx] . identity . id ;
976
+ let item_last_marker = ( item_last_time, item_last_id) ;
977
+
978
+ let marker_fn =
979
+ |_: & ScanByTimeAndId , item : & MyThing | -> ( DateTime < Utc > , Uuid ) {
980
+ ( item. identity . time_created , item. identity . id )
981
+ } ;
982
+ let ( p0, p1) = test_scan_param_common (
983
+ & list,
984
+ & scan,
985
+ "sort_by=ascending" ,
986
+ & item0_marker,
987
+ & item_last_marker,
988
+ & scan,
989
+ & marker_fn,
990
+ ) ;
991
+
992
+ assert_eq ! ( scan. direction( ) , PaginationOrder :: Ascending ) ;
993
+
994
+ // Verify data pages based on the query params.
995
+ let limit = NonZeroU32 :: new ( 123 ) . unwrap ( ) ;
996
+ let data_page = data_page_params_with_limit ( limit, & p0) . unwrap ( ) ;
997
+ assert_eq ! ( data_page. marker, None ) ;
998
+ assert_eq ! ( data_page. direction, PaginationOrder :: Ascending ) ;
999
+ assert_eq ! ( data_page. limit, limit) ;
1000
+
1001
+ let data_page = data_page_params_with_limit ( limit, & p1) . unwrap ( ) ;
1002
+ assert_eq ! ( data_page. marker, Some ( & item_last_marker) ) ;
1003
+ assert_eq ! ( data_page. direction, PaginationOrder :: Ascending ) ;
1004
+ assert_eq ! ( data_page. limit, limit) ;
1005
+
1006
+ // test descending too, why not (it caught a mistake!)
1007
+ let scan_desc = ScanByTimeAndId {
1008
+ sort_by : TimeAndIdSortMode :: Descending ,
1009
+ selector : ( ) ,
1010
+ } ;
1011
+ let ( p0, p1) = test_scan_param_common (
1012
+ & list,
1013
+ & scan_desc,
1014
+ "sort_by=descending" ,
1015
+ & item0_marker,
1016
+ & item_last_marker,
1017
+ & scan,
1018
+ & marker_fn,
1019
+ ) ;
1020
+ assert_eq ! ( scan_desc. direction( ) , PaginationOrder :: Descending ) ;
1021
+
1022
+ // Verify data pages based on the query params.
1023
+ let limit = NonZeroU32 :: new ( 123 ) . unwrap ( ) ;
1024
+ let data_page = data_page_params_with_limit ( limit, & p0) . unwrap ( ) ;
1025
+ assert_eq ! ( data_page. marker, None ) ;
1026
+ assert_eq ! ( data_page. direction, PaginationOrder :: Descending ) ;
1027
+ assert_eq ! ( data_page. limit, limit) ;
1028
+
1029
+ let data_page = data_page_params_with_limit ( limit, & p1) . unwrap ( ) ;
1030
+ assert_eq ! ( data_page. marker, Some ( & item_last_marker) ) ;
1031
+ assert_eq ! ( data_page. direction, PaginationOrder :: Descending ) ;
1032
+ assert_eq ! ( data_page. limit, limit) ;
1033
+
1034
+ // Test error case
1035
+ let error = serde_urlencoded:: from_str :: < PaginatedByTimeAndId > (
1036
+ "sort_by=nothing" ,
1037
+ )
1038
+ . unwrap_err ( ) ;
1039
+
1040
+ assert_eq ! (
1041
+ error. to_string( ) ,
1042
+ "unknown variant `nothing`, expected `ascending` or `descending`"
1043
+ ) ;
1044
+ }
874
1045
}
0 commit comments