bluejay_validator/executable/operation/analyzers/
deprecation.rs

1use crate::executable::{
2    operation::{Analyzer, VariableValues, Visitor},
3    Cache,
4};
5use bluejay_core::definition::{
6    BaseInputTypeReference, EnumTypeDefinition, EnumValueDefinition, HasDirectives,
7    InputObjectTypeDefinition, InputType, InputTypeReference, InputValueDefinition,
8    SchemaDefinition,
9};
10use bluejay_core::executable::{ExecutableDocument, Field, VariableDefinition};
11use bluejay_core::{Argument, AsIter, Directive, ObjectValue, Value, ValueReference};
12
13#[derive(Copy, Clone, PartialEq, Eq, Debug)]
14/// The deprecated usage we encountered.
15pub enum UsageType {
16    Argument,
17    EnumValue,
18    InputField,
19    Field,
20    Variable,
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct Offender<'a> {
25    pub reason: &'a str,
26    pub offense_type: UsageType,
27    pub name: &'a str,
28}
29
30/// The [Deprecation] analyzer will go over all ast-nodes of type Field, EnumValue, Argument and InputField
31/// when it encounters one that is marked as deprecated while being used in the executable document
32/// it will be added ot the list of [Offender].
33/// This method will output the list of [Offender].
34pub struct Deprecation<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> {
35    offenders: Vec<Offender<'a>>,
36    schema_definition: &'a S,
37    cache: &'a Cache<'a, E, S>,
38    variable_values: &'a VV,
39}
40
41const DEPRECATED_DIRECTIVE: &str = "deprecated";
42const DEPRECATION_REASON: &str = "reason";
43
44impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Visitor<'a, E, S, VV>
45    for Deprecation<'a, E, S, VV>
46{
47    type ExtraInfo = ();
48    fn new(
49        _: &'a E::OperationDefinition,
50        schema_definition: &'a S,
51        variable_values: &'a VV,
52        cache: &'a Cache<'a, E, S>,
53        _: Self::ExtraInfo,
54    ) -> Self {
55        Self {
56            offenders: vec![],
57            schema_definition,
58            cache,
59            variable_values,
60        }
61    }
62
63    fn visit_variable_definition(
64        &mut self,
65        variable_definition: &'a <E as ExecutableDocument>::VariableDefinition,
66    ) {
67        if let Some(input_type) = self
68            .cache
69            .variable_definition_input_type(variable_definition.r#type())
70        {
71            if let Some(value) = self
72                .variable_values
73                .get(variable_definition.variable().as_ref())
74            {
75                self.find_deprecations_for_value(input_type, value, variable_definition.variable());
76            }
77            if let Some(default_value) = variable_definition.default_value() {
78                self.find_deprecations_for_value(
79                    input_type,
80                    default_value,
81                    variable_definition.variable(),
82                );
83            }
84        }
85    }
86
87    fn visit_field(
88        &mut self,
89        field: &'a <E as ExecutableDocument>::Field,
90        field_definition: &'a <S as SchemaDefinition>::FieldDefinition,
91        _scoped_type: bluejay_core::definition::TypeDefinitionReference<
92            'a,
93            <S as SchemaDefinition>::TypeDefinition,
94        >,
95        included: bool,
96    ) {
97        if !included {
98            return;
99        }
100
101        if let Some(reason) =
102            get_deprecation_reason::<<S as SchemaDefinition>::FieldDefinition>(field_definition)
103        {
104            self.offenders.push(Offender {
105                name: field.name(),
106                offense_type: UsageType::Field,
107                reason,
108            });
109        }
110    }
111
112    fn visit_variable_argument(
113        &mut self,
114        argument: &'a <E as ExecutableDocument>::Argument<false>,
115        input_value_definition: &'a <S as SchemaDefinition>::InputValueDefinition,
116    ) {
117        if let Some(reason) = get_deprecation_reason::<<S as SchemaDefinition>::InputValueDefinition>(
118            input_value_definition,
119        ) {
120            self.offenders.push(Offender {
121                name: argument.name(),
122                offense_type: UsageType::Argument,
123                reason,
124            });
125        }
126
127        self.find_deprecations_for_value(
128            input_value_definition.r#type(),
129            argument.value(),
130            argument.name(),
131        );
132    }
133}
134
135fn get_deprecation_reason<N: HasDirectives>(ast_item: &N) -> Option<&str> {
136    let deprecated_directive = ast_item.directives().and_then(|directives| {
137        directives
138            .iter()
139            .find(|directive| directive.name() == DEPRECATED_DIRECTIVE)
140    });
141
142    deprecated_directive.map(|deprecated_directive| {
143        deprecated_directive
144            .arguments()
145            .and_then(|arguments| {
146                arguments
147                    .iter()
148                    .find(|argument| argument.name() == DEPRECATION_REASON)
149                    .and_then(|argument| {
150                        if let ValueReference::String(str) = argument.value().as_ref() {
151                            Some(str)
152                        } else {
153                            None
154                        }
155                    })
156            })
157            .unwrap_or("No longer supported.")
158    })
159}
160
161impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Deprecation<'a, E, S, VV> {
162    fn find_deprecations_for_value<
163        const CONST: bool,
164        I: InputType<
165            CustomScalarTypeDefinition = S::CustomScalarTypeDefinition,
166            InputObjectTypeDefinition = S::InputObjectTypeDefinition,
167            EnumTypeDefinition = S::EnumTypeDefinition,
168        >,
169        V: Value<CONST>,
170    >(
171        &mut self,
172        input_type: &'a I,
173        value: &'a V,
174        name: &'a str,
175    ) {
176        match input_type.as_ref(self.schema_definition) {
177            InputTypeReference::List(inner_list_type, _) => match value.as_ref() {
178                ValueReference::List(list_value) => list_value.iter().for_each(|list_item| {
179                    self.find_deprecations_for_value(inner_list_type, list_item, name)
180                }),
181                _ => self.find_deprecations_for_value(inner_list_type, value, name),
182            },
183            InputTypeReference::Base(base_input_type, _) => match base_input_type {
184                BaseInputTypeReference::Enum(etd) => {
185                    let enum_value = match value.as_ref() {
186                        ValueReference::Enum(enum_value) => Some(enum_value),
187                        ValueReference::String(string_value)
188                            if V::can_coerce_string_value_to_enum() =>
189                        {
190                            Some(string_value)
191                        }
192                        _ => None,
193                    };
194                    if let Some(enum_value) = enum_value {
195                        if let Some(deprecation_reason) = etd
196                            .enum_value_definitions()
197                            .iter()
198                            .find(|evd| evd.name() == enum_value)
199                            .and_then(|found_enum_value| {
200                                get_deprecation_reason::<S::EnumValueDefinition>(found_enum_value)
201                            })
202                        {
203                            self.offenders.push(Offender {
204                                name,
205                                offense_type: UsageType::EnumValue,
206                                reason: deprecation_reason,
207                            });
208                        }
209                    }
210                }
211                BaseInputTypeReference::InputObject(iotd) => {
212                    if let ValueReference::Object(obj_value) = value.as_ref() {
213                        iotd.input_field_definitions()
214                            .iter()
215                            .for_each(|input_field_definition| {
216                                let found_usage = obj_value.iter().find(|(key, _value)| {
217                                    key.as_ref() == input_field_definition.name()
218                                });
219
220                                if let Some((_, value)) = found_usage {
221                                    if let Some(reason) =
222                                        get_deprecation_reason::<S::InputValueDefinition>(
223                                            input_field_definition,
224                                        )
225                                    {
226                                        self.offenders.push(Offender {
227                                            name: input_field_definition.name(),
228                                            offense_type: UsageType::InputField,
229                                            reason,
230                                        });
231                                    }
232
233                                    self.find_deprecations_for_value(
234                                        input_field_definition.r#type(),
235                                        value,
236                                        name,
237                                    )
238                                }
239                            });
240                    }
241                }
242                _ => {}
243            },
244        }
245    }
246}
247
248impl<'a, E: ExecutableDocument, S: SchemaDefinition, VV: VariableValues> Analyzer<'a, E, S, VV>
249    for Deprecation<'a, E, S, VV>
250{
251    type Output = Vec<Offender<'a>>;
252
253    fn into_output(self) -> Self::Output {
254        self.offenders
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::{Deprecation, Offender};
261    use crate::executable::{
262        operation::{analyzers::deprecation::UsageType, Orchestrator},
263        Cache,
264    };
265    use bluejay_parser::ast::{
266        definition::{DefinitionDocument, SchemaDefinition as ParserSchemaDefinition},
267        executable::ExecutableDocument as ParserExecutableDocument,
268        Parse,
269    };
270    use once_cell::sync::Lazy;
271    use serde_json::{Map as JsonMap, Value as JsonValue};
272
273    type DeprecationAnalyzer<'a, E, S> = Orchestrator<
274        'a,
275        E,
276        S,
277        JsonMap<String, JsonValue>,
278        Deprecation<'a, E, S, JsonMap<String, JsonValue>>,
279    >;
280
281    const TEST_SCHEMA_SDL: &str = r#"
282        enum TestEnum {
283            DEPRECATED @deprecated(reason: "enum_value")
284        }
285
286        input TestInput {
287            deprecated_input_field: String @deprecated(reason: "input_field")
288        }
289
290        input NestedInput {
291            nested: TestInput
292        }
293
294        type Query {
295          valid_field: String!
296          test_field: String! @deprecated(reason: "field")
297          test_enum(deprecated_enum: TestEnum): String!
298          test_arg(
299            deprecated_arg: String @deprecated(reason: "arg")
300          ): String!
301          test_input(
302            input: TestInput
303          ): String!
304          test_nested_input(nested_input: NestedInput): String!
305          test_nested_input_list(nested_input: [NestedInput]): String!
306        }
307        schema {
308          query: Query
309        }
310    "#;
311
312    static TEST_DEFINITION_DOCUMENT: Lazy<DefinitionDocument<'static>> =
313        Lazy::new(|| DefinitionDocument::parse(TEST_SCHEMA_SDL).unwrap());
314
315    static TEST_SCHEMA_DEFINITION: Lazy<ParserSchemaDefinition<'static>> =
316        Lazy::new(|| ParserSchemaDefinition::try_from(&*TEST_DEFINITION_DOCUMENT).unwrap());
317
318    fn validate_deprecations(query: &str, variables: serde_json::Value, expected: Vec<Offender>) {
319        let executable_document = ParserExecutableDocument::parse(query)
320            .unwrap_or_else(|_| panic!("Document had parse errors"));
321        let cache = Cache::new(&executable_document, &*TEST_SCHEMA_DEFINITION);
322        let variables = variables.as_object().expect("Variables must be an object");
323        let deprecations = DeprecationAnalyzer::analyze(
324            &executable_document,
325            &*TEST_SCHEMA_DEFINITION,
326            None,
327            variables,
328            &cache,
329            (),
330        )
331        .unwrap();
332        assert_eq!(deprecations, expected);
333    }
334
335    #[test]
336    fn field_deprecation() {
337        validate_deprecations(
338            r#"query { test_field }"#,
339            serde_json::json!({}),
340            vec![Offender {
341                name: "test_field",
342                reason: "field",
343                offense_type: UsageType::Field,
344            }],
345        );
346    }
347
348    #[test]
349    fn valid_field() {
350        validate_deprecations(r#"query { valid_field }"#, serde_json::json!({}), vec![]);
351    }
352
353    #[test]
354    fn variable_enum_value_deprecation() {
355        validate_deprecations(
356            r#"query ($test: TestEnum) { test_enum(deprecated_enum: $test) }"#,
357            serde_json::json!({ "test": "DEPRECATED" }),
358            vec![Offender {
359                name: "test",
360                reason: "enum_value",
361                offense_type: UsageType::EnumValue,
362            }],
363        );
364    }
365
366    #[test]
367    fn enum_value_deprecation() {
368        validate_deprecations(
369            r#"query { test_enum(deprecated_enum: DEPRECATED) }"#,
370            serde_json::json!({}),
371            vec![Offender {
372                name: "deprecated_enum",
373                reason: "enum_value",
374                offense_type: UsageType::EnumValue,
375            }],
376        );
377    }
378
379    #[test]
380    fn arg_deprecation() {
381        validate_deprecations(
382            r#"query { test_arg(deprecated_arg: "x") }"#,
383            serde_json::json!({}),
384            vec![Offender {
385                name: "deprecated_arg",
386                reason: "arg",
387                offense_type: UsageType::Argument,
388            }],
389        );
390    }
391
392    #[test]
393    fn variable_arg_deprecation() {
394        validate_deprecations(
395            r#"query($test: String) { test_arg(deprecated_arg: $test) }"#,
396            serde_json::json!({ "test": "x" }),
397            vec![Offender {
398                name: "deprecated_arg",
399                reason: "arg",
400                offense_type: UsageType::Argument,
401            }],
402        );
403    }
404
405    #[test]
406    fn input_field_deprecation() {
407        validate_deprecations(
408            r#"query { test_input(input: { deprecated_input_field: "x" }) }"#,
409            serde_json::json!({}),
410            vec![Offender {
411                name: "deprecated_input_field",
412                reason: "input_field",
413                offense_type: UsageType::InputField,
414            }],
415        );
416    }
417
418    #[test]
419    fn variable_input_field_deprecation() {
420        validate_deprecations(
421            r#"query($input: TestInput) { test_input(input: $input) }"#,
422            serde_json::json!({ "input": { "deprecated_input_field": "x" } }),
423            vec![Offender {
424                name: "deprecated_input_field",
425                reason: "input_field",
426                offense_type: UsageType::InputField,
427            }],
428        );
429    }
430
431    #[test]
432    fn nested_variable_input_field_deprecation() {
433        validate_deprecations(
434            r#"query($test: String) { test_input(input: { deprecated_input_field: $test }) }"#,
435            serde_json::json!({
436                "test": "x"
437            }),
438            vec![Offender {
439                name: "deprecated_input_field",
440                reason: "input_field",
441                offense_type: UsageType::InputField,
442            }],
443        );
444    }
445
446    #[test]
447    fn nested_input_field_deprecation() {
448        validate_deprecations(
449            r#"query { test_nested_input(nested_input: { nested: { deprecated_input_field: "x" } }) }"#,
450            serde_json::json!({}),
451            vec![Offender {
452                name: "deprecated_input_field",
453                reason: "input_field",
454                offense_type: UsageType::InputField,
455            }],
456        );
457    }
458
459    #[test]
460    fn nested_list_input_field_deprecation() {
461        validate_deprecations(
462            r#"query { test_nested_input_list(nested_input: [{ nested: { deprecated_input_field: "x" } }]) }"#,
463            serde_json::json!({}),
464            vec![Offender {
465                name: "deprecated_input_field",
466                reason: "input_field",
467                offense_type: UsageType::InputField,
468            }],
469        );
470    }
471
472    #[test]
473    fn nested_variable_list_input_field_deprecation() {
474        validate_deprecations(
475            r#"query($test: [NestedInput]) { test_nested_input_list(nested_input: $test) }"#,
476            serde_json::json!({ "test": [{ "nested": { "deprecated_input_field": "x" } }] }),
477            vec![Offender {
478                name: "deprecated_input_field",
479                reason: "input_field",
480                offense_type: UsageType::InputField,
481            }],
482        );
483    }
484}