libgraphql_core/operation/
fragment_registry_builder.rs

1use crate::ast;
2use crate::file_reader;
3use crate::loc;
4use crate::operation::Fragment;
5use crate::operation::FragmentBuilder;
6use crate::operation::FragmentBuildError;
7use crate::operation::FragmentRegistry;
8use crate::operation::Selection;
9use crate::operation::SelectionSet;
10use crate::schema::Schema;
11use std::collections::HashMap;
12use std::collections::HashSet;
13use std::path::Path;
14use std::sync::Arc;
15use thiserror::Error;
16
17type Result<T> = std::result::Result<T, Vec<FragmentRegistryBuildError>>;
18
19/// Builder for constructing a [`FragmentRegistry`] with validation.
20///
21/// The `FragmentRegistryBuilder` allows you to incrementally add fragments
22/// from multiple sources (files, strings, AST) and then build an immutable
23/// [`FragmentRegistry`] with comprehensive validation including cycle
24/// detection and reference checking.
25///
26/// # Example
27///
28/// ```
29/// use libgraphql_core::schema::SchemaBuilder;
30/// use libgraphql_core::operation::FragmentRegistryBuilder;
31///
32/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
33/// let schema = SchemaBuilder::from_str(
34///     None,
35///     "type Query { hello: String }"
36/// )?
37/// .build()?;
38///
39/// let mut builder = FragmentRegistryBuilder::new();
40///
41/// builder.add_from_document_str(
42///     &schema,
43///     "fragment UserFields on User { id name }",
44///     None
45/// ).unwrap();
46///
47/// builder.add_from_document_str(
48///     &schema,
49///     "fragment PostFields on Post { title body }",
50///     None
51/// ).unwrap();
52///
53/// let registry = builder.build().unwrap();
54/// # Ok(())
55/// # }
56/// ```
57#[derive(Debug)]
58pub struct FragmentRegistryBuilder<'schema> {
59    fragments: HashMap<String, Fragment<'schema>>,
60}
61
62impl<'schema> FragmentRegistryBuilder<'schema> {
63    /// Create a new empty `FragmentRegistryBuilder`.
64    pub fn new() -> Self {
65        Self {
66            fragments: HashMap::new(),
67        }
68    }
69
70    /// Add a pre-built fragment to the registry.
71    ///
72    /// Returns an error if a fragment with the same name already exists.
73    pub fn add_fragment(
74        &mut self,
75        fragment: Fragment<'schema>,
76    ) -> std::result::Result<(), FragmentRegistryBuildError> {
77        let name = fragment.name.clone();
78
79        if let Some(existing) = self.fragments.get(&name) {
80            return Err(FragmentRegistryBuildError::DuplicateFragmentDefinition {
81                fragment_name: name,
82                first_def_location: existing.def_location.clone(),
83                second_def_location: fragment.def_location.clone(),
84            });
85        }
86
87        self.fragments.insert(name, fragment);
88        Ok(())
89    }
90
91    /// Parse fragments from an AST document and add them to the builder.
92    ///
93    /// This method follows the pattern of [`QueryBuilder::from_ast`](crate::operation::QueryBuilder::from_ast).
94    ///
95    /// Only fragment definitions in the document are processed; operation
96    /// definitions are ignored.
97    pub fn add_from_document_ast(
98        &mut self,
99        schema: &'schema Schema,
100        ast: &ast::operation::Document,
101        file_path: Option<&Path>,
102    ) -> std::result::Result<(), Vec<FragmentBuildError>> {
103        let mut errors = vec![];
104
105        for def in &ast.definitions {
106            if let ast::operation::Definition::Fragment(frag_def) = def {
107                // Use empty registry for building - references validated later
108                let temp_registry = FragmentRegistry::empty();
109
110                match FragmentBuilder::from_ast(schema, temp_registry, frag_def, file_path)
111                    .and_then(|builder| builder.build())
112                {
113                    Ok(fragment) => {
114                        // Convert registry build error to fragment build error
115                        if let Err(FragmentRegistryBuildError::DuplicateFragmentDefinition {
116                            fragment_name,
117                            first_def_location,
118                            second_def_location,
119                        }) = self.add_fragment(fragment) {
120                            errors.push(FragmentBuildError::DuplicateFragmentDefinition {
121                                fragment_name,
122                                first_def_location,
123                                second_def_location,
124                            });
125                        }
126                    }
127                    Err(e) => errors.push(e),
128                }
129            }
130        }
131
132        if !errors.is_empty() {
133            return Err(errors);
134        }
135
136        Ok(())
137    }
138
139    /// Parse fragments from a file and add them to the builder.
140    ///
141    /// This method follows the pattern of [`QueryBuilder::from_file`](crate::operation::QueryBuilder::from_file).
142    pub fn add_from_document_file(
143        &mut self,
144        schema: &'schema Schema,
145        file_path: impl AsRef<Path>,
146    ) -> std::result::Result<(), Vec<FragmentBuildError>> {
147        let file_path = file_path.as_ref();
148        let file_content = file_reader::read_content(file_path)
149            .map_err(|e| vec![FragmentBuildError::FileReadError(Box::new(e))])?;
150
151        self.add_from_document_str(schema, file_content, Some(file_path))
152    }
153
154    /// Parse fragments from a string and add them to the builder.
155    ///
156    /// This method follows the pattern of [`QueryBuilder::from_str`](crate::operation::QueryBuilder::from_str).
157    pub fn add_from_document_str(
158        &mut self,
159        schema: &'schema Schema,
160        content: impl AsRef<str>,
161        file_path: Option<&Path>,
162    ) -> std::result::Result<(), Vec<FragmentBuildError>> {
163        let ast_doc = ast::operation::parse(content.as_ref())
164            .map_err(|e| vec![FragmentBuildError::ParseError(Arc::new(e))])?;
165
166        self.add_from_document_ast(schema, &ast_doc, file_path)
167    }
168
169    /// Build the immutable [`FragmentRegistry`] with comprehensive validation.
170    ///
171    /// This method performs the following validations:
172    /// - Detects cycles in fragment spreads
173    /// - Deduplicates phase-shifted cycles (e.g., A→B→C→A is the same as B→C→A→B)
174    /// - Validates that all fragment references exist
175    ///
176    /// If any validation errors are found, returns all errors at once rather
177    /// than failing on the first error.
178    pub fn build(self) -> Result<FragmentRegistry<'schema>> {
179        let mut errors = Vec::new();
180
181        // Collect all cycle errors
182        errors.extend(self.validate_no_cycles());
183
184        // Collect all undefined reference errors
185        errors.extend(self.validate_fragment_references());
186
187        if !errors.is_empty() {
188            return Err(errors);
189        }
190
191        Ok(FragmentRegistry {
192            fragments: self.fragments,
193        })
194    }
195
196    /// Validate that no cycles exist in fragment spreads.
197    ///
198    /// Uses DFS traversal with cycle normalization to detect and deduplicate
199    /// cycles. Phase-shifted cycles (rotations of the same cycle) are
200    /// deduplicated.
201    fn validate_no_cycles(&self) -> Vec<FragmentRegistryBuildError> {
202        let mut all_cycles = Vec::new();
203        let mut seen_normalized_cycles = HashSet::new();
204
205        for fragment_name in self.fragments.keys() {
206            let mut path = Vec::new();
207            let mut visiting = HashSet::new();
208
209            self.check_fragment_cycles(
210                fragment_name,
211                &mut path,
212                &mut visiting,
213                &mut all_cycles,
214                &mut seen_normalized_cycles,
215            );
216        }
217
218        all_cycles
219    }
220
221    fn check_fragment_cycles(
222        &self,
223        fragment_name: &str,
224        path: &mut Vec<String>,
225        visiting: &mut HashSet<String>,
226        errors: &mut Vec<FragmentRegistryBuildError>,
227        seen_normalized: &mut HashSet<Vec<String>>,
228    ) {
229        // Cycle detected
230        if visiting.contains(fragment_name) {
231            path.push(fragment_name.to_string());
232
233            // Normalize the cycle to check for duplicates
234            let normalized = Self::normalize_cycle(path);
235
236            // Only add if we haven't seen this cycle before (in any phase)
237            if !seen_normalized.contains(&normalized) {
238                seen_normalized.insert(normalized);
239                errors.push(FragmentRegistryBuildError::FragmentCycleDetected {
240                    cycle_path: path.clone(),
241                });
242            }
243
244            path.pop();
245            return;
246        }
247
248        // Fragment doesn't exist - will be caught by reference validation
249        let Some(fragment) = self.fragments.get(fragment_name) else {
250            return;
251        };
252
253        path.push(fragment_name.to_string());
254        visiting.insert(fragment_name.to_string());
255
256        // Recursively check all fragment spreads
257        self.check_selection_set_cycles(
258            &fragment.selection_set,
259            path,
260            visiting,
261            errors,
262            seen_normalized,
263        );
264
265        path.pop();
266        visiting.remove(fragment_name);
267    }
268
269    fn check_selection_set_cycles(
270        &self,
271        selection_set: &SelectionSet<'schema>,
272        path: &mut Vec<String>,
273        visiting: &mut HashSet<String>,
274        errors: &mut Vec<FragmentRegistryBuildError>,
275        seen_normalized: &mut HashSet<Vec<String>>,
276    ) {
277        for selection in &selection_set.selections {
278            match selection {
279                Selection::FragmentSpread(spread) => {
280                    self.check_fragment_cycles(
281                        spread.fragment_name(),
282                        path,
283                        visiting,
284                        errors,
285                        seen_normalized,
286                    );
287                }
288                Selection::InlineFragment(inline) => {
289                    self.check_selection_set_cycles(
290                        inline.selection_set(),
291                        path,
292                        visiting,
293                        errors,
294                        seen_normalized,
295                    );
296                }
297                Selection::Field(field) => {
298                    if let Some(nested_set) = field.selection_set() {
299                        self.check_selection_set_cycles(
300                            nested_set,
301                            path,
302                            visiting,
303                            errors,
304                            seen_normalized,
305                        );
306                    }
307                }
308            }
309        }
310    }
311
312    /// Validate that all fragment references point to existing fragments.
313    fn validate_fragment_references(&self) -> Vec<FragmentRegistryBuildError> {
314        let mut errors = Vec::new();
315
316        for (fragment_name, fragment) in &self.fragments {
317            self.check_fragment_refs_in_selection_set(
318                fragment_name,
319                &fragment.selection_set,
320                &mut errors,
321            );
322        }
323
324        errors
325    }
326
327    fn check_fragment_refs_in_selection_set(
328        &self,
329        parent_fragment: &str,
330        selection_set: &SelectionSet<'schema>,
331        errors: &mut Vec<FragmentRegistryBuildError>,
332    ) {
333        for selection in &selection_set.selections {
334            match selection {
335                Selection::FragmentSpread(spread) => {
336                    let ref_name = spread.fragment_name();
337                    if !self.fragments.contains_key(ref_name) {
338                        errors.push(FragmentRegistryBuildError::UndefinedFragmentReference {
339                            fragment_name: parent_fragment.to_string(),
340                            undefined_fragment: ref_name.to_string(),
341                            reference_location: spread.def_location.clone(),
342                        });
343                    }
344                }
345                Selection::InlineFragment(inline) => {
346                    self.check_fragment_refs_in_selection_set(
347                        parent_fragment,
348                        inline.selection_set(),
349                        errors,
350                    );
351                }
352                Selection::Field(field) => {
353                    if let Some(nested_set) = field.selection_set() {
354                        self.check_fragment_refs_in_selection_set(
355                            parent_fragment,
356                            nested_set,
357                            errors,
358                        );
359                    }
360                }
361            }
362        }
363    }
364
365    /// Normalize a cycle to canonical form for deduplication.
366    ///
367    /// Cycles that are rotations of each other are considered identical.
368    /// For example, `[A, B, C, A]`, `[B, C, A, B]`, and `[C, A, B, C]` are
369    /// all the same cycle.
370    ///
371    /// Normalization rotates the cycle to start with the lexicographically
372    /// smallest fragment name.
373    ///
374    /// # Examples
375    ///
376    /// ```ignore
377    /// normalize_cycle(&["B", "C", "A", "B"]) // => ["A", "B", "C", "A"]
378    /// normalize_cycle(&["C", "A", "B", "C"]) // => ["A", "B", "C", "A"]
379    /// ```
380    fn normalize_cycle(cycle: &[String]) -> Vec<String> {
381        if cycle.is_empty() {
382            return Vec::new();
383        }
384
385        // Remove the duplicate last element: [A, B, C, A] → [A, B, C]
386        let cycle_without_repeat = &cycle[..cycle.len() - 1];
387
388        // Find the position of the lexicographically smallest fragment
389        let min_idx = cycle_without_repeat
390            .iter()
391            .enumerate()
392            .min_by(|(_, a), (_, b)| a.cmp(b))
393            .map(|(idx, _)| idx)
394            .unwrap_or(0);
395
396        // Rotate to start from the minimum
397        let mut normalized = Vec::new();
398        normalized.extend_from_slice(&cycle_without_repeat[min_idx..]);
399        normalized.extend_from_slice(&cycle_without_repeat[..min_idx]);
400
401        // Add back the duplicate last element
402        normalized.push(normalized[0].clone());
403
404        normalized
405    }
406}
407
408impl<'schema> Default for FragmentRegistryBuilder<'schema> {
409    fn default() -> Self {
410        Self::new()
411    }
412}
413
414#[derive(Clone, Debug, Error)]
415pub enum FragmentRegistryBuildError {
416    #[error("Duplicate fragment definition: '{fragment_name}'")]
417    DuplicateFragmentDefinition {
418        fragment_name: String,
419        first_def_location: loc::SourceLocation,
420        second_def_location: loc::SourceLocation,
421    },
422
423    #[error("Fragment cycle detected: {}", format_cycle_path(.cycle_path))]
424    FragmentCycleDetected { cycle_path: Vec<String> },
425
426    #[error("Fragment '{fragment_name}' references undefined fragment '{undefined_fragment}'")]
427    UndefinedFragmentReference {
428        fragment_name: String,
429        undefined_fragment: String,
430        reference_location: loc::SourceLocation,
431    },
432}
433
434fn format_cycle_path(cycle: &[String]) -> String {
435    cycle.join(" → ")
436}