cynic/queries/
builders.rs

1use std::{borrow::Cow, collections::HashSet, marker::PhantomData, sync::mpsc::Sender};
2
3use crate::{coercions::CoercesTo, schema, variables::VariableDefinition, QueryVariableLiterals};
4
5use super::{ast::*, to_input_literal, FlattensInto, IsFieldType, Recursable};
6
7// The maximum depth we'll recurse to before assuming something is wrong
8// and giving up
9const MAX_DEPTH: u16 = 4096;
10
11/// Builds a SelectionSet for the given `SchemaType` and `VariablesFields`
12pub struct SelectionBuilder<'a, SchemaType, VariablesFields> {
13    phantom: PhantomData<fn() -> (SchemaType, VariablesFields)>,
14    selection_set: &'a mut SelectionSet,
15    has_typename: bool,
16    context: BuilderContext<'a>,
17}
18
19impl<'a, T, U> SelectionBuilder<'a, Vec<T>, U> {
20    pub(crate) fn into_inner(self) -> SelectionBuilder<'a, T, U> {
21        SelectionBuilder {
22            selection_set: self.selection_set,
23            has_typename: self.has_typename,
24            phantom: PhantomData,
25            context: self.context,
26        }
27    }
28}
29
30impl<'a, T, U> SelectionBuilder<'a, Option<T>, U> {
31    pub(crate) fn into_inner(self) -> SelectionBuilder<'a, T, U> {
32        SelectionBuilder {
33            selection_set: self.selection_set,
34            has_typename: self.has_typename,
35            phantom: PhantomData,
36            context: self.context,
37        }
38    }
39}
40
41impl<'a, SchemaType, VariablesFields> SelectionBuilder<'a, SchemaType, VariablesFields> {
42    pub(crate) fn new(
43        selection_set: &'a mut SelectionSet,
44        variables_used: &'a Sender<&'static str>,
45        features_enabled: &'a HashSet<String>,
46        inline_variables: Option<&'a dyn QueryVariableLiterals>,
47    ) -> Self {
48        SelectionBuilder::private_new(
49            selection_set,
50            BuilderContext {
51                recurse_depth: None,
52                overall_depth: 0,
53                features_enabled,
54                variables_used,
55                inline_variables,
56            },
57        )
58    }
59
60    fn private_new(selection_set: &'a mut SelectionSet, context: BuilderContext<'a>) -> Self {
61        SelectionBuilder {
62            phantom: PhantomData,
63            has_typename: false,
64            selection_set,
65            context,
66        }
67    }
68
69    /// Selects a field applying the flattening rules to its type.
70    ///
71    /// Note that the deserialization for this query must also
72    /// implement these rules if calling this function.
73    pub fn select_flattened_field<FieldMarker, Flattened, FieldType>(
74        &'_ mut self,
75    ) -> FieldSelectionBuilder<'_, FieldMarker, Flattened, VariablesFields>
76    where
77        FieldMarker: schema::Field,
78        FieldType: FlattensInto<Flattened>,
79        SchemaType: schema::HasField<FieldMarker>,
80        FieldType: IsFieldType<SchemaType::Type>,
81    {
82        FieldSelectionBuilder {
83            context: self.context,
84            field: self.push_selection(FieldMarker::NAME),
85            phantom: PhantomData,
86        }
87    }
88
89    /// Adds the `FieldMarker` field into this selection, with the given
90    /// `FieldType`.  Will type error if the field is not applicable or is
91    /// not of this type.
92    ///
93    /// This returns a `FieldSelectionBuilder` that can be used to apply
94    /// arguments, aliases, and to build an inner selection.
95    pub fn select_field<FieldMarker, FieldType>(
96        &'_ mut self,
97    ) -> FieldSelectionBuilder<'_, FieldMarker, FieldType, VariablesFields>
98    where
99        FieldMarker: schema::Field,
100        SchemaType: schema::HasField<FieldMarker>,
101        FieldType: IsFieldType<SchemaType::Type>,
102    {
103        FieldSelectionBuilder {
104            context: self.context,
105            field: self.push_selection(FieldMarker::NAME),
106            phantom: PhantomData,
107        }
108    }
109
110    /// Recursively selects a field into this selection, with the given
111    /// `FieldMarker` and `FieldType`, up to the given `max_depth`.
112    ///
113    /// This will return a `None` if we have reached the max_depth.
114    pub fn recurse<FieldMarker, FieldType>(
115        &'_ mut self,
116        max_depth: u8,
117    ) -> Option<FieldSelectionBuilder<'_, FieldMarker, FieldType, VariablesFields>>
118    where
119        FieldMarker: schema::Field,
120        SchemaType: schema::HasField<FieldMarker>,
121        FieldType: Recursable<FieldMarker::Type>,
122    {
123        let context = self.context.recurse();
124        let new_depth = context.recurse_depth.unwrap();
125        if new_depth >= max_depth {
126            return None;
127        }
128
129        Some(FieldSelectionBuilder {
130            context,
131            field: self.push_selection(FieldMarker::NAME),
132            phantom: PhantomData,
133        })
134    }
135
136    fn push_selection(&'_ mut self, name: &'static str) -> &mut FieldSelection {
137        self.selection_set
138            .selections
139            .push(Selection::Field(FieldSelection::new(name)));
140
141        match self.selection_set.selections.last_mut() {
142            Some(Selection::Field(field_selection)) => field_selection,
143            _ => panic!("This should not be possible"),
144        }
145    }
146
147    /// Adds an inline fragment to the SelectionSet
148    pub fn inline_fragment(&'_ mut self) -> InlineFragmentBuilder<'_, SchemaType, VariablesFields> {
149        if !self.has_typename {
150            self.selection_set
151                .selections
152                .push(Selection::Field(FieldSelection::new("__typename")));
153            self.has_typename = true;
154        }
155
156        self.selection_set
157            .selections
158            .push(Selection::InlineFragment(InlineFragment::default()));
159
160        let inline_fragment = match self.selection_set.selections.last_mut() {
161            Some(Selection::InlineFragment(inline_fragment)) => inline_fragment,
162            _ => panic!("This should not be possible"),
163        };
164
165        InlineFragmentBuilder {
166            inline_fragment,
167            phantom: PhantomData,
168            context: self.context,
169        }
170    }
171
172    /// Checks if a feature has been enabled for this operation.
173    ///
174    /// QueryFragment implementations can use this to avoid sending parts of
175    /// queries to servers that aren't going to understand them.
176    pub fn is_feature_enabled(&self, feature: &str) -> bool {
177        self.context.features_enabled.contains(feature)
178    }
179}
180
181/// Builds the selection of a field
182pub struct FieldSelectionBuilder<'a, Field, SchemaType, VariablesFields> {
183    #[allow(clippy::type_complexity)]
184    phantom: PhantomData<fn() -> (Field, SchemaType, VariablesFields)>,
185    field: &'a mut FieldSelection,
186    context: BuilderContext<'a>,
187}
188
189impl<Field, FieldSchemaType, VariablesFields>
190    FieldSelectionBuilder<'_, Field, FieldSchemaType, VariablesFields>
191{
192    /// Adds an alias to this field.
193    ///
194    /// Should accept static strs or owned Stringsk
195    pub fn alias(&mut self, alias: impl Into<Cow<'static, str>>) {
196        self.field.alias = Some(alias.into())
197    }
198
199    /// Adds an argument to this field.
200    ///
201    /// Accepts `ArgumentName` - the schema marker struct for the argument you
202    /// wish to add.
203    pub fn argument<ArgumentName>(
204        &'_ mut self,
205    ) -> InputBuilder<'_, Field::ArgumentType, VariablesFields>
206    where
207        Field: schema::HasArgument<ArgumentName>,
208    {
209        InputBuilder {
210            destination: InputLiteralContainer::object(Field::NAME, &mut self.field.arguments),
211            context: self.context,
212            phantom: PhantomData,
213        }
214    }
215
216    /// Adds an argument to this field.
217    ///
218    /// Accepts `ArgumentName` - the schema marker struct for the argument you
219    /// wish to add.
220    pub fn directive<DirectiveMarker>(
221        &'_ mut self,
222    ) -> DirectiveBuilder<'_, DirectiveMarker, VariablesFields>
223    where
224        DirectiveMarker: schema::FieldDirective,
225    {
226        self.field.directives.push(Directive {
227            name: Cow::Borrowed(DirectiveMarker::NAME),
228            arguments: vec![],
229        });
230        let directive = self.field.directives.last_mut().unwrap();
231
232        DirectiveBuilder {
233            arguments: &mut directive.arguments,
234            context: self.context,
235            phantom: PhantomData,
236        }
237    }
238
239    /// Returns a SelectionBuilder that can be used to select fields
240    /// within this field.
241    pub fn select_children<InnerVariables>(
242        &'_ mut self,
243    ) -> SelectionBuilder<'_, FieldSchemaType, InnerVariables>
244    where
245        VariablesFields: VariableMatch<InnerVariables>,
246    {
247        SelectionBuilder::private_new(&mut self.field.children, self.context.descend())
248    }
249}
250
251/// Builds an inline fragment in a selection
252pub struct InlineFragmentBuilder<'a, SchemaType, VariablesFields> {
253    phantom: PhantomData<fn() -> (SchemaType, VariablesFields)>,
254    inline_fragment: &'a mut InlineFragment,
255    context: BuilderContext<'a>,
256}
257
258impl<'a, SchemaType, VariablesFields> InlineFragmentBuilder<'a, SchemaType, VariablesFields> {
259    /// Adds an on clause for the given `Subtype` to the inline fragment.
260    ///
261    /// `Subtype` should be the schema marker type for the type you wish this
262    /// fragment to match.
263    pub fn on<Subtype>(self) -> InlineFragmentBuilder<'a, Subtype, VariablesFields>
264    where
265        Subtype: crate::schema::NamedType,
266        SchemaType: crate::schema::HasSubtype<Subtype>,
267    {
268        self.inline_fragment.on_clause = Some(Subtype::NAME);
269        InlineFragmentBuilder {
270            inline_fragment: self.inline_fragment,
271            phantom: PhantomData,
272            context: self.context,
273        }
274    }
275
276    /// Returns a SelectionBuilder that can be used to select the fields
277    /// of this fragment.
278    pub fn select_children<InnerVariablesFields>(
279        &'_ mut self,
280    ) -> SelectionBuilder<'_, SchemaType, InnerVariablesFields>
281    where
282        VariablesFields: VariableMatch<InnerVariablesFields>,
283    {
284        SelectionBuilder::private_new(&mut self.inline_fragment.children, self.context.descend())
285    }
286}
287
288pub struct InputBuilder<'a, SchemaType, VariablesFields> {
289    destination: InputLiteralContainer<'a>,
290    context: BuilderContext<'a>,
291
292    phantom: PhantomData<fn() -> (SchemaType, VariablesFields)>,
293}
294
295impl<SchemaType, VariablesFields> InputBuilder<'_, SchemaType, VariablesFields> {
296    /// Puts a variable into the input.
297    pub fn variable<Type>(self, def: VariableDefinition<VariablesFields, Type>)
298    where
299        Type: CoercesTo<SchemaType>,
300    {
301        match &self.context.inline_variables {
302            None => {
303                self.context
304                    .variables_used
305                    .send(def.name)
306                    .expect("the variables_used channel to be open");
307
308                self.destination.push(InputLiteral::Variable(def.name));
309            }
310            Some(variables) => {
311                // If the variable returns None we assume it hit a skip_serializing_if and skip it
312                if let Some(literal) = variables.get(def.name) {
313                    self.destination.push(literal);
314                }
315            }
316        }
317    }
318}
319
320impl<'a, SchemaType, ArgumentStruct> InputBuilder<'a, Option<SchemaType>, ArgumentStruct> {
321    /// Puts null into the input.
322    pub fn null(self) {
323        self.destination.push(InputLiteral::Null);
324    }
325
326    /// Returns a builder that can put input into a nullable input position.
327    pub fn value(self) -> InputBuilder<'a, SchemaType, ArgumentStruct> {
328        InputBuilder {
329            destination: self.destination,
330            context: self.context,
331            phantom: PhantomData,
332        }
333    }
334}
335
336impl<T, ArgStruct> InputBuilder<'_, T, ArgStruct> {
337    /// Puts a literal input type into the input.
338    pub fn literal(self, l: impl serde::Serialize + CoercesTo<T>) {
339        self.destination
340            .push(to_input_literal(&l).expect("could not convert to InputLiteral"));
341    }
342}
343
344impl<'a, SchemaType, VariablesFields> InputBuilder<'a, SchemaType, VariablesFields>
345where
346    SchemaType: schema::InputObjectMarker,
347{
348    /// Puts an object literal into the input
349    pub fn object(self) -> ObjectArgumentBuilder<'a, SchemaType, VariablesFields> {
350        let fields = match self.destination.push(InputLiteral::Object(Vec::new())) {
351            InputLiteral::Object(fields) => fields,
352            _ => panic!("This should be impossible"),
353        };
354
355        ObjectArgumentBuilder {
356            fields,
357            context: self.context.descend(),
358            phantom: PhantomData,
359        }
360    }
361}
362
363/// Builds an object literal into some input.
364pub struct ObjectArgumentBuilder<'a, ItemType, VariablesFields> {
365    fields: &'a mut Vec<Argument>,
366    context: BuilderContext<'a>,
367    phantom: PhantomData<fn() -> (ItemType, VariablesFields)>,
368}
369
370impl<SchemaType, ArgStruct> ObjectArgumentBuilder<'_, SchemaType, ArgStruct> {
371    /// Adds a field to the object literal, using the field_fn to determine the
372    /// contents of that field.
373    pub fn field<FieldMarker, F>(self, field_fn: F) -> Self
374    where
375        FieldMarker: schema::Field,
376        SchemaType: schema::HasInputField<FieldMarker, FieldMarker::Type>,
377        F: FnOnce(InputBuilder<'_, FieldMarker::Type, ArgStruct>),
378    {
379        field_fn(InputBuilder {
380            destination: InputLiteralContainer::object(FieldMarker::NAME, self.fields),
381            context: self.context,
382            phantom: PhantomData,
383        });
384
385        self
386    }
387}
388
389impl<'a, SchemaType, VariablesFields> InputBuilder<'a, Vec<SchemaType>, VariablesFields> {
390    /// Adds a list literal into some input
391    pub fn list(self) -> ListArgumentBuilder<'a, SchemaType, VariablesFields> {
392        let items = match self.destination.push(InputLiteral::List(Vec::new())) {
393            InputLiteral::List(items) => items,
394            _ => panic!("This should be impossible"),
395        };
396
397        ListArgumentBuilder {
398            items,
399            context: self.context.descend(),
400            phantom: PhantomData,
401        }
402    }
403}
404
405pub struct ListArgumentBuilder<'a, ItemType, VariablesFields> {
406    items: &'a mut Vec<InputLiteral>,
407    context: BuilderContext<'a>,
408    phantom: PhantomData<fn() -> (ItemType, VariablesFields)>,
409}
410
411impl<ItemType, VariablesFields> ListArgumentBuilder<'_, ItemType, VariablesFields> {
412    /// Adds an item to the list literal, using the item_fn to determine the
413    /// contents of that item.
414    pub fn item(self, item_fn: impl FnOnce(InputBuilder<'_, ItemType, VariablesFields>)) -> Self {
415        item_fn(InputBuilder {
416            destination: InputLiteralContainer::list(self.items),
417            context: self.context,
418            phantom: PhantomData,
419        });
420
421        self
422    }
423}
424
425enum InputLiteralContainer<'a> {
426    Object {
427        // The name of the field we're inserting
428        argument_name: &'static str,
429
430        // The list to insert into once we're done
431        arguments: &'a mut Vec<Argument>,
432    },
433    List(&'a mut Vec<InputLiteral>),
434}
435
436impl<'a> InputLiteralContainer<'a> {
437    fn list(list: &'a mut Vec<InputLiteral>) -> Self {
438        InputLiteralContainer::List(list)
439    }
440
441    fn object(argument_name: &'static str, arguments: &'a mut Vec<Argument>) -> Self {
442        InputLiteralContainer::Object {
443            argument_name,
444            arguments,
445        }
446    }
447
448    fn push(self, value: InputLiteral) -> &'a mut InputLiteral {
449        match self {
450            InputLiteralContainer::Object {
451                argument_name: name,
452                arguments,
453            } => {
454                arguments.push(Argument::new(name, value));
455
456                &mut arguments.last_mut().unwrap().value
457            }
458            InputLiteralContainer::List(arguments) => {
459                arguments.push(value);
460
461                arguments.last_mut().unwrap()
462            }
463        }
464    }
465}
466
467pub struct DirectiveBuilder<'a, DirectiveMarker, VariablesFields> {
468    arguments: &'a mut Vec<Argument>,
469    context: BuilderContext<'a>,
470    phantom: PhantomData<fn() -> (DirectiveMarker, VariablesFields)>,
471}
472
473impl<'a, DirectiveMarker, VariablesFields> DirectiveBuilder<'a, DirectiveMarker, VariablesFields> {
474    /// Adds an argument to this directive.
475    ///
476    /// Accepts `ArgumentName` - the schema marker struct for the argument you
477    /// wish to add.
478    pub fn argument<ArgumentName>(
479        &'_ mut self,
480    ) -> InputBuilder<'_, DirectiveMarker::ArgumentType, VariablesFields>
481    where
482        DirectiveMarker: schema::HasArgument<ArgumentName>,
483    {
484        InputBuilder {
485            destination: InputLiteralContainer::object(
486                <DirectiveMarker as schema::HasArgument<ArgumentName>>::NAME,
487                self.arguments,
488            ),
489            context: self.context,
490            phantom: PhantomData,
491        }
492    }
493}
494
495/// Enforces type equality on a VariablesFields struct.
496///
497/// Each `crate::QueryVariablesFields` implementation should also implement this
498/// for `()` for compatibility with QueryFragments that don't need variables.
499pub trait VariableMatch<T> {}
500
501impl<T> VariableMatch<()> for T where T: crate::QueryVariablesFields {}
502
503#[derive(Clone, Copy)]
504struct BuilderContext<'a> {
505    features_enabled: &'a HashSet<String>,
506    variables_used: &'a Sender<&'static str>,
507    recurse_depth: Option<u8>,
508    overall_depth: u16,
509    inline_variables: Option<&'a dyn QueryVariableLiterals>,
510}
511
512impl BuilderContext<'_> {
513    pub fn descend(&self) -> Self {
514        let overall_depth = self.overall_depth + 1;
515
516        assert!(
517            overall_depth < MAX_DEPTH,
518            "Maximum query depth exceeded.  Have you forgotten to mark a query as recursive?",
519        );
520
521        Self {
522            overall_depth,
523            ..*self
524        }
525    }
526
527    pub fn recurse(&self) -> Self {
528        Self {
529            recurse_depth: self.recurse_depth.map(|d| d + 1).or(Some(0)),
530            ..self.descend()
531        }
532    }
533}