Skip to main content

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).result.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            .result
321            .unwrap_or_else(|_| panic!("Document had parse errors"));
322        let cache = Cache::new(&executable_document, &*TEST_SCHEMA_DEFINITION);
323        let variables = variables.as_object().expect("Variables must be an object");
324        let deprecations = DeprecationAnalyzer::analyze(
325            &executable_document,
326            &*TEST_SCHEMA_DEFINITION,
327            None,
328            variables,
329            &cache,
330            (),
331        )
332        .unwrap();
333        assert_eq!(deprecations, expected);
334    }
335
336    #[test]
337    fn field_deprecation() {
338        validate_deprecations(
339            r#"query { test_field }"#,
340            serde_json::json!({}),
341            vec![Offender {
342                name: "test_field",
343                reason: "field",
344                offense_type: UsageType::Field,
345            }],
346        );
347    }
348
349    #[test]
350    fn valid_field() {
351        validate_deprecations(r#"query { valid_field }"#, serde_json::json!({}), vec![]);
352    }
353
354    #[test]
355    fn variable_enum_value_deprecation() {
356        validate_deprecations(
357            r#"query ($test: TestEnum) { test_enum(deprecated_enum: $test) }"#,
358            serde_json::json!({ "test": "DEPRECATED" }),
359            vec![Offender {
360                name: "test",
361                reason: "enum_value",
362                offense_type: UsageType::EnumValue,
363            }],
364        );
365    }
366
367    #[test]
368    fn enum_value_deprecation() {
369        validate_deprecations(
370            r#"query { test_enum(deprecated_enum: DEPRECATED) }"#,
371            serde_json::json!({}),
372            vec![Offender {
373                name: "deprecated_enum",
374                reason: "enum_value",
375                offense_type: UsageType::EnumValue,
376            }],
377        );
378    }
379
380    #[test]
381    fn arg_deprecation() {
382        validate_deprecations(
383            r#"query { test_arg(deprecated_arg: "x") }"#,
384            serde_json::json!({}),
385            vec![Offender {
386                name: "deprecated_arg",
387                reason: "arg",
388                offense_type: UsageType::Argument,
389            }],
390        );
391    }
392
393    #[test]
394    fn variable_arg_deprecation() {
395        validate_deprecations(
396            r#"query($test: String) { test_arg(deprecated_arg: $test) }"#,
397            serde_json::json!({ "test": "x" }),
398            vec![Offender {
399                name: "deprecated_arg",
400                reason: "arg",
401                offense_type: UsageType::Argument,
402            }],
403        );
404    }
405
406    #[test]
407    fn input_field_deprecation() {
408        validate_deprecations(
409            r#"query { test_input(input: { deprecated_input_field: "x" }) }"#,
410            serde_json::json!({}),
411            vec![Offender {
412                name: "deprecated_input_field",
413                reason: "input_field",
414                offense_type: UsageType::InputField,
415            }],
416        );
417    }
418
419    #[test]
420    fn variable_input_field_deprecation() {
421        validate_deprecations(
422            r#"query($input: TestInput) { test_input(input: $input) }"#,
423            serde_json::json!({ "input": { "deprecated_input_field": "x" } }),
424            vec![Offender {
425                name: "deprecated_input_field",
426                reason: "input_field",
427                offense_type: UsageType::InputField,
428            }],
429        );
430    }
431
432    #[test]
433    fn nested_variable_input_field_deprecation() {
434        validate_deprecations(
435            r#"query($test: String) { test_input(input: { deprecated_input_field: $test }) }"#,
436            serde_json::json!({
437                "test": "x"
438            }),
439            vec![Offender {
440                name: "deprecated_input_field",
441                reason: "input_field",
442                offense_type: UsageType::InputField,
443            }],
444        );
445    }
446
447    #[test]
448    fn nested_input_field_deprecation() {
449        validate_deprecations(
450            r#"query { test_nested_input(nested_input: { nested: { deprecated_input_field: "x" } }) }"#,
451            serde_json::json!({}),
452            vec![Offender {
453                name: "deprecated_input_field",
454                reason: "input_field",
455                offense_type: UsageType::InputField,
456            }],
457        );
458    }
459
460    #[test]
461    fn nested_list_input_field_deprecation() {
462        validate_deprecations(
463            r#"query { test_nested_input_list(nested_input: [{ nested: { deprecated_input_field: "x" } }]) }"#,
464            serde_json::json!({}),
465            vec![Offender {
466                name: "deprecated_input_field",
467                reason: "input_field",
468                offense_type: UsageType::InputField,
469            }],
470        );
471    }
472
473    #[test]
474    fn nested_variable_list_input_field_deprecation() {
475        validate_deprecations(
476            r#"query($test: [NestedInput]) { test_nested_input_list(nested_input: $test) }"#,
477            serde_json::json!({ "test": [{ "nested": { "deprecated_input_field": "x" } }] }),
478            vec![Offender {
479                name: "deprecated_input_field",
480                reason: "input_field",
481                offense_type: UsageType::InputField,
482            }],
483        );
484    }
485}