use-db-core 0.1.0

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

//! Shared database vocabulary for `RustUse`.

use core::{fmt, str::FromStr};
use std::error::Error;

/// Common database storage families.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DatabaseKind {
    /// Relational database systems.
    #[default]
    Relational,
    /// Document database systems.
    Document,
    /// Key-value database systems.
    KeyValue,
    /// Graph database systems.
    Graph,
    /// Search database systems.
    Search,
    /// Time-series database systems.
    TimeSeries,
    /// Column-oriented analytical database systems.
    Columnar,
    /// Object database systems.
    Object,
    /// Other or intentionally unspecified systems.
    Other,
}

impl DatabaseKind {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Relational => "relational",
            Self::Document => "document",
            Self::KeyValue => "key-value",
            Self::Graph => "graph",
            Self::Search => "search",
            Self::TimeSeries => "time-series",
            Self::Columnar => "columnar",
            Self::Object => "object",
            Self::Other => "other",
        }
    }
}

impl fmt::Display for DatabaseKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Common database object kinds.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DatabaseObjectKind {
    /// A database/catalog object.
    Database,
    /// A schema or namespace object.
    Schema,
    /// A relational table-like object.
    #[default]
    Table,
    /// A document collection-like object.
    Collection,
    /// A column or field-like object.
    Column,
    /// An index object.
    Index,
    /// A constraint object.
    Constraint,
    /// A relation or relationship object.
    Relation,
    /// A view-like object.
    View,
    /// A migration object.
    Migration,
    /// Other or intentionally unspecified object.
    Other,
}

impl DatabaseObjectKind {
    /// Returns a stable lowercase label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Database => "database",
            Self::Schema => "schema",
            Self::Table => "table",
            Self::Collection => "collection",
            Self::Column => "column",
            Self::Index => "index",
            Self::Constraint => "constraint",
            Self::Relation => "relation",
            Self::View => "view",
            Self::Migration => "migration",
            Self::Other => "other",
        }
    }
}

impl fmt::Display for DatabaseObjectKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

macro_rules! database_label_type {
    ($type_name:ident, $error_empty:expr) => {
        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
        pub struct $type_name(String);

        impl $type_name {
            /// Creates a database label from non-empty text.
            ///
            /// # Errors
            ///
            /// Returns [`DatabaseError`] when the label is empty or contains control characters.
            pub fn new(input: impl AsRef<str>) -> Result<Self, DatabaseError> {
                validate_label(input.as_ref(), $error_empty).map(|value| Self(value.to_owned()))
            }

            /// Returns the stored label.
            #[must_use]
            pub fn as_str(&self) -> &str {
                &self.0
            }

            /// Consumes the label and returns the owned string.
            #[must_use]
            pub fn into_string(self) -> String {
                self.0
            }
        }

        impl AsRef<str> for $type_name {
            fn as_ref(&self) -> &str {
                self.as_str()
            }
        }

        impl fmt::Display for $type_name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(self.as_str())
            }
        }

        impl FromStr for $type_name {
            type Err = DatabaseError;

            fn from_str(input: &str) -> Result<Self, Self::Err> {
                Self::new(input)
            }
        }

        impl TryFrom<&str> for $type_name {
            type Error = DatabaseError;

            fn try_from(value: &str) -> Result<Self, Self::Error> {
                Self::new(value)
            }
        }
    };
}

database_label_type!(DatabaseEngine, DatabaseError::EmptyEngine);
database_label_type!(DatabaseFeature, DatabaseError::EmptyFeature);
database_label_type!(DatabaseCapability, DatabaseError::EmptyCapability);
database_label_type!(DatabaseVersion, DatabaseError::EmptyVersion);
database_label_type!(DatabaseDialect, DatabaseError::EmptyDialect);

/// Error returned by primitive database vocabulary constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DatabaseError {
    /// A database engine label was empty after trimming.
    EmptyEngine,
    /// A database feature label was empty after trimming.
    EmptyFeature,
    /// A database capability label was empty after trimming.
    EmptyCapability,
    /// A database version label was empty after trimming.
    EmptyVersion,
    /// A database dialect label was empty after trimming.
    EmptyDialect,
    /// A label contained a control character.
    ControlCharacter,
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyEngine => formatter.write_str("database engine label cannot be empty"),
            Self::EmptyFeature => formatter.write_str("database feature label cannot be empty"),
            Self::EmptyCapability => {
                formatter.write_str("database capability label cannot be empty")
            },
            Self::EmptyVersion => formatter.write_str("database version label cannot be empty"),
            Self::EmptyDialect => formatter.write_str("database dialect label cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("database label cannot contain control characters")
            },
        }
    }
}

impl Error for DatabaseError {}

/// Result alias for primitive database vocabulary operations.
pub type DatabaseResult<T> = Result<T, DatabaseError>;

fn validate_label(input: &str, empty_error: DatabaseError) -> Result<&str, DatabaseError> {
    if input.chars().any(char::is_control) {
        return Err(DatabaseError::ControlCharacter);
    }
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(empty_error);
    }
    Ok(trimmed)
}

#[cfg(test)]
mod tests {
    use super::{DatabaseDialect, DatabaseEngine, DatabaseError, DatabaseKind, DatabaseObjectKind};

    #[test]
    fn formats_database_kinds_and_objects() {
        assert_eq!(DatabaseKind::Document.to_string(), "document");
        assert_eq!(DatabaseObjectKind::Collection.to_string(), "collection");
    }

    #[test]
    fn validates_labels() -> Result<(), DatabaseError> {
        let engine = DatabaseEngine::new(" postgres ")?;
        let dialect = DatabaseDialect::new("sql")?;

        assert_eq!(engine.as_str(), "postgres");
        assert_eq!(dialect.to_string(), "sql");
        assert_eq!(DatabaseEngine::new(" "), Err(DatabaseError::EmptyEngine));
        Ok(())
    }
}