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)]
14pub 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
30pub 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}