use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinConfig {
pub eager_load: bool,
pub max_depth: Option<usize>,
#[serde(skip)]
pub loading_strategy: Option<crate::proxy::LoadingStrategy>,
pub join_type: Option<String>,
pub condition: Option<String>,
}
impl JoinConfig {
pub fn new() -> Self {
Self {
eager_load: false,
max_depth: None,
loading_strategy: None,
join_type: None,
condition: None,
}
}
pub fn with_loading_strategy(mut self, strategy: crate::proxy::LoadingStrategy) -> Self {
self.loading_strategy = Some(strategy);
self
}
pub fn with_join_type(mut self, join_type: &str) -> Self {
self.join_type = Some(join_type.to_string());
self
}
pub fn with_condition(mut self, condition: &str) -> Self {
self.condition = Some(condition.to_string());
self
}
}
impl Default for JoinConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct NestedProxy {
pub path: Vec<String>,
pub conditions: Vec<String>,
pub final_attribute: Option<String>,
}
impl NestedProxy {
pub fn new() -> Self {
Self {
path: Vec::new(),
conditions: Vec::new(),
final_attribute: None,
}
}
pub fn from_path(path: Vec<String>) -> Self {
Self {
path,
conditions: Vec::new(),
final_attribute: None,
}
}
pub fn add_level(mut self, level: &str) -> Self {
self.path.push(level.to_string());
self
}
pub fn with_condition(mut self, condition: &str) -> Self {
self.conditions.push(condition.to_string());
self
}
pub fn with_attribute(mut self, attr: &str) -> Self {
self.final_attribute = Some(attr.to_string());
self
}
pub fn depth(&self) -> usize {
self.path.len()
}
pub fn attribute(&self) -> &str {
self.final_attribute.as_deref().unwrap_or("")
}
pub fn conditions(&self) -> &[String] {
&self.conditions
}
}
impl Default for NestedProxy {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RelationshipPath {
pub segments: Vec<String>,
visited: HashSet<String>,
filters: HashMap<String, Vec<(String, String)>>,
transforms: HashMap<String, Vec<(String, String)>>,
attribute: Option<String>,
}
impl RelationshipPath {
pub fn new() -> Self {
Self {
segments: Vec::new(),
visited: HashSet::new(),
filters: HashMap::new(),
transforms: HashMap::new(),
attribute: None,
}
}
pub fn through(mut self, relationship: &str) -> Self {
let rel = relationship.to_string();
self.segments.push(rel.clone());
self.visited.insert(rel);
self
}
pub fn try_through(mut self, relationship: &str) -> Result<Self, CircularReferenceError> {
let rel = relationship.to_string();
if self.visited.contains(&rel) {
return Err(CircularReferenceError {
relationship: rel,
path: self.segments.clone(),
});
}
self.segments.push(rel.clone());
self.visited.insert(rel);
Ok(self)
}
pub fn with_filter(mut self, field: &str, value: &str) -> Self {
let current_rel = self.segments.last().cloned().unwrap_or_default();
self.filters
.entry(current_rel)
.or_default()
.push((field.to_string(), value.to_string()));
self
}
pub fn with_transform(mut self, relationship: &str, transform: &str) -> Self {
self.transforms
.entry(relationship.to_string())
.or_default()
.push((relationship.to_string(), transform.to_string()));
self
}
pub fn attribute(mut self, attr: &str) -> Self {
self.attribute = Some(attr.to_string());
self
}
pub fn path(&self) -> &[String] {
&self.segments
}
pub fn get_attribute(&self) -> &str {
self.attribute.as_deref().unwrap_or("")
}
pub fn has_filters(&self) -> bool {
!self.filters.is_empty()
}
pub fn filters(&self) -> Vec<(String, String)> {
self.filters
.values()
.flat_map(|v| v.iter().cloned())
.collect()
}
pub fn has_transforms(&self) -> bool {
!self.transforms.is_empty()
}
pub fn transforms(&self) -> Vec<(String, String)> {
self.transforms
.values()
.flat_map(|v| v.iter().cloned())
.collect()
}
pub fn contains(&self, relationship: &str) -> bool {
self.visited.contains(relationship)
}
pub fn validate(self) -> Result<Self, CircularReferenceError> {
Ok(self)
}
}
impl Default for RelationshipPath {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("Circular reference detected: relationship '{relationship}' already exists in path {}", path_display(.path))]
pub struct CircularReferenceError {
pub relationship: String,
pub path: Vec<String>,
}
fn path_display(path: &[String]) -> String {
if path.is_empty() {
"(empty)".to_string()
} else {
format!("[{}]", path.join(" -> "))
}
}
pub fn extract_through_path(path: &str) -> Vec<String> {
path.split('.').map(|s| s.to_string()).collect()
}
pub fn filter_through_path(path: &RelationshipPath, predicate: impl Fn(&str) -> bool) -> bool {
path.segments.iter().any(|s| predicate(s))
}
pub fn traverse_and_extract(proxy: &NestedProxy) -> Vec<String> {
proxy.path.clone()
}
pub fn traverse_relationships(path: &RelationshipPath) -> Vec<String> {
path.segments.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relationship_path_no_cycle() {
let path = RelationshipPath::new()
.through("posts")
.through("comments")
.through("author")
.attribute("name");
assert_eq!(path.path().len(), 3);
assert_eq!(path.path()[0], "posts");
assert_eq!(path.path()[1], "comments");
assert_eq!(path.path()[2], "author");
assert_eq!(path.get_attribute(), "name");
}
#[test]
fn test_simple_cycle_detection() {
let path = RelationshipPath::new().through("posts").through("author");
let result = path.try_through("posts");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.relationship, "posts");
assert_eq!(err.path, vec!["posts", "author"]);
}
#[test]
fn test_complex_cycle_detection() {
let path = RelationshipPath::new()
.through("user")
.through("posts")
.through("comments");
let result = path.try_through("user");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.relationship, "user");
assert_eq!(err.path, vec!["user", "posts", "comments"]);
}
#[test]
fn test_no_false_positive_cycle() {
let path = RelationshipPath::new()
.through("posts")
.through("comments")
.through("author")
.through("profile");
assert_eq!(path.path().len(), 4);
}
#[test]
fn test_contains_relationship() {
let path = RelationshipPath::new().through("posts").through("comments");
assert!(path.contains("posts"));
assert!(path.contains("comments"));
assert!(!path.contains("author"));
}
#[test]
fn test_path_with_filters() {
let path = RelationshipPath::new()
.through("posts")
.with_filter("published", "true")
.through("comments")
.attribute("content");
assert!(path.has_filters());
assert_eq!(path.filters().len(), 1);
assert_eq!(
path.filters()[0],
("published".to_string(), "true".to_string())
);
}
#[test]
fn test_path_with_transforms() {
let path = RelationshipPath::new()
.through("posts")
.through("comments")
.with_transform("author", "upper")
.attribute("name");
assert!(path.has_transforms());
assert_eq!(path.transforms().len(), 1);
}
#[test]
fn test_multiple_filters() {
let path = RelationshipPath::new()
.through("posts")
.with_filter("published", "true")
.through("comments")
.with_filter("approved", "true")
.attribute("content");
assert!(path.has_filters());
assert_eq!(path.filters().len(), 2);
}
#[test]
fn test_error_message_format() {
let path = RelationshipPath::new().through("posts").through("author");
let result = path.try_through("posts");
assert!(result.is_err());
let err = result.unwrap_err();
let error_msg = err.to_string();
assert!(error_msg.contains("Circular reference detected"));
assert!(error_msg.contains("posts"));
assert!(error_msg.contains("author"));
}
#[test]
fn test_default_path() {
let path = RelationshipPath::default();
assert_eq!(path.path().len(), 0);
assert!(!path.has_filters());
assert!(!path.has_transforms());
}
#[test]
fn test_empty_path_error() {
let err = CircularReferenceError {
relationship: "posts".to_string(),
path: vec![],
};
let msg = err.to_string();
assert!(msg.contains("(empty)"));
}
#[test]
fn test_path_clone() {
let path1 = RelationshipPath::new()
.through("posts")
.through("comments")
.attribute("content");
let path2 = path1.clone();
assert_eq!(path1.path(), path2.path());
assert_eq!(path1.get_attribute(), path2.get_attribute());
}
#[test]
fn test_through_allows_cycles() {
let path = RelationshipPath::new()
.through("posts")
.through("author")
.through("posts");
assert_eq!(path.path().len(), 3);
assert!(path.contains("posts"));
}
#[test]
fn test_validate_method() {
let path = RelationshipPath::new()
.through("posts")
.through("comments")
.attribute("content");
let result = path.validate();
assert!(result.is_ok());
}
#[test]
fn test_attribute_only_path() {
let path = RelationshipPath::new().attribute("name");
assert_eq!(path.path().len(), 0);
assert_eq!(path.get_attribute(), "name");
}
#[test]
fn test_filter_on_empty_path() {
let path = RelationshipPath::new().with_filter("field", "value");
assert!(path.has_filters());
}
}