1use crate::{
8 filter::Filter,
9 ordering::{Ordering, OrderingTerm},
10 query::{
11 error::{QueryError, QueryResult},
12 page_token::{
13 FilterPageToken, PageTokenBuilder, aes256::Aes256PageTokenBuilder,
14 base64::Base64PageTokenBuilder, plain::PlainPageTokenBuilder, rsa::RsaPageTokenBuilder,
15 },
16 utility::{parse_query_filter, parse_query_ordering},
17 },
18 schema::{FunctionSchemaMap, Schema, SchemaMapped},
19};
20
21#[derive(Debug, Clone, PartialEq)]
24pub struct ListQuery<T: Clone + ToString = FilterPageToken> {
25 pub page_size: i32,
27 pub page_token: Option<T>,
29 pub filter: Filter,
31 pub ordering: Ordering,
33}
34
35#[derive(Debug, Clone)]
42pub struct ListQueryConfig {
43 pub max_page_size: Option<i32>,
45 pub default_page_size: i32,
47 pub primary_ordering_term: Option<OrderingTerm>,
49 pub max_filter_length: Option<usize>,
51 pub max_ordering_length: Option<usize>,
53}
54
55#[derive(Debug, Clone)]
57pub struct ListQueryBuilder<P: PageTokenBuilder> {
58 schema: Schema,
59 schema_functions: FunctionSchemaMap,
60 options: ListQueryConfig,
61 page_token_builder: P,
62}
63
64pub type PlainListQueryBuilder = ListQueryBuilder<PlainPageTokenBuilder>;
66pub type Aes256ListQueryBuilder = ListQueryBuilder<Aes256PageTokenBuilder>;
68pub type Base64ListQueryBuilder = ListQueryBuilder<Base64PageTokenBuilder>;
70pub type RsaListQueryBuilder = ListQueryBuilder<RsaPageTokenBuilder>;
72
73impl ListQuery {
74 pub fn make_salt(page_size: i32) -> Vec<u8> {
76 page_size.to_be_bytes().to_vec()
77 }
78}
79
80impl Default for ListQueryConfig {
81 fn default() -> Self {
82 Self {
83 max_page_size: None,
84 default_page_size: 20,
85 primary_ordering_term: None,
86 max_filter_length: None,
87 max_ordering_length: None,
88 }
89 }
90}
91
92impl<P: PageTokenBuilder> ListQueryBuilder<P> {
93 pub const fn new(
95 schema: Schema,
96 schema_functions: FunctionSchemaMap,
97 options: ListQueryConfig,
98 page_token_builder: P,
99 ) -> Self {
100 Self {
101 schema,
102 schema_functions,
103 options,
104 page_token_builder,
105 }
106 }
107
108 pub fn build(
119 &self,
120 page_size: Option<i32>,
121 page_token: Option<&str>,
122 filter: Option<&str>,
123 ordering: Option<&str>,
124 ) -> QueryResult<ListQuery<P::PageToken>> {
125 let filter = parse_query_filter(
126 filter,
127 &self.schema,
128 Some(&self.schema_functions),
129 self.options.max_filter_length,
130 )?;
131 let mut ordering =
132 parse_query_ordering(ordering, &self.schema, self.options.max_ordering_length)?;
133
134 if let Some(primary_ordering_term) = self.options.primary_ordering_term.as_ref()
137 && ordering
138 .iter()
139 .all(|term| term.name != primary_ordering_term.name)
140 {
141 ordering.insert(0, primary_ordering_term.clone());
142 }
143
144 let mut page_size = page_size.unwrap_or(self.options.default_page_size);
146 if page_size < 0 {
147 return Err(QueryError::InvalidPageSize);
148 }
149 if let Some(max_page_size) = self.options.max_page_size {
150 if page_size > max_page_size {
152 page_size = max_page_size;
153 }
154 }
155
156 let page_token =
157 if let Some(page_token) = page_token.filter(|page_token| !page_token.is_empty()) {
158 Some(self.page_token_builder.parse(
159 &filter,
160 &ordering,
161 &ListQuery::make_salt(page_size),
162 page_token,
163 )?)
164 } else {
165 None
166 };
167
168 Ok(ListQuery {
169 page_size,
170 page_token,
171 filter,
172 ordering,
173 })
174 }
175
176 pub fn build_next_page_token<T: SchemaMapped>(
182 &self,
183 query: &ListQuery<P::PageToken>,
184 next_item: &T,
185 ) -> QueryResult<String> {
186 self.page_token_builder.build_next(
187 &query.filter,
188 &query.ordering,
189 &ListQuery::make_salt(query.page_size),
190 next_item,
191 )
192 }
193
194 pub const fn page_token_builder(&self) -> &P {
196 &self.page_token_builder
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use crate::{
203 filter::error::FilterError,
204 ordering::{OrderingDirection, error::OrderingError},
205 query::page_token::plain::PlainPageTokenBuilder,
206 testing::schema::UserItem,
207 };
208
209 use super::*;
210
211 #[test]
212 fn it_works() {
213 let qb = get_query_builder();
214 let query = qb
215 .build(
216 Some(10_000),
217 None,
218 Some("displayName = \"John\""),
219 Some("age desc"),
220 )
221 .unwrap();
222 assert_eq!(query.page_size, 20);
223 assert_eq!(query.filter.to_string(), "displayName = \"John\"");
224 assert_eq!(query.ordering.to_string(), "id desc, age desc");
225 }
226
227 #[test]
228 fn errors() {
229 let q = get_query_builder();
230 assert!(matches!(
231 q.build(Some(-1), None, None, None),
232 Err(QueryError::InvalidPageSize)
233 ));
234 assert!(matches!(
235 q.build(Some(-1), None, None, None),
236 Err(QueryError::InvalidPageSize)
237 ));
238 assert!(matches!(
239 q.build(None, None, Some("f!"), None).unwrap_err(),
240 QueryError::FilterError(FilterError::Parse { start, end })
241 if start == 1 && end == 1
242 ));
243 assert_eq!(
244 q.build(None, None, Some(&("a".repeat(100))), None)
245 .unwrap_err(),
246 QueryError::FilterTooLong
247 );
248 assert_eq!(
249 q.build(None, None, Some("lol"), None).unwrap_err(),
250 QueryError::FilterError(FilterError::UnknownMember("lol".into()))
251 );
252 assert_eq!(
253 q.build(None, None, None, Some(&("a".repeat(100))))
254 .unwrap_err(),
255 QueryError::OrderingTooLong
256 );
257 assert_eq!(
258 q.build(None, None, None, Some("lol")).unwrap_err(),
259 QueryError::OrderingError(OrderingError::UnknownMember("lol".into()))
260 );
261 }
262
263 #[test]
264 fn page_tokens() {
265 let qb = get_query_builder();
266 let last_item: UserItem = UserItem {
267 id: "1337".into(),
268 display_name: "John".into(),
269 age: 14000,
270 };
271
272 macro_rules! assert_page_token {
273 ($filter1:expr, $ordering1:expr, $filter2:expr, $ordering2:expr, $expected_token:expr $(,)?) => {{
274 let first_page = qb.build(Some(3), None, $filter1, $ordering1).unwrap();
275 let next_page_token = qb
276 .page_token_builder
277 .build_next(&first_page.filter, &first_page.ordering, &[], &last_item)
278 .unwrap();
279 let next_page: ListQuery = qb
280 .build(Some(3), Some(&next_page_token), $filter2, $ordering2)
281 .unwrap();
282 assert_eq!(
283 next_page.page_token.unwrap().filter.to_string(),
284 $expected_token
285 );
286 }};
287 }
288
289 assert_page_token!(
290 Some(r#"displayName = "John""#),
291 None,
292 Some(r#"displayName = "John""#),
293 None,
294 r#"id <= "1337""#,
295 );
296 assert_page_token!(
297 None,
298 Some("id desc, age desc"),
299 None,
300 Some("id desc, age desc"),
301 r#"id <= "1337" AND age <= 14000"#,
302 );
303 assert_page_token!(
304 None,
305 Some("id desc, age asc"),
306 None,
307 Some("id desc, age desc"),
308 r#"id <= "1337" AND age >= 14000"#,
309 );
310 }
311
312 fn get_query_builder() -> ListQueryBuilder<PlainPageTokenBuilder> {
313 ListQueryBuilder::<PlainPageTokenBuilder>::new(
314 UserItem::get_schema(),
315 FunctionSchemaMap::new(),
316 ListQueryConfig {
317 max_page_size: Some(20),
318 default_page_size: 10,
319 primary_ordering_term: Some(OrderingTerm {
320 name: "id".into(),
321 direction: OrderingDirection::Descending,
322 }),
323 max_filter_length: Some(50),
324 max_ordering_length: Some(50),
325 },
326 PlainPageTokenBuilder {},
327 )
328 }
329}