1use crate::error::{ModelError, ModelResult};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8pub enum RelationshipType {
9 #[default]
11 HasOne,
12 HasMany,
14 BelongsTo,
16 ManyToMany,
18 MorphOne,
20 MorphMany,
22 MorphTo,
24}
25
26impl RelationshipType {
27 pub fn is_polymorphic(self) -> bool {
29 matches!(self, Self::MorphOne | Self::MorphMany | Self::MorphTo)
30 }
31
32 pub fn is_collection(self) -> bool {
34 matches!(self, Self::HasMany | Self::ManyToMany | Self::MorphMany)
35 }
36
37 pub fn requires_pivot(self) -> bool {
39 matches!(self, Self::ManyToMany)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
45pub struct RelationshipMetadata {
46 pub relationship_type: RelationshipType,
48
49 pub name: String,
51
52 pub related_table: String,
54
55 pub related_model: String,
57
58 pub foreign_key: ForeignKeyConfig,
60
61 pub local_key: String,
63
64 pub custom_name: Option<String>,
66
67 pub pivot_config: Option<PivotConfig>,
69
70 pub polymorphic_config: Option<PolymorphicConfig>,
72
73 pub eager_load: bool,
75
76 pub constraints: Vec<RelationshipConstraint>,
78
79 pub inverse: Option<String>,
81}
82
83impl RelationshipMetadata {
84 pub fn new(
86 relationship_type: RelationshipType,
87 name: String,
88 related_table: String,
89 related_model: String,
90 foreign_key: ForeignKeyConfig,
91 ) -> Self {
92 Self {
93 relationship_type,
94 name,
95 related_table,
96 related_model,
97 foreign_key,
98 local_key: "id".to_string(),
99 custom_name: None,
100 pivot_config: None,
101 polymorphic_config: None,
102 eager_load: false,
103 constraints: Vec::new(),
104 inverse: None,
105 }
106 }
107
108 pub fn new_with_pivot(
110 relationship_type: RelationshipType,
111 name: String,
112 related_table: String,
113 related_model: String,
114 foreign_key: ForeignKeyConfig,
115 pivot_config: PivotConfig,
116 ) -> Self {
117 Self {
118 relationship_type,
119 name,
120 related_table,
121 related_model,
122 foreign_key,
123 local_key: "id".to_string(),
124 custom_name: None,
125 pivot_config: Some(pivot_config),
126 polymorphic_config: None,
127 eager_load: false,
128 constraints: Vec::new(),
129 inverse: None,
130 }
131 }
132
133 pub fn with_local_key(mut self, local_key: String) -> Self {
135 self.local_key = local_key;
136 self
137 }
138
139 pub fn with_custom_name(mut self, custom_name: String) -> Self {
141 self.custom_name = Some(custom_name);
142 self
143 }
144
145 pub fn with_pivot(mut self, pivot_config: PivotConfig) -> Self {
147 self.pivot_config = Some(pivot_config);
148 self
149 }
150
151 pub fn with_polymorphic(mut self, polymorphic_config: PolymorphicConfig) -> Self {
153 self.polymorphic_config = Some(polymorphic_config);
154 self
155 }
156
157 pub fn with_eager_load(mut self, eager_load: bool) -> Self {
159 self.eager_load = eager_load;
160 self
161 }
162
163 pub fn with_constraints(mut self, constraints: Vec<RelationshipConstraint>) -> Self {
165 self.constraints = constraints;
166 self
167 }
168
169 pub fn with_inverse(mut self, inverse: String) -> Self {
171 self.inverse = Some(inverse);
172 self
173 }
174
175 pub fn validate(&self) -> ModelResult<()> {
177 if self.relationship_type.requires_pivot() && self.pivot_config.is_none() {
179 return Err(ModelError::Configuration(format!(
180 "Relationship '{}' of type {:?} requires pivot configuration",
181 self.name, self.relationship_type
182 )));
183 }
184
185 if self.relationship_type.is_polymorphic() && self.polymorphic_config.is_none() {
186 return Err(ModelError::Configuration(format!(
187 "Relationship '{}' of type {:?} requires polymorphic configuration",
188 self.name, self.relationship_type
189 )));
190 }
191
192 self.foreign_key.validate()?;
194
195 if let Some(ref pivot) = self.pivot_config {
197 pivot.validate()?;
198 }
199
200 if let Some(ref poly) = self.polymorphic_config {
202 poly.validate()?;
203 }
204
205 Ok(())
206 }
207
208 pub fn query_name(&self) -> &str {
210 self.custom_name.as_ref().unwrap_or(&self.name)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
216pub struct ForeignKeyConfig {
217 pub columns: Vec<String>,
219
220 pub is_composite: bool,
222
223 pub table: String,
225}
226
227impl ForeignKeyConfig {
228 pub fn simple(column: String, table: String) -> Self {
230 Self {
231 columns: vec![column],
232 is_composite: false,
233 table,
234 }
235 }
236
237 pub fn composite(columns: Vec<String>, table: String) -> Self {
239 Self {
240 columns,
241 is_composite: true,
242 table,
243 }
244 }
245
246 pub fn primary_column(&self) -> &str {
248 self.columns.first().map(|s| s.as_str()).unwrap_or("")
249 }
250
251 pub fn validate(&self) -> ModelResult<()> {
253 if self.columns.is_empty() {
254 return Err(ModelError::Configuration(
255 "Foreign key configuration must have at least one column".to_string(),
256 ));
257 }
258
259 if self.is_composite && self.columns.len() < 2 {
260 return Err(ModelError::Configuration(
261 "Composite foreign key must have at least 2 columns".to_string(),
262 ));
263 }
264
265 if self.table.is_empty() {
266 return Err(ModelError::Configuration(
267 "Foreign key configuration must specify a table".to_string(),
268 ));
269 }
270
271 Ok(())
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277pub struct PivotConfig {
278 pub table: String,
280
281 pub local_key: String,
283
284 pub foreign_key: String,
286
287 pub additional_columns: Vec<String>,
289
290 pub with_timestamps: bool,
292}
293
294impl PivotConfig {
295 pub fn new(table: String, local_key: String, foreign_key: String) -> Self {
297 Self {
298 table,
299 local_key,
300 foreign_key,
301 additional_columns: Vec::new(),
302 with_timestamps: false,
303 }
304 }
305
306 pub fn with_additional_columns(mut self, columns: Vec<String>) -> Self {
308 self.additional_columns = columns;
309 self
310 }
311
312 pub fn with_timestamps(mut self) -> Self {
314 self.with_timestamps = true;
315 self
316 }
317
318 pub fn validate(&self) -> ModelResult<()> {
320 if self.table.is_empty() {
321 return Err(ModelError::Configuration(
322 "Pivot table name cannot be empty".to_string(),
323 ));
324 }
325
326 if self.local_key.is_empty() {
327 return Err(ModelError::Configuration(
328 "Pivot local key cannot be empty".to_string(),
329 ));
330 }
331
332 if self.foreign_key.is_empty() {
333 return Err(ModelError::Configuration(
334 "Pivot foreign key cannot be empty".to_string(),
335 ));
336 }
337
338 if self.local_key == self.foreign_key {
339 return Err(ModelError::Configuration(
340 "Pivot local key and foreign key must be different".to_string(),
341 ));
342 }
343
344 Ok(())
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct PolymorphicConfig {
351 pub type_column: String,
353
354 pub id_column: String,
356
357 pub name: String,
359
360 pub allowed_types: Vec<String>,
362}
363
364impl PolymorphicConfig {
365 pub fn new(name: String, type_column: String, id_column: String) -> Self {
367 Self {
368 type_column,
369 id_column,
370 name,
371 allowed_types: Vec::new(),
372 }
373 }
374
375 pub fn with_allowed_types(mut self, types: Vec<String>) -> Self {
377 self.allowed_types = types;
378 self
379 }
380
381 pub fn validate(&self) -> ModelResult<()> {
383 if self.name.is_empty() {
384 return Err(ModelError::Configuration(
385 "Polymorphic relationship name cannot be empty".to_string(),
386 ));
387 }
388
389 if self.type_column.is_empty() {
390 return Err(ModelError::Configuration(
391 "Polymorphic type column cannot be empty".to_string(),
392 ));
393 }
394
395 if self.id_column.is_empty() {
396 return Err(ModelError::Configuration(
397 "Polymorphic ID column cannot be empty".to_string(),
398 ));
399 }
400
401 if self.type_column == self.id_column {
402 return Err(ModelError::Configuration(
403 "Polymorphic type column and ID column must be different".to_string(),
404 ));
405 }
406
407 Ok(())
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct RelationshipConstraint {
414 pub column: String,
416
417 pub operator: ConstraintOperator,
419
420 pub value: String,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426pub enum ConstraintOperator {
427 Equal,
428 NotEqual,
429 GreaterThan,
430 LessThan,
431 GreaterThanOrEqual,
432 LessThanOrEqual,
433 In,
434 NotIn,
435 Like,
436 NotLike,
437 IsNull,
438 IsNotNull,
439}
440
441impl ConstraintOperator {
442 pub fn to_sql(&self) -> &'static str {
444 match self {
445 Self::Equal => "=",
446 Self::NotEqual => "!=",
447 Self::GreaterThan => ">",
448 Self::LessThan => "<",
449 Self::GreaterThanOrEqual => ">=",
450 Self::LessThanOrEqual => "<=",
451 Self::In => "IN",
452 Self::NotIn => "NOT IN",
453 Self::Like => "LIKE",
454 Self::NotLike => "NOT LIKE",
455 Self::IsNull => "IS NULL",
456 Self::IsNotNull => "IS NOT NULL",
457 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_relationship_type_properties() {
467 assert!(RelationshipType::MorphOne.is_polymorphic());
468 assert!(RelationshipType::MorphMany.is_polymorphic());
469 assert!(RelationshipType::MorphTo.is_polymorphic());
470 assert!(!RelationshipType::HasOne.is_polymorphic());
471
472 assert!(RelationshipType::HasMany.is_collection());
473 assert!(RelationshipType::ManyToMany.is_collection());
474 assert!(RelationshipType::MorphMany.is_collection());
475 assert!(!RelationshipType::HasOne.is_collection());
476
477 assert!(RelationshipType::ManyToMany.requires_pivot());
478 assert!(!RelationshipType::HasMany.requires_pivot());
479 }
480
481 #[test]
482 fn test_relationship_metadata_creation() {
483 let metadata = RelationshipMetadata::new(
484 RelationshipType::HasMany,
485 "posts".to_string(),
486 "posts".to_string(),
487 "Post".to_string(),
488 ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
489 );
490
491 assert_eq!(metadata.relationship_type, RelationshipType::HasMany);
492 assert_eq!(metadata.name, "posts");
493 assert_eq!(metadata.related_table, "posts");
494 assert_eq!(metadata.local_key, "id");
495 assert!(!metadata.eager_load);
496 }
497
498 #[test]
499 fn test_relationship_metadata_validation() {
500 let metadata = RelationshipMetadata::new(
502 RelationshipType::HasMany,
503 "posts".to_string(),
504 "posts".to_string(),
505 "Post".to_string(),
506 ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string()),
507 );
508 assert!(metadata.validate().is_ok());
509
510 let invalid_metadata = RelationshipMetadata::new(
512 RelationshipType::ManyToMany,
513 "roles".to_string(),
514 "roles".to_string(),
515 "Role".to_string(),
516 ForeignKeyConfig::simple("user_id".to_string(), "user_roles".to_string()),
517 );
518 assert!(invalid_metadata.validate().is_err());
519 }
520
521 #[test]
522 fn test_foreign_key_config() {
523 let simple_fk = ForeignKeyConfig::simple("user_id".to_string(), "posts".to_string());
524 assert!(!simple_fk.is_composite);
525 assert_eq!(simple_fk.primary_column(), "user_id");
526 assert!(simple_fk.validate().is_ok());
527
528 let composite_fk = ForeignKeyConfig::composite(
529 vec!["user_id".to_string(), "company_id".to_string()],
530 "posts".to_string(),
531 );
532 assert!(composite_fk.is_composite);
533 assert_eq!(composite_fk.primary_column(), "user_id");
534 assert!(composite_fk.validate().is_ok());
535 }
536
537 #[test]
538 fn test_pivot_config() {
539 let pivot = PivotConfig::new(
540 "user_roles".to_string(),
541 "user_id".to_string(),
542 "role_id".to_string(),
543 )
544 .with_timestamps();
545
546 assert_eq!(pivot.table, "user_roles");
547 assert_eq!(pivot.local_key, "user_id");
548 assert_eq!(pivot.foreign_key, "role_id");
549 assert!(pivot.with_timestamps);
550 assert!(pivot.validate().is_ok());
551 }
552
553 #[test]
554 fn test_polymorphic_config() {
555 let poly = PolymorphicConfig::new(
556 "commentable".to_string(),
557 "commentable_type".to_string(),
558 "commentable_id".to_string(),
559 )
560 .with_allowed_types(vec!["Post".to_string(), "Video".to_string()]);
561
562 assert_eq!(poly.name, "commentable");
563 assert_eq!(poly.type_column, "commentable_type");
564 assert_eq!(poly.id_column, "commentable_id");
565 assert_eq!(poly.allowed_types.len(), 2);
566 assert!(poly.validate().is_ok());
567 }
568
569 #[test]
570 fn test_constraint_operator_sql() {
571 assert_eq!(ConstraintOperator::Equal.to_sql(), "=");
572 assert_eq!(ConstraintOperator::In.to_sql(), "IN");
573 assert_eq!(ConstraintOperator::IsNull.to_sql(), "IS NULL");
574 }
575
576 #[test]
577 fn test_relationship_metadata_builder_pattern() {
578 let metadata = RelationshipMetadata::new(
579 RelationshipType::HasOne,
580 "profile".to_string(),
581 "profiles".to_string(),
582 "Profile".to_string(),
583 ForeignKeyConfig::simple("user_id".to_string(), "profiles".to_string()),
584 )
585 .with_local_key("uuid".to_string())
586 .with_custom_name("user_profile".to_string())
587 .with_eager_load(true)
588 .with_inverse("user".to_string());
589
590 assert_eq!(metadata.local_key, "uuid");
591 assert_eq!(metadata.custom_name, Some("user_profile".to_string()));
592 assert!(metadata.eager_load);
593 assert_eq!(metadata.inverse, Some("user".to_string()));
594 assert_eq!(metadata.query_name(), "user_profile");
595 }
596}