1use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct JoinConfig {
9 pub eager_load: bool,
11 pub max_depth: Option<usize>,
13 #[serde(skip)]
15 pub loading_strategy: Option<crate::proxy::LoadingStrategy>,
16 pub join_type: Option<String>,
18 pub condition: Option<String>,
20}
21
22impl JoinConfig {
23 pub fn new() -> Self {
25 Self {
26 eager_load: false,
27 max_depth: None,
28 loading_strategy: None,
29 join_type: None,
30 condition: None,
31 }
32 }
33
34 pub fn with_loading_strategy(mut self, strategy: crate::proxy::LoadingStrategy) -> Self {
45 self.loading_strategy = Some(strategy);
46 self
47 }
48
49 pub fn with_join_type(mut self, join_type: &str) -> Self {
60 self.join_type = Some(join_type.to_string());
61 self
62 }
63
64 pub fn with_condition(mut self, condition: &str) -> Self {
75 self.condition = Some(condition.to_string());
76 self
77 }
78}
79
80impl Default for JoinConfig {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[derive(Debug, Clone)]
90pub struct NestedProxy {
91 pub path: Vec<String>,
93 pub conditions: Vec<String>,
95 pub final_attribute: Option<String>,
97}
98
99impl NestedProxy {
100 pub fn new() -> Self {
111 Self {
112 path: Vec::new(),
113 conditions: Vec::new(),
114 final_attribute: None,
115 }
116 }
117
118 pub fn from_path(path: Vec<String>) -> Self {
120 Self {
121 path,
122 conditions: Vec::new(),
123 final_attribute: None,
124 }
125 }
126
127 pub fn add_level(mut self, level: &str) -> Self {
140 self.path.push(level.to_string());
141 self
142 }
143
144 pub fn with_condition(mut self, condition: &str) -> Self {
157 self.conditions.push(condition.to_string());
158 self
159 }
160
161 pub fn with_attribute(mut self, attr: &str) -> Self {
174 self.final_attribute = Some(attr.to_string());
175 self
176 }
177
178 pub fn depth(&self) -> usize {
192 self.path.len()
193 }
194
195 pub fn attribute(&self) -> &str {
208 self.final_attribute.as_deref().unwrap_or("")
209 }
210
211 pub fn conditions(&self) -> &[String] {
226 &self.conditions
227 }
228}
229
230impl Default for NestedProxy {
231 fn default() -> Self {
232 Self::new()
233 }
234}
235
236#[derive(Debug, Clone)]
259pub struct RelationshipPath {
260 pub segments: Vec<String>,
262 visited: HashSet<String>,
264 filters: HashMap<String, Vec<(String, String)>>,
266 transforms: HashMap<String, Vec<(String, String)>>,
268 attribute: Option<String>,
270}
271
272impl RelationshipPath {
273 pub fn new() -> Self {
284 Self {
285 segments: Vec::new(),
286 visited: HashSet::new(),
287 filters: HashMap::new(),
288 transforms: HashMap::new(),
289 attribute: None,
290 }
291 }
292
293 pub fn through(mut self, relationship: &str) -> Self {
308 let rel = relationship.to_string();
309 self.segments.push(rel.clone());
310 self.visited.insert(rel);
311 self
312 }
313
314 pub fn try_through(mut self, relationship: &str) -> Result<Self, CircularReferenceError> {
334 let rel = relationship.to_string();
335
336 if self.visited.contains(&rel) {
337 return Err(CircularReferenceError {
338 relationship: rel,
339 path: self.segments.clone(),
340 });
341 }
342
343 self.segments.push(rel.clone());
344 self.visited.insert(rel);
345 Ok(self)
346 }
347
348 pub fn with_filter(mut self, field: &str, value: &str) -> Self {
361 let current_rel = self.segments.last().cloned().unwrap_or_default();
362 self.filters
363 .entry(current_rel)
364 .or_default()
365 .push((field.to_string(), value.to_string()));
366 self
367 }
368
369 pub fn with_transform(mut self, relationship: &str, transform: &str) -> Self {
383 self.transforms
384 .entry(relationship.to_string())
385 .or_default()
386 .push((relationship.to_string(), transform.to_string()));
387 self
388 }
389
390 pub fn attribute(mut self, attr: &str) -> Self {
404 self.attribute = Some(attr.to_string());
405 self
406 }
407
408 pub fn path(&self) -> &[String] {
410 &self.segments
411 }
412
413 pub fn get_attribute(&self) -> &str {
415 self.attribute.as_deref().unwrap_or("")
416 }
417
418 pub fn has_filters(&self) -> bool {
420 !self.filters.is_empty()
421 }
422
423 pub fn filters(&self) -> Vec<(String, String)> {
425 self.filters
426 .values()
427 .flat_map(|v| v.iter().cloned())
428 .collect()
429 }
430
431 pub fn has_transforms(&self) -> bool {
433 !self.transforms.is_empty()
434 }
435
436 pub fn transforms(&self) -> Vec<(String, String)> {
438 self.transforms
439 .values()
440 .flat_map(|v| v.iter().cloned())
441 .collect()
442 }
443
444 pub fn contains(&self, relationship: &str) -> bool {
446 self.visited.contains(relationship)
447 }
448
449 pub fn validate(self) -> Result<Self, CircularReferenceError> {
454 Ok(self)
456 }
457}
458
459impl Default for RelationshipPath {
460 fn default() -> Self {
461 Self::new()
462 }
463}
464
465#[derive(Debug, Clone, thiserror::Error)]
467#[error("Circular reference detected: relationship '{relationship}' already exists in path {}", path_display(.path))]
468pub struct CircularReferenceError {
469 pub relationship: String,
471 pub path: Vec<String>,
473}
474
475fn path_display(path: &[String]) -> String {
476 if path.is_empty() {
477 "(empty)".to_string()
478 } else {
479 format!("[{}]", path.join(" -> "))
480 }
481}
482
483pub fn extract_through_path(path: &str) -> Vec<String> {
485 path.split('.').map(|s| s.to_string()).collect()
486}
487
488pub fn filter_through_path(path: &RelationshipPath, predicate: impl Fn(&str) -> bool) -> bool {
490 path.segments.iter().any(|s| predicate(s))
491}
492
493pub fn traverse_and_extract(proxy: &NestedProxy) -> Vec<String> {
495 proxy.path.clone()
496}
497
498pub fn traverse_relationships(path: &RelationshipPath) -> Vec<String> {
500 path.segments.clone()
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
509 fn test_relationship_path_no_cycle() {
510 let path = RelationshipPath::new()
511 .through("posts")
512 .through("comments")
513 .through("author")
514 .attribute("name");
515
516 assert_eq!(path.path().len(), 3);
517 assert_eq!(path.path()[0], "posts");
518 assert_eq!(path.path()[1], "comments");
519 assert_eq!(path.path()[2], "author");
520 assert_eq!(path.get_attribute(), "name");
521 }
522
523 #[test]
525 fn test_simple_cycle_detection() {
526 let path = RelationshipPath::new().through("posts").through("author");
527
528 let result = path.try_through("posts");
530
531 assert!(result.is_err());
532 let err = result.unwrap_err();
533 assert_eq!(err.relationship, "posts");
534 assert_eq!(err.path, vec!["posts", "author"]);
535 }
536
537 #[test]
539 fn test_complex_cycle_detection() {
540 let path = RelationshipPath::new()
541 .through("user")
542 .through("posts")
543 .through("comments");
544
545 let result = path.try_through("user");
547
548 assert!(result.is_err());
549 let err = result.unwrap_err();
550 assert_eq!(err.relationship, "user");
551 assert_eq!(err.path, vec!["user", "posts", "comments"]);
552 }
553
554 #[test]
556 fn test_no_false_positive_cycle() {
557 let path = RelationshipPath::new()
558 .through("posts")
559 .through("comments")
560 .through("author")
561 .through("profile");
562
563 assert_eq!(path.path().len(), 4);
564 }
565
566 #[test]
568 fn test_contains_relationship() {
569 let path = RelationshipPath::new().through("posts").through("comments");
570
571 assert!(path.contains("posts"));
572 assert!(path.contains("comments"));
573 assert!(!path.contains("author"));
574 }
575
576 #[test]
578 fn test_path_with_filters() {
579 let path = RelationshipPath::new()
580 .through("posts")
581 .with_filter("published", "true")
582 .through("comments")
583 .attribute("content");
584
585 assert!(path.has_filters());
586 assert_eq!(path.filters().len(), 1);
587 assert_eq!(
588 path.filters()[0],
589 ("published".to_string(), "true".to_string())
590 );
591 }
592
593 #[test]
595 fn test_path_with_transforms() {
596 let path = RelationshipPath::new()
597 .through("posts")
598 .through("comments")
599 .with_transform("author", "upper")
600 .attribute("name");
601
602 assert!(path.has_transforms());
603 assert_eq!(path.transforms().len(), 1);
604 }
605
606 #[test]
608 fn test_multiple_filters() {
609 let path = RelationshipPath::new()
610 .through("posts")
611 .with_filter("published", "true")
612 .through("comments")
613 .with_filter("approved", "true")
614 .attribute("content");
615
616 assert!(path.has_filters());
617 assert_eq!(path.filters().len(), 2);
618 }
619
620 #[test]
622 fn test_error_message_format() {
623 let path = RelationshipPath::new().through("posts").through("author");
624
625 let result = path.try_through("posts");
626 assert!(result.is_err());
627
628 let err = result.unwrap_err();
629 let error_msg = err.to_string();
630 assert!(error_msg.contains("Circular reference detected"));
631 assert!(error_msg.contains("posts"));
632 assert!(error_msg.contains("author"));
633 }
634
635 #[test]
637 fn test_default_path() {
638 let path = RelationshipPath::default();
639 assert_eq!(path.path().len(), 0);
640 assert!(!path.has_filters());
641 assert!(!path.has_transforms());
642 }
643
644 #[test]
646 fn test_empty_path_error() {
647 let err = CircularReferenceError {
648 relationship: "posts".to_string(),
649 path: vec![],
650 };
651 let msg = err.to_string();
652 assert!(msg.contains("(empty)"));
653 }
654
655 #[test]
657 fn test_path_clone() {
658 let path1 = RelationshipPath::new()
659 .through("posts")
660 .through("comments")
661 .attribute("content");
662
663 let path2 = path1.clone();
664
665 assert_eq!(path1.path(), path2.path());
666 assert_eq!(path1.get_attribute(), path2.get_attribute());
667 }
668
669 #[test]
671 fn test_through_allows_cycles() {
672 let path = RelationshipPath::new()
675 .through("posts")
676 .through("author")
677 .through("posts"); assert_eq!(path.path().len(), 3);
680 assert!(path.contains("posts"));
682 }
683
684 #[test]
686 fn test_validate_method() {
687 let path = RelationshipPath::new()
688 .through("posts")
689 .through("comments")
690 .attribute("content");
691
692 let result = path.validate();
693 assert!(result.is_ok());
694 }
695
696 #[test]
698 fn test_attribute_only_path() {
699 let path = RelationshipPath::new().attribute("name");
700
701 assert_eq!(path.path().len(), 0);
702 assert_eq!(path.get_attribute(), "name");
703 }
704
705 #[test]
707 fn test_filter_on_empty_path() {
708 let path = RelationshipPath::new().with_filter("field", "value");
709
710 assert!(path.has_filters());
712 }
713}