use-db-relation 0.1.0

Primitive database relation metadata for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Relation and cardinality primitives for `RustUse`.

use core::fmt;
use std::error::Error;

use use_db_name::RelationName;

/// Relation reference metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RelationRef {
    name: Option<RelationName>,
}

impl RelationRef {
    /// Creates relation reference metadata.
    #[must_use]
    pub const fn new(name: Option<RelationName>) -> Self {
        Self { name }
    }

    /// Returns the optional relation name.
    #[must_use]
    pub const fn name(&self) -> Option<&RelationName> {
        self.name.as_ref()
    }
}

/// Broad relation kind.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RelationKind {
    /// Parent-child relationship.
    ParentChild,
    /// Reference relationship.
    #[default]
    Reference,
    /// Ownership relationship.
    Ownership,
    /// Membership relationship.
    Membership,
    /// Other or unspecified relationship.
    Other,
}

/// Common cardinality labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Cardinality {
    /// One-to-one relation.
    OneToOne,
    /// One-to-many relation.
    #[default]
    OneToMany,
    /// Many-to-one relation.
    ManyToOne,
    /// Many-to-many relation.
    ManyToMany,
}

impl Cardinality {
    /// Returns a stable cardinality label.
    #[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",
        }
    }
}

/// A relation endpoint label with optionality metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RelationEndpoint {
    label: String,
    required: bool,
}

impl RelationEndpoint {
    /// Creates a relation endpoint label.
    ///
    /// # Errors
    ///
    /// Returns [`RelationError`] when the label is empty or contains control characters.
    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,
        })
    }

    /// Returns the endpoint label.
    #[must_use]
    pub fn label(&self) -> &str {
        &self.label
    }

    /// Returns whether this endpoint is required.
    #[must_use]
    pub const fn is_required(&self) -> bool {
        self.required
    }

    /// Returns whether this endpoint is optional.
    #[must_use]
    pub const fn is_optional(&self) -> bool {
        !self.required
    }
}

/// Relationship metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Relationship {
    reference: RelationRef,
    kind: RelationKind,
    cardinality: Cardinality,
    endpoints: Vec<RelationEndpoint>,
}

impl Relationship {
    /// Creates relationship metadata.
    #[must_use]
    pub const fn new(reference: RelationRef, kind: RelationKind, cardinality: Cardinality) -> Self {
        Self {
            reference,
            kind,
            cardinality,
            endpoints: Vec::new(),
        }
    }

    /// Sets relation endpoints.
    #[must_use]
    pub fn with_endpoints(mut self, endpoints: Vec<RelationEndpoint>) -> Self {
        self.endpoints = endpoints;
        self
    }

    /// Returns the relation reference.
    #[must_use]
    pub const fn reference(&self) -> &RelationRef {
        &self.reference
    }

    /// Returns the relation kind.
    #[must_use]
    pub const fn kind(&self) -> RelationKind {
        self.kind
    }

    /// Returns cardinality metadata.
    #[must_use]
    pub const fn cardinality(&self) -> Cardinality {
        self.cardinality
    }

    /// Returns endpoints.
    #[must_use]
    pub fn endpoints(&self) -> &[RelationEndpoint] {
        &self.endpoints
    }
}

/// Error returned by relation constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RelationError {
    /// Label was empty.
    Empty,
    /// Label contained a control character.
    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(())
    }
}