bomboni_request/query/
list.rs

1//! # List query.
2//!
3//! Utility for working with Google AIP standard List method [1].
4//!
5//! [1]: https://google.aip.dev/132
6
7use 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/// Represents a list query.
23/// List queries list paged, filtered and ordered items.
24#[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/// Config for list query builder.
33///
34/// `primary_ordering_term` should probably never be `None`.
35/// If the request does not contain an `order_by` field, usage of this function should pre-insert one.
36/// The default ordering term can the primary key of the schema item.
37/// If ordering is not specified, then behavior of query's page tokens [`PageToken`] is undefined.
38#[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        // Pre-insert primary ordering term.
110        // This is needed for page tokens to work.
111        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        // Handle paging.
121        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            // Intentionally clamp page size to max page size.
127            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}