Skip to main content

apollo_smith/
fragment.rs

1use crate::directive::Directive;
2use crate::directive::DirectiveLocation;
3use crate::name::Name;
4use crate::operation::OperationDef;
5use crate::selection_set::SelectionSet;
6use crate::ty::Ty;
7use crate::DocumentBuilder;
8use apollo_compiler::ast;
9use arbitrary::Result as ArbitraryResult;
10use indexmap::IndexMap;
11use indexmap::IndexSet;
12
13/// The __fragmentDef type represents a fragment definition
14///
15/// *FragmentDefinition*:
16///     fragment FragmentName TypeCondition Directives? SelectionSet
17///
18/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#FragmentDefinition).
19#[derive(Debug, Clone)]
20pub struct FragmentDef {
21    pub(crate) name: Name,
22    pub(crate) type_condition: TypeCondition,
23    pub(crate) directives: IndexMap<Name, Directive>,
24    pub(crate) selection_set: SelectionSet,
25}
26
27impl From<FragmentDef> for ast::Definition {
28    fn from(x: FragmentDef) -> Self {
29        ast::FragmentDefinition {
30            name: x.name.into(),
31            type_condition: x.type_condition.name.into(),
32            directives: Directive::to_ast(x.directives),
33            selection_set: x.selection_set.into(),
34        }
35        .into()
36    }
37}
38
39impl TryFrom<apollo_parser::cst::FragmentDefinition> for FragmentDef {
40    type Error = crate::FromError;
41
42    fn try_from(fragment_def: apollo_parser::cst::FragmentDefinition) -> Result<Self, Self::Error> {
43        Ok(Self {
44            name: fragment_def.fragment_name().unwrap().name().unwrap().into(),
45            directives: fragment_def
46                .directives()
47                .map(Directive::convert_directives)
48                .transpose()?
49                .unwrap_or_default(),
50            type_condition: fragment_def.type_condition().unwrap().into(),
51            selection_set: fragment_def.selection_set().unwrap().try_into()?,
52        })
53    }
54}
55
56/// The __fragmentSpread type represents a named fragment used in a selection set.
57///
58/// *FragmentSpread*:
59///     ... FragmentName Directives?
60///
61/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#FragmentSpread).
62#[derive(Debug, Clone)]
63pub struct FragmentSpread {
64    pub(crate) name: Name,
65    pub(crate) directives: IndexMap<Name, Directive>,
66}
67
68impl From<FragmentSpread> for ast::FragmentSpread {
69    fn from(x: FragmentSpread) -> Self {
70        Self {
71            fragment_name: x.name.into(),
72            directives: Directive::to_ast(x.directives),
73        }
74    }
75}
76
77impl TryFrom<apollo_parser::cst::FragmentSpread> for FragmentSpread {
78    type Error = crate::FromError;
79
80    fn try_from(fragment_spread: apollo_parser::cst::FragmentSpread) -> Result<Self, Self::Error> {
81        Ok(Self {
82            name: fragment_spread
83                .fragment_name()
84                .unwrap()
85                .name()
86                .unwrap()
87                .into(),
88            directives: fragment_spread
89                .directives()
90                .map(Directive::convert_directives)
91                .transpose()?
92                .unwrap_or_default(),
93        })
94    }
95}
96
97/// The __inlineFragment type represents an inline fragment in a selection set that could be used as a field
98///
99/// *InlineFragment*:
100///     ... TypeCondition? Directives? SelectionSet
101///
102/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#sec-Inline-Fragments).
103#[derive(Debug, Clone)]
104pub struct InlineFragment {
105    pub(crate) type_condition: Option<TypeCondition>,
106    pub(crate) directives: IndexMap<Name, Directive>,
107    pub(crate) selection_set: SelectionSet,
108}
109
110impl From<InlineFragment> for ast::InlineFragment {
111    fn from(x: InlineFragment) -> Self {
112        Self {
113            type_condition: x.type_condition.map(|t| t.name.into()),
114            directives: Directive::to_ast(x.directives),
115            selection_set: x.selection_set.into(),
116        }
117    }
118}
119
120impl TryFrom<apollo_parser::cst::InlineFragment> for InlineFragment {
121    type Error = crate::FromError;
122
123    fn try_from(inline_fragment: apollo_parser::cst::InlineFragment) -> Result<Self, Self::Error> {
124        Ok(Self {
125            directives: inline_fragment
126                .directives()
127                .map(Directive::convert_directives)
128                .transpose()?
129                .unwrap_or_default(),
130            selection_set: inline_fragment.selection_set().unwrap().try_into()?,
131            type_condition: inline_fragment.type_condition().map(TypeCondition::from),
132        })
133    }
134}
135
136/// The __typeCondition type represents where a fragment could be applied
137///
138/// *TypeCondition*:
139///     on NamedType
140///
141/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#TypeCondition).
142#[derive(Debug, Clone)]
143pub struct TypeCondition {
144    name: Name,
145}
146
147impl From<apollo_parser::cst::TypeCondition> for TypeCondition {
148    fn from(type_condition: apollo_parser::cst::TypeCondition) -> Self {
149        Self {
150            name: type_condition.named_type().unwrap().name().unwrap().into(),
151        }
152    }
153}
154
155impl DocumentBuilder<'_> {
156    /// Create an arbitrary `FragmentDef`
157    pub fn fragment_definition(&mut self) -> ArbitraryResult<FragmentDef> {
158        // TODO: also choose between enum/scalars/object
159        let selected_object_type_name = self.u.choose(&self.object_type_defs)?.name.clone();
160        let _ = self.stack_ty(&Ty::Named(selected_object_type_name));
161        let name = self.type_name()?;
162        let directives = self.directives(DirectiveLocation::FragmentDefinition)?;
163        let selection_set = self.selection_set()?;
164        let type_condition = self.type_condition()?;
165        self.stack.pop();
166
167        Ok(FragmentDef {
168            name,
169            type_condition,
170            directives,
171            selection_set,
172        })
173    }
174
175    /// Create an arbitrary `FragmentSpread`, returns `None` if no fragment definition was previously created
176    pub fn fragment_spread(
177        &mut self,
178        excludes: &mut Vec<Name>,
179    ) -> ArbitraryResult<Option<FragmentSpread>> {
180        let current_type = self.stack.last().map(|e| e.name().clone());
181        let available_fragment: Vec<&FragmentDef> = self
182            .fragment_defs
183            .iter()
184            .filter(|f| {
185                !excludes.contains(&f.name)
186                    && self.fragment_spread_possible(&f.type_condition.name, current_type.as_ref())
187            })
188            .collect();
189
190        let name = if available_fragment.is_empty() {
191            return Ok(None);
192        } else {
193            self.u.choose(&available_fragment)?.name.clone()
194        };
195        let directives = self.directives(DirectiveLocation::FragmentSpread)?;
196        excludes.push(name.clone());
197
198        Ok(Some(FragmentSpread { name, directives }))
199    }
200
201    /// Create an arbitrary `InlineFragment`
202    pub fn inline_fragment(&mut self) -> ArbitraryResult<InlineFragment> {
203        let type_condition = self
204            .u
205            .arbitrary()
206            .unwrap_or(false)
207            .then(|| self.type_condition())
208            .transpose()?;
209        let selection_set = self.selection_set()?;
210        let directives = self.directives(DirectiveLocation::InlineFragment)?;
211
212        Ok(InlineFragment {
213            type_condition,
214            directives,
215            selection_set,
216        })
217    }
218
219    /// Whether a fragment with `fragment_type` can be spread inside a
220    /// selection set for `current_type`. The two types must share at
221    /// least one possible object type.
222    ///
223    /// See <https://spec.graphql.org/October2021/#sec-Fragment-spread-is-possible>.
224    fn fragment_spread_possible(&self, fragment_type: &Name, current_type: Option<&Name>) -> bool {
225        let Some(current) = current_type else {
226            return true;
227        };
228        let current_objects = self.possible_object_types(current);
229        let fragment_objects = self.possible_object_types(fragment_type);
230        current_objects.iter().any(|o| fragment_objects.contains(o))
231    }
232
233    /// The set of object types that `type_name` can resolve to at runtime.
234    fn possible_object_types(&self, type_name: &Name) -> IndexSet<Name> {
235        if self.object_type_defs.iter().any(|o| &o.name == type_name) {
236            return IndexSet::from([type_name.clone()]);
237        }
238        if let Some(u) = self.union_type_defs.iter().find(|u| &u.name == type_name) {
239            return u.members.clone();
240        }
241        // Interface: collect every object whose implements closure includes it
242        self.object_type_defs
243            .iter()
244            .filter(|o| self.implements_graph.closure(&o.name).contains(type_name))
245            .map(|o| o.name.clone())
246            .collect()
247    }
248
249    /// Create an arbitrary `TypeCondition`
250    pub fn type_condition(&mut self) -> ArbitraryResult<TypeCondition> {
251        let last_element = self.stack.last();
252        match last_element {
253            Some(last_element) => Ok(TypeCondition {
254                name: last_element.name().clone(),
255            }),
256            None => {
257                let named_types: Vec<Ty> = self
258                    .list_existing_object_types()
259                    .into_iter()
260                    .filter(Ty::is_named)
261                    .collect();
262
263                Ok(TypeCondition {
264                    name: self.choose_named_ty(&named_types)?.name().clone(),
265                })
266            }
267        }
268    }
269}
270
271/// Compute the set of fragment names reachable from `operations`, walking
272/// through `fragments` transitively when one fragment spreads another.
273///
274/// A fragment is reachable iff some operation spreads it directly, or spreads
275/// some other reachable fragment whose chain leads to it. Chains like
276/// `A -> B` with no operation referencing A produce no reachable names, even
277/// though `A` syntactically references `B`.
278pub(crate) fn reachable_fragment_names(
279    operations: &[OperationDef],
280    fragments: &[FragmentDef],
281) -> IndexSet<Name> {
282    let mut reachable: IndexSet<Name> = IndexSet::new();
283    for op in operations {
284        op.selection_set.collect_fragment_spreads(&mut reachable);
285    }
286    let mut frontier: Vec<Name> = reachable.iter().cloned().collect();
287    while let Some(name) = frontier.pop() {
288        if let Some(frag) = fragments.iter().find(|f| f.name == name) {
289            let mut nested: IndexSet<Name> = IndexSet::new();
290            frag.selection_set.collect_fragment_spreads(&mut nested);
291            for n in nested {
292                if reachable.insert(n.clone()) {
293                    frontier.push(n);
294                }
295            }
296        }
297    }
298    reachable
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    fn parse(src: &str) -> (Vec<OperationDef>, Vec<FragmentDef>) {
306        let cst = apollo_parser::Parser::new(src).parse();
307        assert!(cst.errors().next().is_none(), "parse errors: {src}");
308        let mut ops = vec![];
309        let mut frags = vec![];
310        for def in cst.document().definitions() {
311            match def {
312                apollo_parser::cst::Definition::OperationDefinition(o) => {
313                    ops.push(o.try_into().unwrap())
314                }
315                apollo_parser::cst::Definition::FragmentDefinition(f) => {
316                    frags.push(f.try_into().unwrap())
317                }
318                _ => panic!("unexpected definition in test input"),
319            }
320        }
321        (ops, frags)
322    }
323
324    fn names(items: &[&str]) -> IndexSet<Name> {
325        items.iter().map(|s| Name::new(s.to_string())).collect()
326    }
327
328    #[test]
329    fn no_operations_means_nothing_reachable() {
330        let (ops, frags) = parse("fragment A on T { __typename }");
331        let result = reachable_fragment_names(&ops, &frags);
332        assert!(result.is_empty());
333    }
334
335    #[test]
336    fn direct_spread_is_reachable() {
337        let (ops, frags) = parse(
338            "
339            query { ...A }
340            fragment A on T { __typename }
341            ",
342        );
343        let result = reachable_fragment_names(&ops, &frags);
344        assert_eq!(result, names(&["A"]));
345    }
346
347    #[test]
348    fn transitive_chain_is_reachable() {
349        let (ops, frags) = parse(
350            "
351            query { ...A }
352            fragment A on T { ...B }
353            fragment B on T { ...C }
354            fragment C on T { __typename }
355            ",
356        );
357        let result = reachable_fragment_names(&ops, &frags);
358        assert_eq!(result, names(&["A", "B", "C"]));
359    }
360
361    #[test]
362    fn orphan_chain_is_not_reachable() {
363        // A -> B with no operation referencing A: neither retained
364        let (ops, frags) = parse(
365            "
366            fragment A on T { ...B }
367            fragment B on T { __typename }
368            ",
369        );
370        let result = reachable_fragment_names(&ops, &frags);
371        assert!(result.is_empty());
372    }
373
374    #[test]
375    fn unreferenced_fragment_among_used_ones_is_pruned() {
376        let (ops, frags) = parse(
377            "
378            query { ...A }
379            fragment A on T { __typename }
380            fragment B on T { __typename }
381            ",
382        );
383        let result = reachable_fragment_names(&ops, &frags);
384        assert_eq!(result, names(&["A"]));
385    }
386
387    #[test]
388    fn cycle_terminates() {
389        // op -> A -> B -> A : both reachable, no infinite loop
390        let (ops, frags) = parse(
391            "
392            query { ...A }
393            fragment A on T { ...B }
394            fragment B on T { ...A }
395            ",
396        );
397        let result = reachable_fragment_names(&ops, &frags);
398        assert_eq!(result, names(&["A", "B"]));
399    }
400}