#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
macro_rules! query_text_type {
($type_name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $type_name(String);
impl $type_name {
pub fn new(input: impl AsRef<str>) -> Result<Self, QueryError> {
validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $type_name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
query_text_type!(QueryLabel);
query_text_type!(Cursor);
query_text_type!(SortKey);
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QueryKind {
#[default]
Read,
Write,
Schema,
Maintenance,
Unknown,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QueryMode {
#[default]
ReadOnly,
ReadWrite,
Explain,
DryRun,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct QueryTimeout(u64);
impl QueryTimeout {
#[must_use]
pub const fn new(milliseconds: u64) -> Option<Self> {
if milliseconds == 0 {
None
} else {
Some(Self(milliseconds))
}
}
#[must_use]
pub const fn milliseconds(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Limit(usize);
impl Limit {
#[must_use]
pub const fn new(value: usize) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> usize {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Offset(usize);
impl Offset {
#[must_use]
pub const fn new(value: usize) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> usize {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PageRequest {
page: usize,
per_page: usize,
}
impl PageRequest {
#[must_use]
pub const fn new(page: usize, per_page: usize) -> Self {
Self { page, per_page }
}
#[must_use]
pub const fn page(self) -> usize {
self.page
}
#[must_use]
pub const fn per_page(self) -> usize {
self.per_page
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SortDirection {
#[default]
Ascending,
Descending,
}
impl SortDirection {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ascending => "ascending",
Self::Descending => "descending",
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FilterOperator {
#[default]
Equal,
NotEqual,
LessThan,
GreaterThan,
Contains,
StartsWith,
Exists,
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Projection {
fields: Vec<String>,
}
impl Projection {
#[must_use]
pub const fn new(fields: Vec<String>) -> Self {
Self { fields }
}
#[must_use]
pub fn fields(&self) -> &[String] {
&self.fields
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryError {
Empty,
ControlCharacter,
}
impl fmt::Display for QueryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("query label cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("query label cannot contain control characters")
},
}
}
}
impl Error for QueryError {}
fn validate_text(input: &str) -> Result<&str, QueryError> {
if input.chars().any(char::is_control) {
return Err(QueryError::ControlCharacter);
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(QueryError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{
Cursor, Limit, PageRequest, Projection, QueryError, QueryLabel, QueryTimeout, SortDirection,
};
#[test]
fn stores_query_metadata() -> Result<(), QueryError> {
let label = QueryLabel::new("list-users")?;
let cursor = Cursor::new("abc")?;
let page = PageRequest::new(1, 50);
let projection = Projection::new(vec!["id".to_owned(), "email".to_owned()]);
assert_eq!(label.as_str(), "list-users");
assert_eq!(cursor.as_str(), "abc");
assert_eq!(page.per_page(), 50);
assert_eq!(Limit::new(10).value(), 10);
assert_eq!(QueryTimeout::new(0), None);
assert_eq!(SortDirection::Descending.as_str(), "descending");
assert_eq!(projection.fields().len(), 2);
Ok(())
}
}