#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_db_name::RelationName;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RelationRef {
name: Option<RelationName>,
}
impl RelationRef {
#[must_use]
pub const fn new(name: Option<RelationName>) -> Self {
Self { name }
}
#[must_use]
pub const fn name(&self) -> Option<&RelationName> {
self.name.as_ref()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RelationKind {
ParentChild,
#[default]
Reference,
Ownership,
Membership,
Other,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Cardinality {
OneToOne,
#[default]
OneToMany,
ManyToOne,
ManyToMany,
}
impl Cardinality {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::OneToOne => "one-to-one",
Self::OneToMany => "one-to-many",
Self::ManyToOne => "many-to-one",
Self::ManyToMany => "many-to-many",
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RelationEndpoint {
label: String,
required: bool,
}
impl RelationEndpoint {
pub fn new(label: impl AsRef<str>, required: bool) -> Result<Self, RelationError> {
validate_text(label.as_ref()).map(|value| Self {
label: value.to_owned(),
required,
})
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub const fn is_required(&self) -> bool {
self.required
}
#[must_use]
pub const fn is_optional(&self) -> bool {
!self.required
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Relationship {
reference: RelationRef,
kind: RelationKind,
cardinality: Cardinality,
endpoints: Vec<RelationEndpoint>,
}
impl Relationship {
#[must_use]
pub const fn new(reference: RelationRef, kind: RelationKind, cardinality: Cardinality) -> Self {
Self {
reference,
kind,
cardinality,
endpoints: Vec::new(),
}
}
#[must_use]
pub fn with_endpoints(mut self, endpoints: Vec<RelationEndpoint>) -> Self {
self.endpoints = endpoints;
self
}
#[must_use]
pub const fn reference(&self) -> &RelationRef {
&self.reference
}
#[must_use]
pub const fn kind(&self) -> RelationKind {
self.kind
}
#[must_use]
pub const fn cardinality(&self) -> Cardinality {
self.cardinality
}
#[must_use]
pub fn endpoints(&self) -> &[RelationEndpoint] {
&self.endpoints
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RelationError {
Empty,
ControlCharacter,
}
impl fmt::Display for RelationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("relation label cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("relation label cannot contain control characters")
},
}
}
}
impl Error for RelationError {}
fn validate_text(input: &str) -> Result<&str, RelationError> {
if input.chars().any(char::is_control) {
return Err(RelationError::ControlCharacter);
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(RelationError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{Cardinality, RelationEndpoint, RelationKind, RelationRef, Relationship};
use use_db_name::RelationName;
#[test]
fn stores_relationship_metadata() -> Result<(), Box<dyn std::error::Error>> {
let relationship = Relationship::new(
RelationRef::new(Some(RelationName::new("user_posts")?)),
RelationKind::ParentChild,
Cardinality::OneToMany,
)
.with_endpoints(vec![
RelationEndpoint::new("users", true)?,
RelationEndpoint::new("posts", false)?,
]);
assert_eq!(relationship.cardinality().as_str(), "one-to-many");
assert!(relationship.endpoints()[0].is_required());
assert!(relationship.endpoints()[1].is_optional());
Ok(())
}
}