use-db-constraint 0.1.0

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

//! Constraint metadata primitives for `RustUse`.

use core::fmt;
use std::error::Error;

use use_db_name::{ConstraintName, TableName};

/// Constraint reference metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ConstraintRef {
    name: Option<ConstraintName>,
    table: Option<TableName>,
}

impl ConstraintRef {
    /// Creates constraint reference metadata.
    #[must_use]
    pub const fn new(name: Option<ConstraintName>, table: Option<TableName>) -> Self {
        Self { name, table }
    }

    /// Returns the optional constraint name.
    #[must_use]
    pub const fn name(&self) -> Option<&ConstraintName> {
        self.name.as_ref()
    }

    /// Returns the optional table name.
    #[must_use]
    pub const fn table(&self) -> Option<&TableName> {
        self.table.as_ref()
    }
}

/// Broad constraint kind.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConstraintKind {
    /// Primary key constraint.
    #[default]
    PrimaryKey,
    /// Foreign key constraint.
    ForeignKey,
    /// Unique constraint.
    Unique,
    /// Check constraint.
    Check,
    /// Not-null constraint.
    NotNull,
    /// Other or unspecified constraint.
    Other,
}

/// A check expression label, not an executable expression.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CheckExpressionLabel(String);

impl CheckExpressionLabel {
    /// Creates a check expression label.
    ///
    /// # Errors
    ///
    /// Returns [`ConstraintError`] when the label is empty or contains control characters.
    pub fn new(input: impl AsRef<str>) -> Result<Self, ConstraintError> {
        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 CheckExpressionLabel {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Constraint status metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConstraintStatus {
    /// Constraint is enabled.
    #[default]
    Enabled,
    /// Constraint is disabled.
    Disabled,
    /// Constraint is validated.
    Validated,
    /// Constraint is not validated.
    NotValidated,
    /// Status is unknown.
    Unknown,
}

/// Deferrability metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Deferrability {
    /// Constraint is not deferrable.
    #[default]
    NotDeferrable,
    /// Constraint is deferrable.
    Deferrable,
    /// Deferrability is unknown.
    Unknown,
}

/// Constraint metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConstraintMetadata {
    reference: ConstraintRef,
    kind: ConstraintKind,
    check: Option<CheckExpressionLabel>,
    status: ConstraintStatus,
    deferrability: Deferrability,
}

impl ConstraintMetadata {
    /// Creates constraint metadata.
    #[must_use]
    pub const fn new(reference: ConstraintRef, kind: ConstraintKind) -> Self {
        Self {
            reference,
            kind,
            check: None,
            status: ConstraintStatus::Enabled,
            deferrability: Deferrability::NotDeferrable,
        }
    }

    /// Adds a check expression label.
    #[must_use]
    pub fn with_check(mut self, check: CheckExpressionLabel) -> Self {
        self.check = Some(check);
        self
    }

    /// Sets constraint status.
    #[must_use]
    pub const fn with_status(mut self, status: ConstraintStatus) -> Self {
        self.status = status;
        self
    }

    /// Sets deferrability metadata.
    #[must_use]
    pub const fn with_deferrability(mut self, deferrability: Deferrability) -> Self {
        self.deferrability = deferrability;
        self
    }

    /// Returns the constraint kind.
    #[must_use]
    pub const fn kind(&self) -> ConstraintKind {
        self.kind
    }

    /// Returns the check expression label.
    #[must_use]
    pub const fn check(&self) -> Option<&CheckExpressionLabel> {
        self.check.as_ref()
    }
}

/// Error returned by constraint metadata constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConstraintError {
    /// Label was empty.
    Empty,
    /// Label contained a control character.
    ControlCharacter,
}

impl fmt::Display for ConstraintError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("constraint label cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("constraint label cannot contain control characters")
            },
        }
    }
}

impl Error for ConstraintError {}

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

#[cfg(test)]
mod tests {
    use super::{
        CheckExpressionLabel, ConstraintKind, ConstraintMetadata, ConstraintRef, Deferrability,
    };
    use use_db_name::{ConstraintName, TableName};

    #[test]
    fn stores_constraint_metadata() -> Result<(), Box<dyn std::error::Error>> {
        let reference = ConstraintRef::new(
            Some(ConstraintName::new("users_email_check")?),
            Some(TableName::new("users")?),
        );
        let metadata = ConstraintMetadata::new(reference, ConstraintKind::Check)
            .with_check(CheckExpressionLabel::new("email contains @")?)
            .with_deferrability(Deferrability::Deferrable);

        assert_eq!(metadata.kind(), ConstraintKind::Check);
        assert_eq!(
            metadata.check().expect("check").as_str(),
            "email contains @"
        );
        Ok(())
    }
}