#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_db_name::{ConstraintName, TableName};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ConstraintRef {
name: Option<ConstraintName>,
table: Option<TableName>,
}
impl ConstraintRef {
#[must_use]
pub const fn new(name: Option<ConstraintName>, table: Option<TableName>) -> Self {
Self { name, table }
}
#[must_use]
pub const fn name(&self) -> Option<&ConstraintName> {
self.name.as_ref()
}
#[must_use]
pub const fn table(&self) -> Option<&TableName> {
self.table.as_ref()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConstraintKind {
#[default]
PrimaryKey,
ForeignKey,
Unique,
Check,
NotNull,
Other,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CheckExpressionLabel(String);
impl CheckExpressionLabel {
pub fn new(input: impl AsRef<str>) -> Result<Self, ConstraintError> {
validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CheckExpressionLabel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConstraintStatus {
#[default]
Enabled,
Disabled,
Validated,
NotValidated,
Unknown,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Deferrability {
#[default]
NotDeferrable,
Deferrable,
Unknown,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConstraintMetadata {
reference: ConstraintRef,
kind: ConstraintKind,
check: Option<CheckExpressionLabel>,
status: ConstraintStatus,
deferrability: Deferrability,
}
impl ConstraintMetadata {
#[must_use]
pub const fn new(reference: ConstraintRef, kind: ConstraintKind) -> Self {
Self {
reference,
kind,
check: None,
status: ConstraintStatus::Enabled,
deferrability: Deferrability::NotDeferrable,
}
}
#[must_use]
pub fn with_check(mut self, check: CheckExpressionLabel) -> Self {
self.check = Some(check);
self
}
#[must_use]
pub const fn with_status(mut self, status: ConstraintStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub const fn with_deferrability(mut self, deferrability: Deferrability) -> Self {
self.deferrability = deferrability;
self
}
#[must_use]
pub const fn kind(&self) -> ConstraintKind {
self.kind
}
#[must_use]
pub const fn check(&self) -> Option<&CheckExpressionLabel> {
self.check.as_ref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConstraintError {
Empty,
ControlCharacter,
}
impl fmt::Display for ConstraintError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("constraint label cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("constraint label cannot contain control characters")
},
}
}
}
impl Error for ConstraintError {}
fn validate_text(input: &str) -> Result<&str, ConstraintError> {
if input.chars().any(char::is_control) {
return Err(ConstraintError::ControlCharacter);
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ConstraintError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{
CheckExpressionLabel, ConstraintKind, ConstraintMetadata, ConstraintRef, Deferrability,
};
use use_db_name::{ConstraintName, TableName};
#[test]
fn stores_constraint_metadata() -> Result<(), Box<dyn std::error::Error>> {
let reference = ConstraintRef::new(
Some(ConstraintName::new("users_email_check")?),
Some(TableName::new("users")?),
);
let metadata = ConstraintMetadata::new(reference, ConstraintKind::Check)
.with_check(CheckExpressionLabel::new("email contains @")?)
.with_deferrability(Deferrability::Deferrable);
assert_eq!(metadata.kind(), ConstraintKind::Check);
assert_eq!(
metadata.check().expect("check").as_str(),
"email contains @"
);
Ok(())
}
}