1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7use std::error::Error;
8
9use use_db_name::{ConstraintName, TableName};
10
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct ConstraintRef {
14 name: Option<ConstraintName>,
15 table: Option<TableName>,
16}
17
18impl ConstraintRef {
19 #[must_use]
21 pub const fn new(name: Option<ConstraintName>, table: Option<TableName>) -> Self {
22 Self { name, table }
23 }
24
25 #[must_use]
27 pub const fn name(&self) -> Option<&ConstraintName> {
28 self.name.as_ref()
29 }
30
31 #[must_use]
33 pub const fn table(&self) -> Option<&TableName> {
34 self.table.as_ref()
35 }
36}
37
38#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub enum ConstraintKind {
41 #[default]
43 PrimaryKey,
44 ForeignKey,
46 Unique,
48 Check,
50 NotNull,
52 Other,
54}
55
56#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub struct CheckExpressionLabel(String);
59
60impl CheckExpressionLabel {
61 pub fn new(input: impl AsRef<str>) -> Result<Self, ConstraintError> {
67 validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
68 }
69
70 #[must_use]
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Display for CheckExpressionLabel {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter.write_str(self.as_str())
80 }
81}
82
83#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub enum ConstraintStatus {
86 #[default]
88 Enabled,
89 Disabled,
91 Validated,
93 NotValidated,
95 Unknown,
97}
98
99#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub enum Deferrability {
102 #[default]
104 NotDeferrable,
105 Deferrable,
107 Unknown,
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct ConstraintMetadata {
114 reference: ConstraintRef,
115 kind: ConstraintKind,
116 check: Option<CheckExpressionLabel>,
117 status: ConstraintStatus,
118 deferrability: Deferrability,
119}
120
121impl ConstraintMetadata {
122 #[must_use]
124 pub const fn new(reference: ConstraintRef, kind: ConstraintKind) -> Self {
125 Self {
126 reference,
127 kind,
128 check: None,
129 status: ConstraintStatus::Enabled,
130 deferrability: Deferrability::NotDeferrable,
131 }
132 }
133
134 #[must_use]
136 pub fn with_check(mut self, check: CheckExpressionLabel) -> Self {
137 self.check = Some(check);
138 self
139 }
140
141 #[must_use]
143 pub const fn with_status(mut self, status: ConstraintStatus) -> Self {
144 self.status = status;
145 self
146 }
147
148 #[must_use]
150 pub const fn with_deferrability(mut self, deferrability: Deferrability) -> Self {
151 self.deferrability = deferrability;
152 self
153 }
154
155 #[must_use]
157 pub const fn kind(&self) -> ConstraintKind {
158 self.kind
159 }
160
161 #[must_use]
163 pub const fn check(&self) -> Option<&CheckExpressionLabel> {
164 self.check.as_ref()
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum ConstraintError {
171 Empty,
173 ControlCharacter,
175}
176
177impl fmt::Display for ConstraintError {
178 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 Self::Empty => formatter.write_str("constraint label cannot be empty"),
181 Self::ControlCharacter => {
182 formatter.write_str("constraint label cannot contain control characters")
183 },
184 }
185 }
186}
187
188impl Error for ConstraintError {}
189
190fn validate_text(input: &str) -> Result<&str, ConstraintError> {
191 if input.chars().any(char::is_control) {
192 return Err(ConstraintError::ControlCharacter);
193 }
194 let trimmed = input.trim();
195 if trimmed.is_empty() {
196 return Err(ConstraintError::Empty);
197 }
198 Ok(trimmed)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::{
204 CheckExpressionLabel, ConstraintKind, ConstraintMetadata, ConstraintRef, Deferrability,
205 };
206 use use_db_name::{ConstraintName, TableName};
207
208 #[test]
209 fn stores_constraint_metadata() -> Result<(), Box<dyn std::error::Error>> {
210 let reference = ConstraintRef::new(
211 Some(ConstraintName::new("users_email_check")?),
212 Some(TableName::new("users")?),
213 );
214 let metadata = ConstraintMetadata::new(reference, ConstraintKind::Check)
215 .with_check(CheckExpressionLabel::new("email contains @")?)
216 .with_deferrability(Deferrability::Deferrable);
217
218 assert_eq!(metadata.kind(), ConstraintKind::Check);
219 assert_eq!(
220 metadata.check().expect("check").as_str(),
221 "email contains @"
222 );
223 Ok(())
224 }
225}