acme_proxy_admin/webadmin/handlers/
paging.rs1use serde::Deserialize;
9use serde_json::{Value, json};
10
11use acme_proxy_core::config::Config;
12
13const DEFAULT_PAGE_SIZE: i64 = 50;
15
16const MAX_OFFSET: i64 = i64::MAX / 2;
19
20#[derive(Debug, Deserialize, Default)]
22pub struct PageParams {
23 pub limit: Option<i64>,
24 pub offset: Option<i64>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Page {
30 pub limit: i64,
31 pub offset: i64,
32}
33
34impl PageParams {
35 #[must_use]
37 pub fn from(limit: Option<i64>, offset: Option<i64>) -> Self {
38 Self { limit, offset }
39 }
40
41 #[must_use]
55 pub fn resolve(&self, config: &Config) -> Page {
56 let max = config.admin.page_size_max.max(1);
57 Page {
58 limit: self.limit.unwrap_or(DEFAULT_PAGE_SIZE).clamp(1, max),
59 offset: self.offset.unwrap_or(0).clamp(0, MAX_OFFSET),
60 }
61 }
62}
63
64#[must_use]
69pub fn page_envelope(items: Vec<Value>, total: i64, page: Page) -> Value {
70 json!({
71 "items": items,
72 "total": total,
73 "limit": page.limit,
74 "offset": page.offset,
75 })
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 fn config_with_max(max: i64) -> Config {
83 let mut config = Config::default();
84 config.admin.page_size_max = max;
85 config
86 }
87
88 #[test]
89 fn an_absent_window_is_the_default_page() {
90 let page = PageParams::default().resolve(&Config::default());
91 assert_eq!(
92 page,
93 Page {
94 limit: 50,
95 offset: 0
96 }
97 );
98 }
99
100 #[test]
101 fn a_limit_is_clamped_into_range_rather_than_refused() {
102 let config = config_with_max(200);
103 let cases = [
104 (Some(10), 10),
105 (Some(200), 200),
106 (Some(100_000), 200),
108 (Some(0), 1),
110 (Some(-5), 1),
111 ];
112 for (requested, expected) in cases {
113 let page = PageParams {
114 limit: requested,
115 offset: None,
116 }
117 .resolve(&config);
118 assert_eq!(page.limit, expected, "limit={requested:?}");
119 }
120 }
121
122 #[test]
123 fn a_negative_offset_becomes_zero() {
124 let page = PageParams {
125 limit: None,
126 offset: Some(-7),
127 }
128 .resolve(&Config::default());
129 assert_eq!(page.offset, 0);
130 }
131
132 #[test]
136 fn an_offset_at_the_top_of_the_range_is_clamped_below_an_overflow() {
137 let page = PageParams {
138 limit: None,
139 offset: Some(i64::MAX),
140 }
141 .resolve(&Config::default());
142 assert_eq!(page.offset, MAX_OFFSET);
143 assert!(page.offset.checked_add(page.limit).is_some());
144 }
145
146 #[test]
148 fn a_page_size_max_of_zero_still_yields_a_usable_page() {
149 let page = PageParams {
150 limit: Some(50),
151 offset: None,
152 }
153 .resolve(&config_with_max(0));
154 assert_eq!(page.limit, 1);
155 }
156
157 #[test]
158 fn the_envelope_reports_the_window_it_was_given() {
159 let page = Page {
160 limit: 2,
161 offset: 4,
162 };
163 let envelope = page_envelope(vec![json!({"id": "a"})], 17, page);
164 assert_eq!(envelope["total"], 17);
165 assert_eq!(envelope["limit"], 2);
166 assert_eq!(envelope["offset"], 4);
167 assert_eq!(envelope["items"].as_array().unwrap().len(), 1);
168 }
169}