use-db-column 0.1.0

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

//! Column metadata primitives for `RustUse`.

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

use use_db_name::{ColumnName, TableName};

/// A column reference with optional table qualification.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ColumnRef {
    table: Option<TableName>,
    column: ColumnName,
}

impl ColumnRef {
    /// Creates an unqualified column reference.
    #[must_use]
    pub const fn new(column: ColumnName) -> Self {
        Self {
            table: None,
            column,
        }
    }

    /// Creates a table-qualified column reference.
    #[must_use]
    pub const fn qualified(table: TableName, column: ColumnName) -> Self {
        Self {
            table: Some(table),
            column,
        }
    }

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

    /// Returns the column name.
    #[must_use]
    pub const fn column(&self) -> &ColumnName {
        &self.column
    }
}

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

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

column_text_type!(ColumnTypeLabel, ColumnError::EmptyTypeLabel);
column_text_type!(ColumnDefault, ColumnError::EmptyDefault);

/// Column nullability metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Nullability {
    /// Null values are allowed.
    Nullable,
    /// Null values are rejected.
    #[default]
    NotNull,
    /// Nullability is unknown or unspecified.
    Unknown,
}

/// A one-based column ordinal.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ColumnOrdinal(u32);

impl ColumnOrdinal {
    /// Creates a one-based column ordinal.
    #[must_use]
    pub const fn new(value: u32) -> Option<Self> {
        if value == 0 { None } else { Some(Self(value)) }
    }

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

/// Column metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ColumnMetadata {
    reference: ColumnRef,
    type_label: Option<ColumnTypeLabel>,
    default: Option<ColumnDefault>,
    nullability: Nullability,
    ordinal: Option<ColumnOrdinal>,
}

impl ColumnMetadata {
    /// Creates column metadata.
    #[must_use]
    pub const fn new(reference: ColumnRef) -> Self {
        Self {
            reference,
            type_label: None,
            default: None,
            nullability: Nullability::Unknown,
            ordinal: None,
        }
    }

    /// Sets the type label.
    #[must_use]
    pub fn with_type_label(mut self, type_label: ColumnTypeLabel) -> Self {
        self.type_label = Some(type_label);
        self
    }

    /// Sets the default label.
    #[must_use]
    pub fn with_default(mut self, default: ColumnDefault) -> Self {
        self.default = Some(default);
        self
    }

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

    /// Sets the column ordinal.
    #[must_use]
    pub const fn with_ordinal(mut self, ordinal: ColumnOrdinal) -> Self {
        self.ordinal = Some(ordinal);
        self
    }

    /// Returns the column reference.
    #[must_use]
    pub const fn reference(&self) -> &ColumnRef {
        &self.reference
    }

    /// Returns the type label.
    #[must_use]
    pub const fn type_label(&self) -> Option<&ColumnTypeLabel> {
        self.type_label.as_ref()
    }

    /// Returns nullability metadata.
    #[must_use]
    pub const fn nullability(&self) -> Nullability {
        self.nullability
    }
}

/// Error returned by column metadata constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColumnError {
    /// The type label was empty.
    EmptyTypeLabel,
    /// The default label was empty.
    EmptyDefault,
    /// Text contained a control character.
    ControlCharacter,
}

impl fmt::Display for ColumnError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyTypeLabel => formatter.write_str("column type label cannot be empty"),
            Self::EmptyDefault => formatter.write_str("column default label cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("column metadata label cannot contain control characters")
            },
        }
    }
}

impl Error for ColumnError {}

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

#[cfg(test)]
mod tests {
    use super::{
        ColumnError, ColumnMetadata, ColumnOrdinal, ColumnRef, ColumnTypeLabel, Nullability,
    };
    use use_db_name::{ColumnName, TableName};

    #[test]
    fn stores_column_metadata() -> Result<(), Box<dyn std::error::Error>> {
        let reference = ColumnRef::qualified(TableName::new("users")?, ColumnName::new("id")?);
        let metadata = ColumnMetadata::new(reference)
            .with_type_label(ColumnTypeLabel::new("uuid")?)
            .with_nullability(Nullability::NotNull)
            .with_ordinal(ColumnOrdinal::new(1).expect("nonzero ordinal"));

        assert_eq!(
            metadata.reference().table().expect("table").as_str(),
            "users"
        );
        assert_eq!(metadata.type_label().expect("type label").as_str(), "uuid");
        assert_eq!(metadata.nullability(), Nullability::NotNull);
        assert_eq!(ColumnTypeLabel::new(" "), Err(ColumnError::EmptyTypeLabel));
        Ok(())
    }
}