use-db-query 0.1.0

Primitive database query vocabulary for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Query vocabulary primitives for `RustUse`.

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 {
            /// Creates a query metadata label.
            ///
            /// # Errors
            ///
            /// Returns [`QueryError`] when the label is empty or contains control characters.
            pub fn new(input: impl AsRef<str>) -> Result<Self, QueryError> {
                validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
            }

            /// Returns the stored label.
            #[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);

/// Broad query kind.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QueryKind {
    /// Read/query operation.
    #[default]
    Read,
    /// Write/mutation operation.
    Write,
    /// Schema or metadata operation.
    Schema,
    /// Maintenance operation.
    Maintenance,
    /// Unknown query kind.
    Unknown,
}

/// Query mode metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum QueryMode {
    /// Read-only mode.
    #[default]
    ReadOnly,
    /// Read-write mode.
    ReadWrite,
    /// Explain or plan-only mode.
    Explain,
    /// Dry-run mode.
    DryRun,
}

/// Query timeout in milliseconds.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct QueryTimeout(u64);

impl QueryTimeout {
    /// Creates a positive query timeout.
    #[must_use]
    pub const fn new(milliseconds: u64) -> Option<Self> {
        if milliseconds == 0 {
            None
        } else {
            Some(Self(milliseconds))
        }
    }

    /// Returns milliseconds.
    #[must_use]
    pub const fn milliseconds(self) -> u64 {
        self.0
    }
}

/// Limit count metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Limit(usize);

impl Limit {
    /// Creates a limit value.
    #[must_use]
    pub const fn new(value: usize) -> Self {
        Self(value)
    }

    /// Returns the limit value.
    #[must_use]
    pub const fn value(self) -> usize {
        self.0
    }
}

/// Offset count metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Offset(usize);

impl Offset {
    /// Creates an offset value.
    #[must_use]
    pub const fn new(value: usize) -> Self {
        Self(value)
    }

    /// Returns the offset value.
    #[must_use]
    pub const fn value(self) -> usize {
        self.0
    }
}

/// Page request metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PageRequest {
    page: usize,
    per_page: usize,
}

impl PageRequest {
    /// Creates a page request. Values are stored as provided for caller-defined paging schemes.
    #[must_use]
    pub const fn new(page: usize, per_page: usize) -> Self {
        Self { page, per_page }
    }

    /// Returns the page value.
    #[must_use]
    pub const fn page(self) -> usize {
        self.page
    }

    /// Returns the page-size value.
    #[must_use]
    pub const fn per_page(self) -> usize {
        self.per_page
    }
}

/// Sort direction metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SortDirection {
    /// Ascending order.
    #[default]
    Ascending,
    /// Descending order.
    Descending,
}

impl SortDirection {
    /// Returns a stable lowercase sort direction label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Ascending => "ascending",
            Self::Descending => "descending",
        }
    }
}

/// Filter operator vocabulary.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FilterOperator {
    /// Equality filter.
    #[default]
    Equal,
    /// Inequality filter.
    NotEqual,
    /// Less-than filter.
    LessThan,
    /// Greater-than filter.
    GreaterThan,
    /// Contains-like filter.
    Contains,
    /// Prefix-like filter.
    StartsWith,
    /// Existence filter.
    Exists,
}

/// Projection metadata.
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Projection {
    fields: Vec<String>,
}

impl Projection {
    /// Creates a projection from field labels.
    #[must_use]
    pub const fn new(fields: Vec<String>) -> Self {
        Self { fields }
    }

    /// Returns projected field labels.
    #[must_use]
    pub fn fields(&self) -> &[String] {
        &self.fields
    }
}

/// Error returned by query vocabulary constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QueryError {
    /// Label was empty.
    Empty,
    /// Label contained a control character.
    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(())
    }
}