Skip to main content

use_db_query/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Query vocabulary primitives for `RustUse`.
5
6use core::fmt;
7use std::error::Error;
8
9macro_rules! query_text_type {
10    ($type_name:ident) => {
11        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12        pub struct $type_name(String);
13
14        impl $type_name {
15            /// Creates a query metadata label.
16            ///
17            /// # Errors
18            ///
19            /// Returns [`QueryError`] when the label is empty or contains control characters.
20            pub fn new(input: impl AsRef<str>) -> Result<Self, QueryError> {
21                validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
22            }
23
24            /// Returns the stored label.
25            #[must_use]
26            pub fn as_str(&self) -> &str {
27                &self.0
28            }
29        }
30
31        impl fmt::Display for $type_name {
32            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33                formatter.write_str(self.as_str())
34            }
35        }
36    };
37}
38
39query_text_type!(QueryLabel);
40query_text_type!(Cursor);
41query_text_type!(SortKey);
42
43/// Broad query kind.
44#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum QueryKind {
46    /// Read/query operation.
47    #[default]
48    Read,
49    /// Write/mutation operation.
50    Write,
51    /// Schema or metadata operation.
52    Schema,
53    /// Maintenance operation.
54    Maintenance,
55    /// Unknown query kind.
56    Unknown,
57}
58
59/// Query mode metadata.
60#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub enum QueryMode {
62    /// Read-only mode.
63    #[default]
64    ReadOnly,
65    /// Read-write mode.
66    ReadWrite,
67    /// Explain or plan-only mode.
68    Explain,
69    /// Dry-run mode.
70    DryRun,
71}
72
73/// Query timeout in milliseconds.
74#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct QueryTimeout(u64);
76
77impl QueryTimeout {
78    /// Creates a positive query timeout.
79    #[must_use]
80    pub const fn new(milliseconds: u64) -> Option<Self> {
81        if milliseconds == 0 {
82            None
83        } else {
84            Some(Self(milliseconds))
85        }
86    }
87
88    /// Returns milliseconds.
89    #[must_use]
90    pub const fn milliseconds(self) -> u64 {
91        self.0
92    }
93}
94
95/// Limit count metadata.
96#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub struct Limit(usize);
98
99impl Limit {
100    /// Creates a limit value.
101    #[must_use]
102    pub const fn new(value: usize) -> Self {
103        Self(value)
104    }
105
106    /// Returns the limit value.
107    #[must_use]
108    pub const fn value(self) -> usize {
109        self.0
110    }
111}
112
113/// Offset count metadata.
114#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub struct Offset(usize);
116
117impl Offset {
118    /// Creates an offset value.
119    #[must_use]
120    pub const fn new(value: usize) -> Self {
121        Self(value)
122    }
123
124    /// Returns the offset value.
125    #[must_use]
126    pub const fn value(self) -> usize {
127        self.0
128    }
129}
130
131/// Page request metadata.
132#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PageRequest {
134    page: usize,
135    per_page: usize,
136}
137
138impl PageRequest {
139    /// Creates a page request. Values are stored as provided for caller-defined paging schemes.
140    #[must_use]
141    pub const fn new(page: usize, per_page: usize) -> Self {
142        Self { page, per_page }
143    }
144
145    /// Returns the page value.
146    #[must_use]
147    pub const fn page(self) -> usize {
148        self.page
149    }
150
151    /// Returns the page-size value.
152    #[must_use]
153    pub const fn per_page(self) -> usize {
154        self.per_page
155    }
156}
157
158/// Sort direction metadata.
159#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
160pub enum SortDirection {
161    /// Ascending order.
162    #[default]
163    Ascending,
164    /// Descending order.
165    Descending,
166}
167
168impl SortDirection {
169    /// Returns a stable lowercase sort direction label.
170    #[must_use]
171    pub const fn as_str(self) -> &'static str {
172        match self {
173            Self::Ascending => "ascending",
174            Self::Descending => "descending",
175        }
176    }
177}
178
179/// Filter operator vocabulary.
180#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
181pub enum FilterOperator {
182    /// Equality filter.
183    #[default]
184    Equal,
185    /// Inequality filter.
186    NotEqual,
187    /// Less-than filter.
188    LessThan,
189    /// Greater-than filter.
190    GreaterThan,
191    /// Contains-like filter.
192    Contains,
193    /// Prefix-like filter.
194    StartsWith,
195    /// Existence filter.
196    Exists,
197}
198
199/// Projection metadata.
200#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct Projection {
202    fields: Vec<String>,
203}
204
205impl Projection {
206    /// Creates a projection from field labels.
207    #[must_use]
208    pub const fn new(fields: Vec<String>) -> Self {
209        Self { fields }
210    }
211
212    /// Returns projected field labels.
213    #[must_use]
214    pub fn fields(&self) -> &[String] {
215        &self.fields
216    }
217}
218
219/// Error returned by query vocabulary constructors.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum QueryError {
222    /// Label was empty.
223    Empty,
224    /// Label contained a control character.
225    ControlCharacter,
226}
227
228impl fmt::Display for QueryError {
229    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::Empty => formatter.write_str("query label cannot be empty"),
232            Self::ControlCharacter => {
233                formatter.write_str("query label cannot contain control characters")
234            },
235        }
236    }
237}
238
239impl Error for QueryError {}
240
241fn validate_text(input: &str) -> Result<&str, QueryError> {
242    if input.chars().any(char::is_control) {
243        return Err(QueryError::ControlCharacter);
244    }
245    let trimmed = input.trim();
246    if trimmed.is_empty() {
247        return Err(QueryError::Empty);
248    }
249    Ok(trimmed)
250}
251
252#[cfg(test)]
253mod tests {
254    use super::{
255        Cursor, Limit, PageRequest, Projection, QueryError, QueryLabel, QueryTimeout, SortDirection,
256    };
257
258    #[test]
259    fn stores_query_metadata() -> Result<(), QueryError> {
260        let label = QueryLabel::new("list-users")?;
261        let cursor = Cursor::new("abc")?;
262        let page = PageRequest::new(1, 50);
263        let projection = Projection::new(vec!["id".to_owned(), "email".to_owned()]);
264
265        assert_eq!(label.as_str(), "list-users");
266        assert_eq!(cursor.as_str(), "abc");
267        assert_eq!(page.per_page(), 50);
268        assert_eq!(Limit::new(10).value(), 10);
269        assert_eq!(QueryTimeout::new(0), None);
270        assert_eq!(SortDirection::Descending.as_str(), "descending");
271        assert_eq!(projection.fields().len(), 2);
272        Ok(())
273    }
274}