Skip to main content

fraiseql_core/graphql/
fragment_resolver.rs

1//! Fragment resolution for GraphQL queries.
2//!
3//! Handles:
4//! - Fragment spread resolution (`...FragmentName`)
5//! - Inline fragment handling (`... on TypeName { fields }`)
6//! - Selection set merging with deduplication
7
8use std::collections::{HashMap, HashSet};
9
10use thiserror::Error;
11
12use crate::graphql::types::{FieldSelection, FragmentDefinition};
13
14/// Errors that can occur during fragment resolution.
15#[derive(Debug, Error)]
16#[non_exhaustive]
17pub enum FragmentError {
18    /// Indicates that the requested fragment was not found.
19    #[error("Fragment not found: {0}")]
20    FragmentNotFound(String),
21
22    /// Indicates that fragment depth limit was exceeded.
23    #[error("Fragment depth exceeded (max: {0})")]
24    FragmentDepthExceeded(u32),
25
26    /// Indicates a circular reference was detected in fragments.
27    #[error("Circular fragment reference detected")]
28    CircularFragmentReference,
29}
30
31/// Resolves GraphQL fragment spreads in query selection sets.
32///
33/// Handles fragment spreads (`...FragmentName`) by expanding them to their actual field selections.
34/// Also merges multiple fragment definitions and handles field deduplication.
35///
36/// # Example
37///
38/// ```
39/// use fraiseql_core::graphql::{FragmentResolver, FragmentDefinition, FieldSelection};
40///
41/// let fragment = FragmentDefinition {
42///     name: "UserFields".to_string(),
43///     type_condition: "User".to_string(),
44///     selections: vec![
45///         FieldSelection {
46///             name: "id".to_string(),
47///             alias: None,
48///             arguments: vec![],
49///             nested_fields: vec![],
50///             directives: vec![],
51///         },
52///     ],
53///     fragment_spreads: vec![],
54/// };
55///
56/// let resolver = FragmentResolver::new(&[fragment]);
57/// ```
58pub struct FragmentResolver {
59    fragments: HashMap<String, FragmentDefinition>,
60    max_depth: u32,
61}
62
63impl FragmentResolver {
64    /// Create a new fragment resolver from a list of fragment definitions.
65    #[must_use]
66    pub fn new(fragments: &[FragmentDefinition]) -> Self {
67        let map = fragments.iter().map(|f| (f.name.clone(), f.clone())).collect();
68        Self {
69            fragments: map,
70            max_depth: 10,
71        }
72    }
73
74    /// Create a resolver with a custom max depth.
75    #[must_use]
76    pub const fn with_max_depth(mut self, max_depth: u32) -> Self {
77        self.max_depth = max_depth;
78        self
79    }
80
81    /// Resolve all fragment spreads in selections.
82    ///
83    /// # Errors
84    /// Returns error if:
85    /// - Fragment is not found
86    /// - Fragment depth exceeds maximum
87    /// - Circular references are detected
88    pub fn resolve_spreads(
89        &self,
90        selections: &[FieldSelection],
91    ) -> Result<Vec<FieldSelection>, FragmentError> {
92        self.resolve_selections(selections, 0, &mut HashSet::new())
93    }
94
95    /// Recursively resolve selections at a given depth.
96    fn resolve_selections(
97        &self,
98        selections: &[FieldSelection],
99        depth: u32,
100        visited_fragments: &mut HashSet<String>,
101    ) -> Result<Vec<FieldSelection>, FragmentError> {
102        if depth > self.max_depth {
103            return Err(FragmentError::FragmentDepthExceeded(self.max_depth));
104        }
105
106        let mut result = Vec::new();
107
108        for selection in selections {
109            // Check if this is a fragment spread (starts with "...")
110            if let Some(fragment_name) = selection.name.strip_prefix("...") {
111                // Skip inline fragments (they have " on " in the name)
112                if fragment_name.starts_with("on ") {
113                    // Inline fragment — counts as a nesting level (depth + 1) so that
114                    // deeply-nested inline fragments cannot bypass the depth limit.
115                    let mut field = selection.clone();
116                    if !field.nested_fields.is_empty() {
117                        field.nested_fields = self.resolve_selections(
118                            &field.nested_fields,
119                            depth + 1,
120                            visited_fragments,
121                        )?;
122                    }
123                    result.push(field);
124                    continue;
125                }
126
127                // Named fragment spread
128                let fragment_name = fragment_name.to_string();
129
130                // Detect circular references
131                if visited_fragments.contains(&fragment_name) {
132                    return Err(FragmentError::CircularFragmentReference);
133                }
134
135                // Get fragment definition
136                let fragment = self
137                    .fragments
138                    .get(&fragment_name)
139                    .ok_or_else(|| FragmentError::FragmentNotFound(fragment_name.clone()))?;
140
141                // Mark as visited
142                visited_fragments.insert(fragment_name.clone());
143
144                // Recursively resolve the fragment's selections
145                let resolved =
146                    self.resolve_selections(&fragment.selections, depth + 1, visited_fragments)?;
147                result.extend(resolved);
148
149                // Unmark for other paths
150                visited_fragments.remove(&fragment_name);
151            } else {
152                // Regular field: nested fields are one level deeper.
153                let mut field = selection.clone();
154                if !field.nested_fields.is_empty() {
155                    field.nested_fields = self.resolve_selections(
156                        &field.nested_fields,
157                        depth + 1,
158                        visited_fragments,
159                    )?;
160                }
161                result.push(field);
162            }
163        }
164
165        Ok(result)
166    }
167
168    /// Handle inline fragments with type conditions.
169    ///
170    /// Evaluates whether an inline fragment applies based on type conditions.
171    /// Returns the selections if the type condition matches, or an empty vector if it doesn't.
172    #[must_use]
173    pub fn evaluate_inline_fragment(
174        selections: &[FieldSelection],
175        type_condition: Option<&str>,
176        actual_type: &str,
177    ) -> Vec<FieldSelection> {
178        // If no type condition, inline fragment applies to all types
179        if type_condition.is_none() {
180            return selections.to_vec();
181        }
182
183        // If type condition matches actual type, include the fields
184        if type_condition == Some(actual_type) {
185            selections.to_vec()
186        } else {
187            // Type condition doesn't match - skip these fields
188            vec![]
189        }
190    }
191
192    /// Merge field selections from multiple sources (e.g., fragment spreads).
193    ///
194    /// Handles:
195    /// - Combining fields from multiple fragments
196    /// - Deduplicating fields by name/alias
197    /// - Merging nested selections
198    #[must_use]
199    pub fn merge_selections(
200        base: &[FieldSelection],
201        additional: Vec<FieldSelection>,
202    ) -> Vec<FieldSelection> {
203        // Build map of existing fields by response key (alias or name)
204        let mut by_key: HashMap<String, FieldSelection> =
205            base.iter().map(|f| (f.response_key().to_string(), f.clone())).collect();
206
207        // Merge additional fields
208        for field in additional {
209            let key = field.response_key().to_string();
210            if let Some(existing) = by_key.get_mut(&key) {
211                // Field already exists - merge nested selections
212                if !field.nested_fields.is_empty() {
213                    existing.nested_fields.extend(field.nested_fields);
214                    // Deduplicate nested fields
215                    existing.nested_fields = Self::deduplicate_fields(&existing.nested_fields);
216                }
217            } else {
218                // New field - add it
219                by_key.insert(key, field);
220            }
221        }
222
223        by_key.into_values().collect()
224    }
225
226    /// Deduplicate fields in a selection set by response key.
227    fn deduplicate_fields(fields: &[FieldSelection]) -> Vec<FieldSelection> {
228        let mut seen = HashSet::new();
229        fields
230            .iter()
231            .filter(|f| seen.insert(f.response_key().to_string()))
232            .cloned()
233            .collect()
234    }
235}