bluejay_validator/executable/operation/
orchestrator.rs

1use crate::executable::{
2    operation::{Analyzer, OperationDefinitionValueEvaluationExt, VariableValues, Visitor},
3    Cache,
4};
5use bluejay_core::{
6    definition::{ArgumentsDefinition, DirectiveDefinition, DirectiveLocation},
7    executable::{
8        ExecutableDocument, Field, FragmentDefinition, FragmentSpread, InlineFragment,
9        OperationDefinition, Selection, SelectionReference,
10    },
11};
12use bluejay_core::{
13    definition::{
14        FieldDefinition, FieldsDefinition, ObjectTypeDefinition, OutputType, SchemaDefinition,
15        TypeDefinitionReference,
16    },
17    ValueReference,
18};
19use bluejay_core::{Argument, AsIter, Directive, OperationType, Value};
20use std::borrow::Cow;
21use std::collections::HashSet;
22
23pub struct Orchestrator<
24    'a,
25    E: ExecutableDocument,
26    S: SchemaDefinition,
27    VV: VariableValues,
28    V: Visitor<'a, E, S, VV>,
29> {
30    schema_definition: &'a S,
31    operation_definition: &'a E::OperationDefinition,
32    variable_values: &'a VV,
33    visitor: V,
34    cache: &'a Cache<'a, E, S>,
35    currently_spread_fragments: HashSet<&'a str>,
36}
37
38impl<
39        'a,
40        E: ExecutableDocument,
41        S: SchemaDefinition,
42        VV: VariableValues,
43        V: Visitor<'a, E, S, VV>,
44    > Orchestrator<'a, E, S, VV, V>
45{
46    const SKIP_DIRECTIVE_NAME: &'static str = "skip";
47    const INCLUDE_DIRECTIVE_NAME: &'static str = "include";
48    const SKIP_INCLUDE_CONDITION_ARGUMENT: &'static str = "if";
49
50    fn new(
51        operation_definition: &'a E::OperationDefinition,
52        schema_definition: &'a S,
53        variable_values: &'a VV,
54        cache: &'a Cache<'a, E, S>,
55        extra_info: V::ExtraInfo,
56    ) -> Self {
57        Self {
58            schema_definition,
59            operation_definition,
60            variable_values,
61            visitor: Visitor::new(
62                operation_definition,
63                schema_definition,
64                variable_values,
65                cache,
66                extra_info,
67            ),
68            cache,
69            currently_spread_fragments: HashSet::new(),
70        }
71    }
72
73    fn visit(&mut self) {
74        self.visit_operation_definition(self.operation_definition);
75    }
76
77    fn visit_variable_directives(
78        &mut self,
79        directives: &'a E::Directives<false>,
80        location: DirectiveLocation,
81    ) {
82        directives
83            .iter()
84            .for_each(|directive| self.visit_variable_directive(directive, location));
85    }
86
87    fn visit_variable_directive(
88        &mut self,
89        directive: &'a E::Directive<false>,
90        _location: DirectiveLocation,
91    ) {
92        if let Some(arguments) = directive.arguments() {
93            if let Some(arguments_definition) = self
94                .schema_definition
95                .get_directive_definition(directive.name())
96                .and_then(DirectiveDefinition::arguments_definition)
97            {
98                self.visit_variable_arguments(arguments, arguments_definition);
99            }
100        }
101    }
102
103    fn visit_operation_definition(&mut self, operation_definition: &'a E::OperationDefinition) {
104        let core_operation_definition = operation_definition.as_ref();
105
106        let root_operation_type_definition_name = match core_operation_definition.operation_type() {
107            OperationType::Query => Some(self.schema_definition.query().name()),
108            OperationType::Mutation => self
109                .schema_definition
110                .mutation()
111                .map(ObjectTypeDefinition::name),
112            OperationType::Subscription => self
113                .schema_definition
114                .subscription()
115                .map(ObjectTypeDefinition::name),
116        };
117
118        if let Some(directives) = core_operation_definition.directives() {
119            self.visit_variable_directives(
120                directives,
121                core_operation_definition
122                    .operation_type()
123                    .associated_directive_location(),
124            )
125        }
126
127        if let Some(root_operation_type_definition_name) = root_operation_type_definition_name {
128            self.visit_selection_set(
129                core_operation_definition.selection_set(),
130                self.schema_definition
131                    .get_type_definition(root_operation_type_definition_name)
132                    .unwrap_or_else(|| {
133                        panic!(
134                            "Schema definition's `get_type` method returned `None` for {} root",
135                            core_operation_definition.operation_type()
136                        )
137                    }),
138                true,
139            );
140        }
141
142        if let Some(variable_definitions) = operation_definition.as_ref().variable_definitions() {
143            variable_definitions.iter().for_each(|variable_definition| {
144                self.visitor.visit_variable_definition(variable_definition)
145            });
146        }
147    }
148
149    fn visit_selection_set(
150        &mut self,
151        selection_set: &'a E::SelectionSet,
152        scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
153        included: bool,
154    ) {
155        selection_set
156            .iter()
157            .for_each(|selection| match selection.as_ref() {
158                SelectionReference::Field(f) => {
159                    let field_definition = scoped_type
160                        .fields_definition()
161                        .and_then(|fields_definition| fields_definition.get(f.name()));
162
163                    if let Some(field_definition) = field_definition {
164                        self.visit_field(f, field_definition, scoped_type, included);
165                    }
166                }
167                SelectionReference::InlineFragment(i) => {
168                    self.visit_inline_fragment(i, scoped_type, included)
169                }
170                SelectionReference::FragmentSpread(fs) => self.visit_fragment_spread(fs, included),
171            })
172    }
173
174    fn visit_field(
175        &mut self,
176        field: &'a E::Field,
177        field_definition: &'a S::FieldDefinition,
178        owner_type: TypeDefinitionReference<'a, S::TypeDefinition>,
179        included: bool,
180    ) {
181        if let Some(directives) = field.directives() {
182            self.visit_variable_directives(directives, DirectiveLocation::Field);
183        }
184        let included = included
185            && field.directives().map_or(true, |directives| {
186                self.evaluate_selection_inclusion(directives)
187            });
188
189        self.visitor
190            .visit_field(field, field_definition, owner_type, included);
191
192        if let Some(arguments) = field.arguments() {
193            if let Some(arguments_definition) = field_definition.arguments_definition() {
194                arguments.iter().for_each(|argument| {
195                    if let Some(ivd) = arguments_definition.get(argument.name()) {
196                        self.visit_variable_argument(argument, ivd);
197                    }
198                })
199            }
200        }
201
202        if let Some(selection_set) = field.selection_set() {
203            if let Some(nested_type) = self
204                .schema_definition
205                .get_type_definition(field_definition.r#type().base_name())
206            {
207                self.visit_selection_set(selection_set, nested_type, included);
208            }
209        }
210
211        self.visitor
212            .leave_field(field, field_definition, owner_type, included);
213    }
214
215    fn visit_variable_arguments(
216        &mut self,
217        arguments: &'a E::Arguments<false>,
218        arguments_definition: &'a S::ArgumentsDefinition,
219    ) {
220        arguments.iter().for_each(|argument| {
221            if let Some(ivd) = arguments_definition.get(argument.name()) {
222                self.visit_variable_argument(argument, ivd);
223            }
224        });
225    }
226
227    fn visit_variable_argument(
228        &mut self,
229        argument: &'a E::Argument<false>,
230        input_value_definition: &'a S::InputValueDefinition,
231    ) {
232        self.visitor
233            .visit_variable_argument(argument, input_value_definition);
234    }
235
236    fn visit_inline_fragment(
237        &mut self,
238        inline_fragment: &'a E::InlineFragment,
239        scoped_type: TypeDefinitionReference<'a, S::TypeDefinition>,
240        included: bool,
241    ) {
242        if let Some(directives) = inline_fragment.directives() {
243            self.visit_variable_directives(directives, DirectiveLocation::InlineFragment);
244        }
245
246        let included = included
247            && inline_fragment.directives().map_or(true, |directives| {
248                self.evaluate_selection_inclusion(directives)
249            });
250
251        let fragment_type = if let Some(type_condition) = inline_fragment.type_condition() {
252            self.schema_definition.get_type_definition(type_condition)
253        } else {
254            Some(scoped_type)
255        };
256
257        if let Some(fragment_type) = fragment_type {
258            self.visit_selection_set(inline_fragment.selection_set(), fragment_type, included);
259        }
260    }
261
262    fn visit_fragment_spread(&mut self, fragment_spread: &'a E::FragmentSpread, included: bool) {
263        if let Some(directives) = fragment_spread.directives() {
264            self.visit_variable_directives(directives, DirectiveLocation::FragmentSpread);
265        }
266
267        let included = included
268            && fragment_spread.directives().map_or(true, |directives| {
269                self.evaluate_selection_inclusion(directives)
270            });
271        if self
272            .currently_spread_fragments
273            .insert(fragment_spread.name())
274        {
275            if let Some(fragment_definition) =
276                self.cache.fragment_definition(fragment_spread.name())
277            {
278                if let Some(type_condition) = self
279                    .schema_definition
280                    .get_type_definition(fragment_definition.type_condition())
281                {
282                    self.visit_selection_set(
283                        fragment_definition.selection_set(),
284                        type_condition,
285                        included,
286                    );
287                }
288            }
289            self.currently_spread_fragments
290                .remove(fragment_spread.name());
291        }
292    }
293
294    fn evaluate_selection_inclusion(&mut self, directives: &'a E::Directives<false>) -> bool {
295        let skip_directive_value = self.evaluate_boolean_directive_argument_value(
296            directives,
297            Self::SKIP_DIRECTIVE_NAME,
298            Self::SKIP_INCLUDE_CONDITION_ARGUMENT,
299        );
300
301        let include_directive_value = self.evaluate_boolean_directive_argument_value(
302            directives,
303            Self::INCLUDE_DIRECTIVE_NAME,
304            Self::SKIP_INCLUDE_CONDITION_ARGUMENT,
305        );
306
307        !matches!(
308            (skip_directive_value, include_directive_value),
309            (Some(true), _) | (_, Some(false))
310        )
311    }
312
313    fn evaluate_boolean_directive_argument_value(
314        &self,
315        directives: &'a E::Directives<false>,
316        directive_name: &str,
317        arg_name: &str,
318    ) -> Option<bool> {
319        directives
320            .iter()
321            .find(|directive| directive.name() == directive_name)
322            .and_then(|directive| {
323                directive
324                    .arguments()
325                    .and_then(|arguments| arguments.iter().find(|arg| arg.name() == arg_name))
326                    .and_then(|argument| match argument.value().as_ref() {
327                        ValueReference::Boolean(val) => Some(val),
328                        ValueReference::Variable(v) => self
329                            .operation_definition
330                            .evaluate_bool(v, self.variable_values),
331                        _ => None,
332                    })
333            })
334    }
335
336    pub fn analyze<'b>(
337        executable_document: &'a E,
338        schema_definition: &'a S,
339        operation_name: Option<&'b str>,
340        variable_values: &'a VV,
341        cache: &'a Cache<'a, E, S>,
342        extra_info: V::ExtraInfo,
343    ) -> Result<<V as Analyzer<'a, E, S, VV>>::Output, OperationResolutionError<'b>>
344    where
345        V: Analyzer<'a, E, S, VV>,
346    {
347        let operation_definition = match operation_name {
348            Some(operation_name) => executable_document
349                .operation_definitions()
350                .find(|operation_definition| {
351                    operation_definition
352                        .as_ref()
353                        .name()
354                        .map_or(false, |name| name == operation_name)
355                })
356                .ok_or(OperationResolutionError::NoOperationWithName {
357                    name: operation_name,
358                })?,
359            None => {
360                let [operation_definition]: [&'a E::OperationDefinition; 1] = executable_document
361                    .operation_definitions()
362                    .collect::<Vec<_>>()
363                    .as_slice()
364                    .try_into()
365                    .map_err(|_| OperationResolutionError::AnonymousNotEligible)?;
366                operation_definition
367            }
368        };
369        let mut instance = Self::new(
370            operation_definition,
371            schema_definition,
372            variable_values,
373            cache,
374            extra_info,
375        );
376        instance.visit();
377        Ok(instance.visitor.into_output())
378    }
379}
380
381#[derive(Debug)]
382pub enum OperationResolutionError<'a> {
383    NoOperationWithName { name: &'a str },
384    AnonymousNotEligible,
385}
386
387impl<'a> OperationResolutionError<'a> {
388    pub fn message(&self) -> Cow<'static, str> {
389        match self {
390            Self::NoOperationWithName { name } => format!("No operation defined with name {}", name).into(),
391            Self::AnonymousNotEligible => "Anonymous operation can only be used when the document contains exactly one operation definition".into(),
392        }
393    }
394}