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            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/// Represents a list query.
22/// List queries list paged, filtered and ordered items.
23#[derive(Debug, Clone, PartialEq)]
24pub struct ListQuery<T: Clone + ToString = FilterPageToken> {
25    /// Page size.
26    pub page_size: i32,
27    /// Page token.
28    pub page_token: Option<T>,
29    /// Filter.
30    pub filter: Filter,
31    /// Ordering.
32    pub ordering: Ordering,
33}
34
35/// Config for list query builder.
36///
37/// `primary_ordering_term` should probably never be `None`.
38/// If you request does not contain an `order_by` field, usage of this function should pre-insert one.
39/// The default ordering term can be primary key of schema item.
40/// If ordering is not specified, then behavior of query's page tokens [`crate::query::page_token::FilterPageToken`] is undefined.
41#[derive(Debug, Clone)]
42pub struct ListQueryConfig {
43    /// Maximum page size.
44    pub max_page_size: Option<i32>,
45    /// Default page size.
46    pub default_page_size: i32,
47    /// Primary ordering term.
48    pub primary_ordering_term: Option<OrderingTerm>,
49    /// Maximum filter length.
50    pub max_filter_length: Option<usize>,
51    /// Maximum ordering length.
52    pub max_ordering_length: Option<usize>,
53}
54
55/// Builder for list queries.
56#[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
64/// Plain list query builder.
65pub type PlainListQueryBuilder = ListQueryBuilder<PlainPageTokenBuilder>;
66/// AES256 list query builder.
67pub type Aes256ListQueryBuilder = ListQueryBuilder<Aes256PageTokenBuilder>;
68/// Base64 list query builder.
69pub type Base64ListQueryBuilder = ListQueryBuilder<Base64PageTokenBuilder>;
70/// RSA list query builder.
71pub type RsaListQueryBuilder = ListQueryBuilder<RsaPageTokenBuilder>;
72
73impl ListQuery {
74    /// Creates salt for page token.
75    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    /// Creates a new list query builder.
94    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    /// Builds a list query.
109    ///
110    /// # Errors
111    ///
112    /// Will return [`QueryError::FilterTooLong`] if filter exceeds maximum length.
113    /// Will return [`QueryError::FilterError`] if filter cannot be parsed or validated.
114    /// Will return [`QueryError::OrderingTooLong`] if ordering exceeds maximum length.
115    /// Will return [`QueryError::OrderingError`] if ordering cannot be parsed or validated.
116    /// Will return [`QueryError::InvalidPageSize`] if page size is negative.
117    /// Will return page token parsing errors from the underlying page token builder.
118    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        // Pre-insert primary ordering term.
135        // This is needed for page tokens to work.
136        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        // Handle paging.
145        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            // Intentionally clamp page size to max page size.
151            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    /// Builds the next page token.
177    ///
178    /// # Errors
179    ///
180    /// Will return page token building errors from the underlying page token builder.
181    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    /// Gets the page token builder.
195    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}