use std::collections::{HashMap, HashSet};
use thiserror::Error;
use crate::graphql::types::{FieldSelection, FragmentDefinition};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FragmentError {
#[error("Fragment not found: {0}")]
FragmentNotFound(String),
#[error("Selection nests deeper than the {0}-level limit")]
FragmentDepthExceeded(u32),
#[error("Circular fragment reference detected")]
CircularFragmentReference,
}
pub struct FragmentResolver {
fragments: HashMap<String, FragmentDefinition>,
max_depth: u32,
}
impl FragmentResolver {
#[must_use]
pub fn new(fragments: &[FragmentDefinition]) -> Self {
let map = fragments.iter().map(|f| (f.name.clone(), f.clone())).collect();
Self {
fragments: map,
max_depth: 10,
}
}
#[must_use]
pub const fn with_max_depth(mut self, max_depth: u32) -> Self {
self.max_depth = max_depth;
self
}
pub fn resolve_spreads(
&self,
selections: &[FieldSelection],
) -> Result<Vec<FieldSelection>, FragmentError> {
self.resolve_selections(selections, 0, &mut HashSet::new())
}
fn resolve_selections(
&self,
selections: &[FieldSelection],
depth: u32,
visited_fragments: &mut HashSet<String>,
) -> Result<Vec<FieldSelection>, FragmentError> {
if depth > self.max_depth {
return Err(FragmentError::FragmentDepthExceeded(self.max_depth));
}
let mut result = Vec::new();
for selection in selections {
if let Some(fragment_name) = selection.name.strip_prefix("...") {
if fragment_name.starts_with("on ") {
let mut field = selection.clone();
if !field.nested_fields.is_empty() {
field.nested_fields = self.resolve_selections(
&field.nested_fields,
depth + 1,
visited_fragments,
)?;
}
result.push(field);
continue;
}
let fragment_name = fragment_name.to_string();
if visited_fragments.contains(&fragment_name) {
return Err(FragmentError::CircularFragmentReference);
}
let fragment = self
.fragments
.get(&fragment_name)
.ok_or_else(|| FragmentError::FragmentNotFound(fragment_name.clone()))?;
visited_fragments.insert(fragment_name.clone());
let mut resolved =
self.resolve_selections(&fragment.selections, depth + 1, visited_fragments)?;
for field in &mut resolved {
field.inherit_directives(&selection.directives);
}
result.extend(resolved);
visited_fragments.remove(&fragment_name);
} else {
let mut field = selection.clone();
if !field.nested_fields.is_empty() {
field.nested_fields = self.resolve_selections(
&field.nested_fields,
depth + 1,
visited_fragments,
)?;
}
result.push(field);
}
}
Ok(result)
}
}