1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use 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 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 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum QueryKind {
46 #[default]
48 Read,
49 Write,
51 Schema,
53 Maintenance,
55 Unknown,
57}
58
59#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub enum QueryMode {
62 #[default]
64 ReadOnly,
65 ReadWrite,
67 Explain,
69 DryRun,
71}
72
73#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct QueryTimeout(u64);
76
77impl QueryTimeout {
78 #[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 #[must_use]
90 pub const fn milliseconds(self) -> u64 {
91 self.0
92 }
93}
94
95#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub struct Limit(usize);
98
99impl Limit {
100 #[must_use]
102 pub const fn new(value: usize) -> Self {
103 Self(value)
104 }
105
106 #[must_use]
108 pub const fn value(self) -> usize {
109 self.0
110 }
111}
112
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub struct Offset(usize);
116
117impl Offset {
118 #[must_use]
120 pub const fn new(value: usize) -> Self {
121 Self(value)
122 }
123
124 #[must_use]
126 pub const fn value(self) -> usize {
127 self.0
128 }
129}
130
131#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PageRequest {
134 page: usize,
135 per_page: usize,
136}
137
138impl PageRequest {
139 #[must_use]
141 pub const fn new(page: usize, per_page: usize) -> Self {
142 Self { page, per_page }
143 }
144
145 #[must_use]
147 pub const fn page(self) -> usize {
148 self.page
149 }
150
151 #[must_use]
153 pub const fn per_page(self) -> usize {
154 self.per_page
155 }
156}
157
158#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
160pub enum SortDirection {
161 #[default]
163 Ascending,
164 Descending,
166}
167
168impl SortDirection {
169 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
181pub enum FilterOperator {
182 #[default]
184 Equal,
185 NotEqual,
187 LessThan,
189 GreaterThan,
191 Contains,
193 StartsWith,
195 Exists,
197}
198
199#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct Projection {
202 fields: Vec<String>,
203}
204
205impl Projection {
206 #[must_use]
208 pub const fn new(fields: Vec<String>) -> Self {
209 Self { fields }
210 }
211
212 #[must_use]
214 pub fn fields(&self) -> &[String] {
215 &self.fields
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum QueryError {
222 Empty,
224 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}