use-pg-enum 0.1.0

PostgreSQL enum type primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

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

use use_pg_identifier::{PgIdentifier, PgIdentifierError};
use use_pg_schema::PgSchemaName;

/// PostgreSQL enum type name primitive.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumName(PgIdentifier);

impl PgEnumName {
    /// Creates an enum type name.
    ///
    /// # Errors
    ///
    /// Returns [`PgEnumError`] when identifier validation fails.
    pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
        PgIdentifier::new(input)
            .map(Self)
            .map_err(PgEnumError::Identifier)
    }

    /// Returns the enum type name text.
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

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

impl FromStr for PgEnumName {
    type Err = PgEnumError;

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

/// PostgreSQL enum variant label.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumVariant(String);

impl PgEnumVariant {
    /// Creates an enum variant label.
    ///
    /// # Errors
    ///
    /// Returns [`PgEnumError`] when the label is empty or contains control characters.
    pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
        validate_variant(input.as_ref()).map(|value| Self(value.to_owned()))
    }

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

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

impl FromStr for PgEnumVariant {
    type Err = PgEnumError;

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

/// PostgreSQL enum type metadata with ordered variants.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumType {
    schema: Option<PgSchemaName>,
    name: PgEnumName,
    variants: Vec<PgEnumVariant>,
}

impl PgEnumType {
    /// Creates enum type metadata from a name.
    #[must_use]
    pub const fn new(name: PgEnumName) -> Self {
        Self {
            schema: None,
            name,
            variants: Vec::new(),
        }
    }

    /// Adds schema qualification.
    #[must_use]
    pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
        self.schema = Some(schema);
        self
    }

    /// Replaces the ordered variant list.
    ///
    /// # Errors
    ///
    /// Returns [`PgEnumError::DuplicateVariant`] when a label appears more than once.
    pub fn with_variants(mut self, variants: Vec<PgEnumVariant>) -> Result<Self, PgEnumError> {
        ensure_unique_variants(&variants)?;
        self.variants = variants;
        Ok(self)
    }

    /// Appends a variant while preserving order.
    ///
    /// # Errors
    ///
    /// Returns [`PgEnumError::DuplicateVariant`] when the label already exists.
    pub fn push_variant(&mut self, variant: PgEnumVariant) -> Result<(), PgEnumError> {
        if self.variants.iter().any(|existing| existing == &variant) {
            return Err(PgEnumError::DuplicateVariant);
        }
        self.variants.push(variant);
        Ok(())
    }

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

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

    /// Returns ordered variants.
    #[must_use]
    pub fn variants(&self) -> &[PgEnumVariant] {
        &self.variants
    }
}

impl fmt::Display for PgEnumType {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(schema) = &self.schema {
            write!(formatter, "{schema}.")?;
        }
        write!(formatter, "{}", self.name)
    }
}

/// Error returned when PostgreSQL enum metadata is invalid.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgEnumError {
    EmptyVariant,
    ControlCharacter,
    DuplicateVariant,
    Identifier(PgIdentifierError),
}

impl fmt::Display for PgEnumError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyVariant => formatter.write_str("PostgreSQL enum variant cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("PostgreSQL enum variant cannot contain control characters")
            }
            Self::DuplicateVariant => {
                formatter.write_str("PostgreSQL enum variants must be unique")
            }
            Self::Identifier(error) => {
                write!(formatter, "invalid PostgreSQL enum identifier: {error}")
            }
        }
    }
}

impl Error for PgEnumError {}

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

fn ensure_unique_variants(variants: &[PgEnumVariant]) -> Result<(), PgEnumError> {
    for (index, variant) in variants.iter().enumerate() {
        if variants[..index].iter().any(|existing| existing == variant) {
            return Err(PgEnumError::DuplicateVariant);
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{PgEnumError, PgEnumName, PgEnumType, PgEnumVariant};
    use use_pg_schema::PgSchemaName;

    #[test]
    fn validates_variant_labels() -> Result<(), PgEnumError> {
        let variant = PgEnumVariant::new("pending")?;
        assert_eq!(variant.as_str(), "pending");
        assert_eq!(PgEnumVariant::new(""), Err(PgEnumError::EmptyVariant));
        Ok(())
    }

    #[test]
    fn preserves_variant_order() -> Result<(), PgEnumError> {
        let enum_type = PgEnumType::new(PgEnumName::new("order_status")?)
            .with_schema(PgSchemaName::public())
            .with_variants(vec![
                PgEnumVariant::new("pending")?,
                PgEnumVariant::new("paid")?,
                PgEnumVariant::new("shipped")?,
            ])?;

        let labels = enum_type
            .variants()
            .iter()
            .map(PgEnumVariant::as_str)
            .collect::<Vec<_>>();
        assert_eq!(labels, vec!["pending", "paid", "shipped"]);
        assert_eq!(enum_type.to_string(), "public.order_status");
        Ok(())
    }

    #[test]
    fn rejects_duplicate_variants() -> Result<(), PgEnumError> {
        let result = PgEnumType::new(PgEnumName::new("status")?).with_variants(vec![
            PgEnumVariant::new("open")?,
            PgEnumVariant::new("open")?,
        ]);
        assert!(matches!(result, Err(PgEnumError::DuplicateVariant)));
        Ok(())
    }
}