#![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;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumName(PgIdentifier);
impl PgEnumName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgEnumError::Identifier)
}
#[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)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumVariant(String);
impl PgEnumVariant {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
validate_variant(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[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)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgEnumType {
schema: Option<PgSchemaName>,
name: PgEnumName,
variants: Vec<PgEnumVariant>,
}
impl PgEnumType {
#[must_use]
pub const fn new(name: PgEnumName) -> Self {
Self {
schema: None,
name,
variants: Vec::new(),
}
}
#[must_use]
pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
self.schema = Some(schema);
self
}
pub fn with_variants(mut self, variants: Vec<PgEnumVariant>) -> Result<Self, PgEnumError> {
ensure_unique_variants(&variants)?;
self.variants = variants;
Ok(self)
}
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(())
}
#[must_use]
pub const fn schema(&self) -> Option<&PgSchemaName> {
self.schema.as_ref()
}
#[must_use]
pub const fn name(&self) -> &PgEnumName {
&self.name
}
#[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)
}
}
#[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(())
}
}