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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgEnumName(PgIdentifier);
13
14impl PgEnumName {
15 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct PgEnumVariant(String);
50
51impl PgEnumVariant {
52 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 #[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#[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 #[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 #[must_use]
103 pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
104 self.schema = Some(schema);
105 self
106 }
107
108 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 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 #[must_use]
134 pub const fn schema(&self) -> Option<&PgSchemaName> {
135 self.schema.as_ref()
136 }
137
138 #[must_use]
140 pub const fn name(&self) -> &PgEnumName {
141 &self.name
142 }
143
144 #[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#[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}