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