Skip to main content

use_pg_enum/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_pg_identifier::{PgIdentifier, PgIdentifierError};
8use use_pg_schema::PgSchemaName;
9
10/// PostgreSQL enum type name primitive.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgEnumName(PgIdentifier);
13
14impl PgEnumName {
15    /// Creates an enum type name.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`PgEnumError`] when identifier validation fails.
20    pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
21        PgIdentifier::new(input)
22            .map(Self)
23            .map_err(PgEnumError::Identifier)
24    }
25
26    /// Returns the enum type name text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl fmt::Display for PgEnumName {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        self.0.fmt(formatter)
36    }
37}
38
39impl FromStr for PgEnumName {
40    type Err = PgEnumError;
41
42    fn from_str(input: &str) -> Result<Self, Self::Err> {
43        Self::new(input)
44    }
45}
46
47/// PostgreSQL enum variant label.
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct PgEnumVariant(String);
50
51impl PgEnumVariant {
52    /// Creates an enum variant label.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`PgEnumError`] when the label is empty or contains control characters.
57    pub fn new(input: impl AsRef<str>) -> Result<Self, PgEnumError> {
58        validate_variant(input.as_ref()).map(|value| Self(value.to_owned()))
59    }
60
61    /// Returns the variant label.
62    #[must_use]
63    pub fn as_str(&self) -> &str {
64        &self.0
65    }
66}
67
68impl fmt::Display for PgEnumVariant {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74impl FromStr for PgEnumVariant {
75    type Err = PgEnumError;
76
77    fn from_str(input: &str) -> Result<Self, Self::Err> {
78        Self::new(input)
79    }
80}
81
82/// PostgreSQL enum type metadata with ordered variants.
83#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub struct PgEnumType {
85    schema: Option<PgSchemaName>,
86    name: PgEnumName,
87    variants: Vec<PgEnumVariant>,
88}
89
90impl PgEnumType {
91    /// Creates enum type metadata from a name.
92    #[must_use]
93    pub const fn new(name: PgEnumName) -> Self {
94        Self {
95            schema: None,
96            name,
97            variants: Vec::new(),
98        }
99    }
100
101    /// Adds schema qualification.
102    #[must_use]
103    pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
104        self.schema = Some(schema);
105        self
106    }
107
108    /// Replaces the ordered variant list.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`PgEnumError::DuplicateVariant`] when a label appears more than once.
113    pub fn with_variants(mut self, variants: Vec<PgEnumVariant>) -> Result<Self, PgEnumError> {
114        ensure_unique_variants(&variants)?;
115        self.variants = variants;
116        Ok(self)
117    }
118
119    /// Appends a variant while preserving order.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`PgEnumError::DuplicateVariant`] when the label already exists.
124    pub fn push_variant(&mut self, variant: PgEnumVariant) -> Result<(), PgEnumError> {
125        if self.variants.iter().any(|existing| existing == &variant) {
126            return Err(PgEnumError::DuplicateVariant);
127        }
128        self.variants.push(variant);
129        Ok(())
130    }
131
132    /// Returns the optional schema name.
133    #[must_use]
134    pub const fn schema(&self) -> Option<&PgSchemaName> {
135        self.schema.as_ref()
136    }
137
138    /// Returns the enum type name.
139    #[must_use]
140    pub const fn name(&self) -> &PgEnumName {
141        &self.name
142    }
143
144    /// Returns ordered variants.
145    #[must_use]
146    pub fn variants(&self) -> &[PgEnumVariant] {
147        &self.variants
148    }
149}
150
151impl fmt::Display for PgEnumType {
152    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153        if let Some(schema) = &self.schema {
154            write!(formatter, "{schema}.")?;
155        }
156        write!(formatter, "{}", self.name)
157    }
158}
159
160/// Error returned when PostgreSQL enum metadata is invalid.
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub enum PgEnumError {
163    EmptyVariant,
164    ControlCharacter,
165    DuplicateVariant,
166    Identifier(PgIdentifierError),
167}
168
169impl fmt::Display for PgEnumError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::EmptyVariant => formatter.write_str("PostgreSQL enum variant cannot be empty"),
173            Self::ControlCharacter => {
174                formatter.write_str("PostgreSQL enum variant cannot contain control characters")
175            }
176            Self::DuplicateVariant => {
177                formatter.write_str("PostgreSQL enum variants must be unique")
178            }
179            Self::Identifier(error) => {
180                write!(formatter, "invalid PostgreSQL enum identifier: {error}")
181            }
182        }
183    }
184}
185
186impl Error for PgEnumError {}
187
188fn validate_variant(input: &str) -> Result<&str, PgEnumError> {
189    let trimmed = input.trim();
190    if trimmed.is_empty() {
191        return Err(PgEnumError::EmptyVariant);
192    }
193    if trimmed.chars().any(char::is_control) {
194        return Err(PgEnumError::ControlCharacter);
195    }
196    Ok(trimmed)
197}
198
199fn ensure_unique_variants(variants: &[PgEnumVariant]) -> Result<(), PgEnumError> {
200    for (index, variant) in variants.iter().enumerate() {
201        if variants[..index].iter().any(|existing| existing == variant) {
202            return Err(PgEnumError::DuplicateVariant);
203        }
204    }
205    Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{PgEnumError, PgEnumName, PgEnumType, PgEnumVariant};
211    use use_pg_schema::PgSchemaName;
212
213    #[test]
214    fn validates_variant_labels() -> Result<(), PgEnumError> {
215        let variant = PgEnumVariant::new("pending")?;
216        assert_eq!(variant.as_str(), "pending");
217        assert_eq!(PgEnumVariant::new(""), Err(PgEnumError::EmptyVariant));
218        Ok(())
219    }
220
221    #[test]
222    fn preserves_variant_order() -> Result<(), PgEnumError> {
223        let enum_type = PgEnumType::new(PgEnumName::new("order_status")?)
224            .with_schema(PgSchemaName::public())
225            .with_variants(vec![
226                PgEnumVariant::new("pending")?,
227                PgEnumVariant::new("paid")?,
228                PgEnumVariant::new("shipped")?,
229            ])?;
230
231        let labels = enum_type
232            .variants()
233            .iter()
234            .map(PgEnumVariant::as_str)
235            .collect::<Vec<_>>();
236        assert_eq!(labels, vec!["pending", "paid", "shipped"]);
237        assert_eq!(enum_type.to_string(), "public.order_status");
238        Ok(())
239    }
240
241    #[test]
242    fn rejects_duplicate_variants() -> Result<(), PgEnumError> {
243        let result = PgEnumType::new(PgEnumName::new("status")?).with_variants(vec![
244            PgEnumVariant::new("open")?,
245            PgEnumVariant::new("open")?,
246        ]);
247        assert!(matches!(result, Err(PgEnumError::DuplicateVariant)));
248        Ok(())
249    }
250}