Skip to main content

use_db_constraint/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Constraint metadata primitives for `RustUse`.
5
6use core::fmt;
7use std::error::Error;
8
9use use_db_name::{ConstraintName, TableName};
10
11/// Constraint reference metadata.
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct ConstraintRef {
14    name: Option<ConstraintName>,
15    table: Option<TableName>,
16}
17
18impl ConstraintRef {
19    /// Creates constraint reference metadata.
20    #[must_use]
21    pub const fn new(name: Option<ConstraintName>, table: Option<TableName>) -> Self {
22        Self { name, table }
23    }
24
25    /// Returns the optional constraint name.
26    #[must_use]
27    pub const fn name(&self) -> Option<&ConstraintName> {
28        self.name.as_ref()
29    }
30
31    /// Returns the optional table name.
32    #[must_use]
33    pub const fn table(&self) -> Option<&TableName> {
34        self.table.as_ref()
35    }
36}
37
38/// Broad constraint kind.
39#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub enum ConstraintKind {
41    /// Primary key constraint.
42    #[default]
43    PrimaryKey,
44    /// Foreign key constraint.
45    ForeignKey,
46    /// Unique constraint.
47    Unique,
48    /// Check constraint.
49    Check,
50    /// Not-null constraint.
51    NotNull,
52    /// Other or unspecified constraint.
53    Other,
54}
55
56/// A check expression label, not an executable expression.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub struct CheckExpressionLabel(String);
59
60impl CheckExpressionLabel {
61    /// Creates a check expression label.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`ConstraintError`] when the label is empty or contains control characters.
66    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    /// Returns the stored label.
71    #[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/// Constraint status metadata.
84#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub enum ConstraintStatus {
86    /// Constraint is enabled.
87    #[default]
88    Enabled,
89    /// Constraint is disabled.
90    Disabled,
91    /// Constraint is validated.
92    Validated,
93    /// Constraint is not validated.
94    NotValidated,
95    /// Status is unknown.
96    Unknown,
97}
98
99/// Deferrability metadata.
100#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
101pub enum Deferrability {
102    /// Constraint is not deferrable.
103    #[default]
104    NotDeferrable,
105    /// Constraint is deferrable.
106    Deferrable,
107    /// Deferrability is unknown.
108    Unknown,
109}
110
111/// Constraint metadata.
112#[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    /// Creates constraint metadata.
123    #[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    /// Adds a check expression label.
135    #[must_use]
136    pub fn with_check(mut self, check: CheckExpressionLabel) -> Self {
137        self.check = Some(check);
138        self
139    }
140
141    /// Sets constraint status.
142    #[must_use]
143    pub const fn with_status(mut self, status: ConstraintStatus) -> Self {
144        self.status = status;
145        self
146    }
147
148    /// Sets deferrability metadata.
149    #[must_use]
150    pub const fn with_deferrability(mut self, deferrability: Deferrability) -> Self {
151        self.deferrability = deferrability;
152        self
153    }
154
155    /// Returns the constraint kind.
156    #[must_use]
157    pub const fn kind(&self) -> ConstraintKind {
158        self.kind
159    }
160
161    /// Returns the check expression label.
162    #[must_use]
163    pub const fn check(&self) -> Option<&CheckExpressionLabel> {
164        self.check.as_ref()
165    }
166}
167
168/// Error returned by constraint metadata constructors.
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum ConstraintError {
171    /// Label was empty.
172    Empty,
173    /// Label contained a control character.
174    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}