1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7use std::error::Error;
8
9use use_db_name::RelationName;
10
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct RelationRef {
14 name: Option<RelationName>,
15}
16
17impl RelationRef {
18 #[must_use]
20 pub const fn new(name: Option<RelationName>) -> Self {
21 Self { name }
22 }
23
24 #[must_use]
26 pub const fn name(&self) -> Option<&RelationName> {
27 self.name.as_ref()
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub enum RelationKind {
34 ParentChild,
36 #[default]
38 Reference,
39 Ownership,
41 Membership,
43 Other,
45}
46
47#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum Cardinality {
50 OneToOne,
52 #[default]
54 OneToMany,
55 ManyToOne,
57 ManyToMany,
59}
60
61impl Cardinality {
62 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct RelationEndpoint {
77 label: String,
78 required: bool,
79}
80
81impl RelationEndpoint {
82 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 #[must_use]
96 pub fn label(&self) -> &str {
97 &self.label
98 }
99
100 #[must_use]
102 pub const fn is_required(&self) -> bool {
103 self.required
104 }
105
106 #[must_use]
108 pub const fn is_optional(&self) -> bool {
109 !self.required
110 }
111}
112
113#[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 #[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 #[must_use]
136 pub fn with_endpoints(mut self, endpoints: Vec<RelationEndpoint>) -> Self {
137 self.endpoints = endpoints;
138 self
139 }
140
141 #[must_use]
143 pub const fn reference(&self) -> &RelationRef {
144 &self.reference
145 }
146
147 #[must_use]
149 pub const fn kind(&self) -> RelationKind {
150 self.kind
151 }
152
153 #[must_use]
155 pub const fn cardinality(&self) -> Cardinality {
156 self.cardinality
157 }
158
159 #[must_use]
161 pub fn endpoints(&self) -> &[RelationEndpoint] {
162 &self.endpoints
163 }
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum RelationError {
169 Empty,
171 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}