Skip to main content

use_db_relation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Relation and cardinality primitives for `RustUse`.
5
6use core::fmt;
7use std::error::Error;
8
9use use_db_name::RelationName;
10
11/// Relation reference metadata.
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct RelationRef {
14    name: Option<RelationName>,
15}
16
17impl RelationRef {
18    /// Creates relation reference metadata.
19    #[must_use]
20    pub const fn new(name: Option<RelationName>) -> Self {
21        Self { name }
22    }
23
24    /// Returns the optional relation name.
25    #[must_use]
26    pub const fn name(&self) -> Option<&RelationName> {
27        self.name.as_ref()
28    }
29}
30
31/// Broad relation kind.
32#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub enum RelationKind {
34    /// Parent-child relationship.
35    ParentChild,
36    /// Reference relationship.
37    #[default]
38    Reference,
39    /// Ownership relationship.
40    Ownership,
41    /// Membership relationship.
42    Membership,
43    /// Other or unspecified relationship.
44    Other,
45}
46
47/// Common cardinality labels.
48#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum Cardinality {
50    /// One-to-one relation.
51    OneToOne,
52    /// One-to-many relation.
53    #[default]
54    OneToMany,
55    /// Many-to-one relation.
56    ManyToOne,
57    /// Many-to-many relation.
58    ManyToMany,
59}
60
61impl Cardinality {
62    /// Returns a stable cardinality label.
63    #[must_use]
64    pub const fn as_str(self) -> &'static str {
65        match self {
66            Self::OneToOne => "one-to-one",
67            Self::OneToMany => "one-to-many",
68            Self::ManyToOne => "many-to-one",
69            Self::ManyToMany => "many-to-many",
70        }
71    }
72}
73
74/// A relation endpoint label with optionality metadata.
75#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct RelationEndpoint {
77    label: String,
78    required: bool,
79}
80
81impl RelationEndpoint {
82    /// Creates a relation endpoint label.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`RelationError`] when the label is empty or contains control characters.
87    pub fn new(label: impl AsRef<str>, required: bool) -> Result<Self, RelationError> {
88        validate_text(label.as_ref()).map(|value| Self {
89            label: value.to_owned(),
90            required,
91        })
92    }
93
94    /// Returns the endpoint label.
95    #[must_use]
96    pub fn label(&self) -> &str {
97        &self.label
98    }
99
100    /// Returns whether this endpoint is required.
101    #[must_use]
102    pub const fn is_required(&self) -> bool {
103        self.required
104    }
105
106    /// Returns whether this endpoint is optional.
107    #[must_use]
108    pub const fn is_optional(&self) -> bool {
109        !self.required
110    }
111}
112
113/// Relationship metadata.
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct Relationship {
116    reference: RelationRef,
117    kind: RelationKind,
118    cardinality: Cardinality,
119    endpoints: Vec<RelationEndpoint>,
120}
121
122impl Relationship {
123    /// Creates relationship metadata.
124    #[must_use]
125    pub const fn new(reference: RelationRef, kind: RelationKind, cardinality: Cardinality) -> Self {
126        Self {
127            reference,
128            kind,
129            cardinality,
130            endpoints: Vec::new(),
131        }
132    }
133
134    /// Sets relation endpoints.
135    #[must_use]
136    pub fn with_endpoints(mut self, endpoints: Vec<RelationEndpoint>) -> Self {
137        self.endpoints = endpoints;
138        self
139    }
140
141    /// Returns the relation reference.
142    #[must_use]
143    pub const fn reference(&self) -> &RelationRef {
144        &self.reference
145    }
146
147    /// Returns the relation kind.
148    #[must_use]
149    pub const fn kind(&self) -> RelationKind {
150        self.kind
151    }
152
153    /// Returns cardinality metadata.
154    #[must_use]
155    pub const fn cardinality(&self) -> Cardinality {
156        self.cardinality
157    }
158
159    /// Returns endpoints.
160    #[must_use]
161    pub fn endpoints(&self) -> &[RelationEndpoint] {
162        &self.endpoints
163    }
164}
165
166/// Error returned by relation constructors.
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum RelationError {
169    /// Label was empty.
170    Empty,
171    /// Label contained a control character.
172    ControlCharacter,
173}
174
175impl fmt::Display for RelationError {
176    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Empty => formatter.write_str("relation label cannot be empty"),
179            Self::ControlCharacter => {
180                formatter.write_str("relation label cannot contain control characters")
181            },
182        }
183    }
184}
185
186impl Error for RelationError {}
187
188fn validate_text(input: &str) -> Result<&str, RelationError> {
189    if input.chars().any(char::is_control) {
190        return Err(RelationError::ControlCharacter);
191    }
192    let trimmed = input.trim();
193    if trimmed.is_empty() {
194        return Err(RelationError::Empty);
195    }
196    Ok(trimmed)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::{Cardinality, RelationEndpoint, RelationKind, RelationRef, Relationship};
202    use use_db_name::RelationName;
203
204    #[test]
205    fn stores_relationship_metadata() -> Result<(), Box<dyn std::error::Error>> {
206        let relationship = Relationship::new(
207            RelationRef::new(Some(RelationName::new("user_posts")?)),
208            RelationKind::ParentChild,
209            Cardinality::OneToMany,
210        )
211        .with_endpoints(vec![
212            RelationEndpoint::new("users", true)?,
213            RelationEndpoint::new("posts", false)?,
214        ]);
215
216        assert_eq!(relationship.cardinality().as_str(), "one-to-many");
217        assert!(relationship.endpoints()[0].is_required());
218        assert!(relationship.endpoints()[1].is_optional());
219        Ok(())
220    }
221}