1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum Cardinality {
11 OneToOne,
12 OneToMany,
13 ManyToOne,
14 ManyToMany,
15}
16
17impl Cardinality {
18 pub fn default_navigation(&self) -> NavigationHint {
25 match self {
26 Cardinality::OneToOne => NavigationHint::Inline,
27 Cardinality::ManyToOne => NavigationHint::Link,
28 Cardinality::OneToMany => NavigationHint::Nested,
29 Cardinality::ManyToMany => NavigationHint::Nested,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
39#[serde(rename_all = "snake_case")]
40pub enum NavigationHint {
41 Inline,
43 Link,
45 Tab,
47 Nested,
49 Hidden,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
69pub struct RelationshipDef {
70 pub name: String,
72 pub target: String,
74 pub cardinality: Cardinality,
76 pub navigation: NavigationHint,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub foreign_key: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub inverse: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub description: Option<String>,
87}
88
89impl RelationshipDef {
90 pub fn new(
92 name: impl Into<String>,
93 target: impl Into<String>,
94 cardinality: Cardinality,
95 ) -> Self {
96 Self {
97 name: name.into(),
98 target: target.into(),
99 navigation: cardinality.default_navigation(),
100 cardinality,
101 foreign_key: None,
102 inverse: None,
103 description: None,
104 }
105 }
106
107 pub fn foreign_key(mut self, fk: impl Into<String>) -> Self {
109 self.foreign_key = Some(fk.into());
110 self
111 }
112
113 pub fn inverse(mut self, inverse: impl Into<String>) -> Self {
115 self.inverse = Some(inverse.into());
116 self
117 }
118
119 pub fn navigation(mut self, hint: NavigationHint) -> Self {
121 self.navigation = hint;
122 self
123 }
124
125 pub fn description(mut self, desc: impl Into<String>) -> Self {
127 self.description = Some(desc.into());
128 self
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
139 fn relationship_def_new_sets_defaults() {
140 let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne);
141 assert_eq!(rel.name, "customer");
142 assert_eq!(rel.target, "customer");
143 assert_eq!(rel.cardinality, Cardinality::ManyToOne);
144 assert_eq!(rel.navigation, NavigationHint::Link);
145 assert!(rel.foreign_key.is_none());
146 assert!(rel.inverse.is_none());
147 assert!(rel.description.is_none());
148 }
149
150 #[test]
151 fn relationship_def_builder_chain() {
152 let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne)
153 .foreign_key("customer_id")
154 .inverse("orders")
155 .navigation(NavigationHint::Tab)
156 .description("Customer who placed this order");
157
158 assert_eq!(rel.name, "customer");
159 assert_eq!(rel.target, "customer");
160 assert_eq!(rel.cardinality, Cardinality::ManyToOne);
161 assert_eq!(rel.navigation, NavigationHint::Tab);
162 assert_eq!(rel.foreign_key.as_deref(), Some("customer_id"));
163 assert_eq!(rel.inverse.as_deref(), Some("orders"));
164 assert_eq!(
165 rel.description.as_deref(),
166 Some("Customer who placed this order")
167 );
168 }
169
170 #[test]
171 fn cardinality_default_navigation() {
172 assert_eq!(
173 Cardinality::OneToOne.default_navigation(),
174 NavigationHint::Inline
175 );
176 assert_eq!(
177 Cardinality::ManyToOne.default_navigation(),
178 NavigationHint::Link
179 );
180 assert_eq!(
181 Cardinality::OneToMany.default_navigation(),
182 NavigationHint::Nested
183 );
184 assert_eq!(
185 Cardinality::ManyToMany.default_navigation(),
186 NavigationHint::Nested
187 );
188 }
189
190 #[test]
193 fn relationship_def_serde_round_trip() {
194 let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne)
195 .foreign_key("customer_id")
196 .inverse("orders")
197 .navigation(NavigationHint::Link)
198 .description("Customer who placed this order");
199
200 let json = serde_json::to_string(&rel).unwrap();
201 let parsed: RelationshipDef = serde_json::from_str(&json).unwrap();
202 assert_eq!(rel, parsed);
203 }
204
205 #[test]
206 fn relationship_def_json_omits_none_fields() {
207 let rel = RelationshipDef::new("items", "item", Cardinality::OneToMany);
208 let json = serde_json::to_string(&rel).unwrap();
209 assert!(!json.contains("foreign_key"));
210 assert!(!json.contains("inverse"));
211 assert!(!json.contains("description"));
212 assert!(json.contains("name"));
214 assert!(json.contains("target"));
215 assert!(json.contains("cardinality"));
216 assert!(json.contains("navigation"));
217 }
218
219 #[test]
220 fn cardinality_serde_values() {
221 assert_eq!(
222 serde_json::to_string(&Cardinality::OneToOne).unwrap(),
223 r#""one_to_one""#
224 );
225 assert_eq!(
226 serde_json::to_string(&Cardinality::OneToMany).unwrap(),
227 r#""one_to_many""#
228 );
229 assert_eq!(
230 serde_json::to_string(&Cardinality::ManyToOne).unwrap(),
231 r#""many_to_one""#
232 );
233 assert_eq!(
234 serde_json::to_string(&Cardinality::ManyToMany).unwrap(),
235 r#""many_to_many""#
236 );
237
238 for card in [
240 Cardinality::OneToOne,
241 Cardinality::OneToMany,
242 Cardinality::ManyToOne,
243 Cardinality::ManyToMany,
244 ] {
245 let json = serde_json::to_string(&card).unwrap();
246 let parsed: Cardinality = serde_json::from_str(&json).unwrap();
247 assert_eq!(card, parsed);
248 }
249 }
250
251 #[test]
252 fn navigation_hint_serde_values() {
253 assert_eq!(
254 serde_json::to_string(&NavigationHint::Inline).unwrap(),
255 r#""inline""#
256 );
257 assert_eq!(
258 serde_json::to_string(&NavigationHint::Link).unwrap(),
259 r#""link""#
260 );
261 assert_eq!(
262 serde_json::to_string(&NavigationHint::Tab).unwrap(),
263 r#""tab""#
264 );
265 assert_eq!(
266 serde_json::to_string(&NavigationHint::Nested).unwrap(),
267 r#""nested""#
268 );
269 assert_eq!(
270 serde_json::to_string(&NavigationHint::Hidden).unwrap(),
271 r#""hidden""#
272 );
273
274 for hint in [
276 NavigationHint::Inline,
277 NavigationHint::Link,
278 NavigationHint::Tab,
279 NavigationHint::Nested,
280 NavigationHint::Hidden,
281 ] {
282 let json = serde_json::to_string(&hint).unwrap();
283 let parsed: NavigationHint = serde_json::from_str(&json).unwrap();
284 assert_eq!(hint, parsed);
285 }
286 }
287
288 #[test]
291 fn relationship_def_json_schema() {
292 let schema = schemars::schema_for!(RelationshipDef);
293 let value = schema.to_value();
294 let props = value
295 .get("properties")
296 .expect("RelationshipDef schema must have properties");
297 let obj = props.as_object().unwrap();
298 assert!(obj.contains_key("name"), "missing 'name' property");
299 assert!(obj.contains_key("target"), "missing 'target' property");
300 assert!(
301 obj.contains_key("cardinality"),
302 "missing 'cardinality' property"
303 );
304 assert!(
305 obj.contains_key("navigation"),
306 "missing 'navigation' property"
307 );
308 assert!(
309 obj.contains_key("foreign_key"),
310 "missing 'foreign_key' property"
311 );
312 assert!(obj.contains_key("inverse"), "missing 'inverse' property");
313 }
314}