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("Fragment depth exceeded (max: {0})")]
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 resolved =
self.resolve_selections(&fragment.selections, depth + 1, visited_fragments)?;
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)
}
#[must_use]
pub fn evaluate_inline_fragment(
selections: &[FieldSelection],
type_condition: Option<&str>,
actual_type: &str,
) -> Vec<FieldSelection> {
if type_condition.is_none() {
return selections.to_vec();
}
if type_condition == Some(actual_type) {
selections.to_vec()
} else {
vec![]
}
}
#[must_use]
pub fn merge_selections(
base: &[FieldSelection],
additional: Vec<FieldSelection>,
) -> Vec<FieldSelection> {
let mut by_key: HashMap<String, FieldSelection> =
base.iter().map(|f| (f.response_key().to_string(), f.clone())).collect();
for field in additional {
let key = field.response_key().to_string();
if let Some(existing) = by_key.get_mut(&key) {
if !field.nested_fields.is_empty() {
existing.nested_fields.extend(field.nested_fields);
existing.nested_fields = Self::deduplicate_fields(&existing.nested_fields);
}
} else {
by_key.insert(key, field);
}
}
by_key.into_values().collect()
}
fn deduplicate_fields(fields: &[FieldSelection]) -> Vec<FieldSelection> {
let mut seen = HashSet::new();
fields
.iter()
.filter(|f| seen.insert(f.response_key().to_string()))
.cloned()
.collect()
}
}