sg_index_query/
lib.rs

1//! # sg-index-query
2//!
3//! `sg-index-query` is a Rust crate providing a `QueryOptions` struct for use in CosmWasm smart contracts.
4//! It allows you to specify query parameters such as limit, order, and range.
5//!
6//! ## Features
7//!
8//! - `QueryOptions` struct for specifying query parameters such as limit, order, and range.
9//! - `unpack` function for converting `QueryOptions` into `QueryOptionsInternal`, which is used for range queries in `cw_storage_plus`.
10//!
11//! ## Usage
12//!
13//! First, add the following to your `Cargo.toml`:
14//!
15//! ```toml
16//! [dependencies]
17//! sg-index-query = "0.1.0"
18//! ```
19//!
20//! Then, you can use the `QueryOptions` struct in your code:
21//!
22//! ```rust
23//! use sg_index_query::QueryOptions;
24//!
25//! let query_options = QueryOptions::<String>::default();
26//! ```
27//!
28//! You can specify query parameters like so:
29//!
30//! ```rust
31//! use sg_index_query::{QueryOptions, QueryBound};
32//!
33//! let query_options = QueryOptions {
34//!     descending: Some(true),
35//!     limit: Some(20),
36//!     min: Some(QueryBound::Inclusive("test".to_string())),
37//!     max: Some(QueryBound::Exclusive("test2".to_string())),
38//! };
39//! ```
40//!
41//! Then, you can unpack the `QueryOptions` into `QueryOptionsInternal`:
42//!
43//! ```rust
44//! let query_options_internal = query_options.unpack(&|offset: &String| offset.to_string(), None, None);
45//! ```
46
47use cosmwasm_schema::cw_serde;
48use cosmwasm_std::Order;
49use cw_storage_plus::{Bound, PrimaryKey};
50
51const DEFAULT_QUERY_LIMIT: u32 = 10;
52const MAX_QUERY_LIMIT: u32 = 100;
53
54#[cw_serde]
55pub enum QueryBound<T> {
56    Inclusive(T),
57    Exclusive(T),
58}
59
60/// QueryOptions are used to pass in options to a query function
61#[cw_serde]
62pub struct QueryOptions<T> {
63    /// The number of items that will be returned
64    pub limit: Option<u32>,
65    /// Whether to sort items in ascending or descending order
66    pub descending: Option<bool>,
67    /// The minimum key value to fetch
68    pub min: Option<QueryBound<T>>,
69    /// The maximum key value to fetch
70    pub max: Option<QueryBound<T>>,
71}
72
73impl<T> Default for QueryOptions<T> {
74    fn default() -> Self {
75        QueryOptions {
76            descending: None,
77            min: None,
78            max: None,
79            limit: None,
80        }
81    }
82}
83
84/// QueryOptionsInternal are derived from QueryOptions
85/// using the `unpack` function.
86pub struct QueryOptionsInternal<'a, U: PrimaryKey<'a>> {
87    /// The number of items that will be returned
88    pub limit: usize,
89    /// The [cosmwasm_std::Order] used to sort items in [cw_storage_plus] range queries
90    pub order: Order,
91    /// The [cw_storage_plus::Bound] used to sort items in [cw_storage_plus] range queries
92    pub min: Option<Bound<'a, U>>,
93    /// The [cw_storage_plus::Bound] used to sort items in [cw_storage_plus] range queries
94    pub max: Option<Bound<'a, U>>,
95}
96
97impl<T> QueryOptions<T> {
98    pub fn unpack<'a, U: PrimaryKey<'a>>(
99        &self,
100        offset_to_bound_fn: &dyn Fn(&T) -> U,
101        default_query_limit: Option<u32>,
102        max_query_limit: Option<u32>,
103    ) -> QueryOptionsInternal<'a, U> {
104        let default_query_limit = default_query_limit.unwrap_or(DEFAULT_QUERY_LIMIT);
105        let max_query_limit = max_query_limit.unwrap_or(MAX_QUERY_LIMIT);
106
107        let limit = self
108            .limit
109            .unwrap_or(default_query_limit)
110            .min(max_query_limit) as usize;
111
112        let mut order = Order::Ascending;
113        if let Some(_descending) = self.descending {
114            if _descending {
115                order = Order::Descending;
116            }
117        };
118
119        let min = match &self.min {
120            Some(QueryBound::Inclusive(offset)) => {
121                Some(Bound::inclusive(offset_to_bound_fn(offset)))
122            }
123            Some(QueryBound::Exclusive(offset)) => {
124                Some(Bound::exclusive(offset_to_bound_fn(offset)))
125            }
126            None => None,
127        };
128
129        let max = match &self.max {
130            Some(QueryBound::Inclusive(offset)) => {
131                Some(Bound::inclusive(offset_to_bound_fn(offset)))
132            }
133            Some(QueryBound::Exclusive(offset)) => {
134                Some(Bound::exclusive(offset_to_bound_fn(offset)))
135            }
136            None => None,
137        };
138
139        QueryOptionsInternal {
140            limit,
141            order,
142            min,
143            max,
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150
151    #[test]
152    fn try_unpack_default() {
153        use super::*;
154        let query_options = QueryOptions::<String>::default();
155
156        let query_options_internal =
157            query_options.unpack(&|offset: &String| offset.to_string(), None, None);
158
159        assert_eq!(query_options_internal.limit as u32, DEFAULT_QUERY_LIMIT);
160
161        match query_options_internal.order {
162            Order::Ascending => {}
163            Order::Descending => panic!("Expected Order::Ascending"),
164        }
165        assert!(query_options_internal.min.is_none());
166        assert!(query_options_internal.max.is_none());
167    }
168
169    #[test]
170    fn try_unpack_query_options() {
171        use super::*;
172        let query_options = QueryOptions {
173            descending: Some(true),
174            limit: Some(20),
175            min: Some(QueryBound::Inclusive("test".to_string())),
176            max: Some(QueryBound::Exclusive("test2".to_string())),
177        };
178
179        let query_options_internal =
180            query_options.unpack(&|offset: &String| offset.to_string(), None, None);
181
182        assert_eq!(query_options_internal.limit as u32, 20u32);
183        match query_options_internal.order {
184            Order::Ascending => panic!("Expected Order::Descending"),
185            Order::Descending => {}
186        }
187
188        match query_options_internal.min {
189            Some(Bound::Inclusive(offset)) => {
190                assert_eq!(offset.0, "test".to_string())
191            }
192            _ => panic!("Expected Bound::Inclusive"),
193        }
194
195        match query_options_internal.max {
196            Some(Bound::Exclusive(offset)) => {
197                assert_eq!(offset.0, "test2".to_string())
198            }
199            _ => panic!("Expected Bound::Exclusive"),
200        }
201    }
202}