1use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use super::{
22 CompiledSchema, DirectiveDefinition, DirectiveLocationKind, EnumDefinition, FieldDefinition,
23 FieldType, InputObjectDefinition, InterfaceDefinition, QueryDefinition, TypeDefinition,
24 UnionDefinition,
25};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct IntrospectionSchema {
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub description: Option<String>,
40
41 pub types: Vec<IntrospectionType>,
43
44 pub query_type: IntrospectionTypeRef,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub mutation_type: Option<IntrospectionTypeRef>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub subscription_type: Option<IntrospectionTypeRef>,
54
55 pub directives: Vec<IntrospectionDirective>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct IntrospectionType {
65 pub kind: TypeKind,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub name: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub description: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub fields: Option<Vec<IntrospectionField>>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub interfaces: Option<Vec<IntrospectionTypeRef>>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub possible_types: Option<Vec<IntrospectionTypeRef>>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub enum_values: Option<Vec<IntrospectionEnumValue>>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub input_fields: Option<Vec<IntrospectionInputValue>>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub of_type: Option<Box<IntrospectionType>>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub specified_by_u_r_l: Option<String>,
103}
104
105impl IntrospectionType {
106 #[must_use]
111 pub fn filter_deprecated_fields(&self, include_deprecated: bool) -> Self {
112 let mut result = self.clone();
113
114 if !include_deprecated {
115 if let Some(ref fields) = result.fields {
116 result.fields = Some(fields.iter().filter(|f| !f.is_deprecated).cloned().collect());
117 }
118 }
119
120 result
121 }
122
123 #[must_use]
128 pub fn filter_deprecated_enum_values(&self, include_deprecated: bool) -> Self {
129 let mut result = self.clone();
130
131 if !include_deprecated {
132 if let Some(ref values) = result.enum_values {
133 result.enum_values =
134 Some(values.iter().filter(|v| !v.is_deprecated).cloned().collect());
135 }
136 }
137
138 result
139 }
140
141 #[must_use]
145 pub fn filter_all_deprecated(&self, include_deprecated: bool) -> Self {
146 self.filter_deprecated_fields(include_deprecated)
147 .filter_deprecated_enum_values(include_deprecated)
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct IntrospectionTypeRef {
154 pub name: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct IntrospectionField {
162 pub name: String,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub description: Option<String>,
168
169 pub args: Vec<IntrospectionInputValue>,
171
172 #[serde(rename = "type")]
174 pub field_type: IntrospectionType,
175
176 pub is_deprecated: bool,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub deprecation_reason: Option<String>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct IntrospectionInputValue {
191 pub name: String,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub description: Option<String>,
197
198 #[serde(rename = "type")]
200 pub input_type: IntrospectionType,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub default_value: Option<String>,
205
206 pub is_deprecated: bool,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub deprecation_reason: Option<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct IntrospectionEnumValue {
218 pub name: String,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub description: Option<String>,
224
225 pub is_deprecated: bool,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub deprecation_reason: Option<String>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct IntrospectionDirective {
237 pub name: String,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub description: Option<String>,
243
244 pub locations: Vec<DirectiveLocation>,
246
247 pub args: Vec<IntrospectionInputValue>,
249
250 #[serde(default)]
252 pub is_repeatable: bool,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
258pub enum TypeKind {
259 Scalar,
261 Object,
263 Interface,
265 Union,
267 Enum,
269 InputObject,
271 List,
273 NonNull,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
280pub enum DirectiveLocation {
281 Query,
283 Mutation,
285 Subscription,
287 Field,
289 FragmentDefinition,
291 FragmentSpread,
293 InlineFragment,
295 VariableDefinition,
297 Schema,
299 Scalar,
301 Object,
303 FieldDefinition,
305 ArgumentDefinition,
307 Interface,
309 Union,
311 Enum,
313 EnumValue,
315 InputObject,
317 InputFieldDefinition,
319}
320
321impl From<DirectiveLocationKind> for DirectiveLocation {
322 fn from(kind: DirectiveLocationKind) -> Self {
323 match kind {
324 DirectiveLocationKind::Query => Self::Query,
325 DirectiveLocationKind::Mutation => Self::Mutation,
326 DirectiveLocationKind::Subscription => Self::Subscription,
327 DirectiveLocationKind::Field => Self::Field,
328 DirectiveLocationKind::FragmentDefinition => Self::FragmentDefinition,
329 DirectiveLocationKind::FragmentSpread => Self::FragmentSpread,
330 DirectiveLocationKind::InlineFragment => Self::InlineFragment,
331 DirectiveLocationKind::VariableDefinition => Self::VariableDefinition,
332 DirectiveLocationKind::Schema => Self::Schema,
333 DirectiveLocationKind::Scalar => Self::Scalar,
334 DirectiveLocationKind::Object => Self::Object,
335 DirectiveLocationKind::FieldDefinition => Self::FieldDefinition,
336 DirectiveLocationKind::ArgumentDefinition => Self::ArgumentDefinition,
337 DirectiveLocationKind::Interface => Self::Interface,
338 DirectiveLocationKind::Union => Self::Union,
339 DirectiveLocationKind::Enum => Self::Enum,
340 DirectiveLocationKind::EnumValue => Self::EnumValue,
341 DirectiveLocationKind::InputObject => Self::InputObject,
342 DirectiveLocationKind::InputFieldDefinition => Self::InputFieldDefinition,
343 }
344 }
345}
346
347pub struct IntrospectionBuilder;
353
354impl IntrospectionBuilder {
355 #[must_use]
357 pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
358 let mut types = Vec::new();
359
360 types.extend(Self::builtin_scalars());
362
363 for type_def in &schema.types {
365 types.push(Self::build_object_type(type_def));
366 }
367
368 for enum_def in &schema.enums {
370 types.push(Self::build_enum_type(enum_def));
371 }
372
373 for input_def in &schema.input_types {
375 types.push(Self::build_input_object_type(input_def));
376 }
377
378 for interface_def in &schema.interfaces {
380 types.push(Self::build_interface_type(interface_def, schema));
381 }
382
383 for union_def in &schema.unions {
385 types.push(Self::build_union_type(union_def));
386 }
387
388 types.push(Self::build_query_type(schema));
390
391 if !schema.mutations.is_empty() {
393 types.push(Self::build_mutation_type(schema));
394 }
395
396 if !schema.subscriptions.is_empty() {
398 types.push(Self::build_subscription_type(schema));
399 }
400
401 let mut directives = Self::builtin_directives();
403 directives.extend(Self::build_custom_directives(&schema.directives));
404
405 IntrospectionSchema {
406 description: Some("FraiseQL GraphQL Schema".to_string()),
407 types,
408 query_type: IntrospectionTypeRef {
409 name: "Query".to_string(),
410 },
411 mutation_type: if schema.mutations.is_empty() {
412 None
413 } else {
414 Some(IntrospectionTypeRef {
415 name: "Mutation".to_string(),
416 })
417 },
418 subscription_type: if schema.subscriptions.is_empty() {
419 None
420 } else {
421 Some(IntrospectionTypeRef {
422 name: "Subscription".to_string(),
423 })
424 },
425 directives,
426 }
427 }
428
429 #[must_use]
431 pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
432 let mut map = HashMap::new();
433 for t in &schema.types {
434 if let Some(ref name) = t.name {
435 map.insert(name.clone(), t.clone());
436 }
437 }
438 map
439 }
440
441 fn builtin_scalars() -> Vec<IntrospectionType> {
443 vec![
444 Self::scalar_type("Int", "Built-in Int scalar"),
445 Self::scalar_type("Float", "Built-in Float scalar"),
446 Self::scalar_type("String", "Built-in String scalar"),
447 Self::scalar_type("Boolean", "Built-in Boolean scalar"),
448 Self::scalar_type("ID", "Built-in ID scalar"),
449 Self::scalar_type_with_url(
451 "DateTime",
452 "ISO-8601 datetime string",
453 Some("https://scalars.graphql.org/andimarek/date-time"),
454 ),
455 Self::scalar_type_with_url(
456 "Date",
457 "ISO-8601 date string",
458 Some("https://scalars.graphql.org/andimarek/local-date"),
459 ),
460 Self::scalar_type_with_url(
461 "Time",
462 "ISO-8601 time string",
463 Some("https://scalars.graphql.org/andimarek/local-time"),
464 ),
465 Self::scalar_type_with_url(
466 "UUID",
467 "UUID string",
468 Some("https://tools.ietf.org/html/rfc4122"),
469 ),
470 Self::scalar_type_with_url(
471 "JSON",
472 "Arbitrary JSON value",
473 Some("https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"),
474 ),
475 Self::scalar_type("Decimal", "Decimal number"),
476 ]
477 }
478
479 fn scalar_type(name: &str, description: &str) -> IntrospectionType {
481 Self::scalar_type_with_url(name, description, None)
482 }
483
484 fn scalar_type_with_url(
486 name: &str,
487 description: &str,
488 specified_by_url: Option<&str>,
489 ) -> IntrospectionType {
490 IntrospectionType {
491 kind: TypeKind::Scalar,
492 name: Some(name.to_string()),
493 description: Some(description.to_string()),
494 fields: None,
495 interfaces: None,
496 possible_types: None,
497 enum_values: None,
498 input_fields: None,
499 of_type: None,
500 specified_by_u_r_l: specified_by_url.map(ToString::to_string),
501 }
502 }
503
504 fn build_object_type(type_def: &TypeDefinition) -> IntrospectionType {
506 let fields = type_def.fields.iter().map(|f| Self::build_field(f)).collect();
507
508 let interfaces: Vec<IntrospectionTypeRef> = type_def
510 .implements
511 .iter()
512 .map(|name| IntrospectionTypeRef { name: name.clone() })
513 .collect();
514
515 IntrospectionType {
516 kind: TypeKind::Object,
517 name: Some(type_def.name.clone()),
518 description: type_def.description.clone(),
519 fields: Some(fields),
520 interfaces: Some(interfaces),
521 possible_types: None,
522 enum_values: None,
523 input_fields: None,
524 of_type: None,
525 specified_by_u_r_l: None,
526 }
527 }
528
529 fn build_enum_type(enum_def: &EnumDefinition) -> IntrospectionType {
531 let enum_values = enum_def
532 .values
533 .iter()
534 .map(|v| IntrospectionEnumValue {
535 name: v.name.clone(),
536 description: v.description.clone(),
537 is_deprecated: v.deprecation.is_some(),
538 deprecation_reason: v.deprecation.as_ref().and_then(|d| d.reason.clone()),
539 })
540 .collect();
541
542 IntrospectionType {
543 kind: TypeKind::Enum,
544 name: Some(enum_def.name.clone()),
545 description: enum_def.description.clone(),
546 fields: None,
547 interfaces: None,
548 possible_types: None,
549 enum_values: Some(enum_values),
550 input_fields: None,
551 of_type: None,
552 specified_by_u_r_l: None,
553 }
554 }
555
556 fn build_input_object_type(input_def: &InputObjectDefinition) -> IntrospectionType {
558 let input_fields = input_def
559 .fields
560 .iter()
561 .map(|f| IntrospectionInputValue {
562 name: f.name.clone(),
563 description: f.description.clone(),
564 input_type: Self::type_ref(&f.field_type),
565 default_value: f.default_value.clone(),
566 is_deprecated: f.is_deprecated(),
567 deprecation_reason: f.deprecation.as_ref().and_then(|d| d.reason.clone()),
568 })
569 .collect();
570
571 IntrospectionType {
572 kind: TypeKind::InputObject,
573 name: Some(input_def.name.clone()),
574 description: input_def.description.clone(),
575 fields: None,
576 interfaces: None,
577 possible_types: None,
578 enum_values: None,
579 input_fields: Some(input_fields),
580 of_type: None,
581 specified_by_u_r_l: None,
582 }
583 }
584
585 fn build_interface_type(
587 interface_def: &InterfaceDefinition,
588 schema: &CompiledSchema,
589 ) -> IntrospectionType {
590 let fields = interface_def.fields.iter().map(|f| Self::build_field(f)).collect();
592
593 let possible_types: Vec<IntrospectionTypeRef> = schema
595 .find_implementors(&interface_def.name)
596 .iter()
597 .map(|t| IntrospectionTypeRef {
598 name: t.name.clone(),
599 })
600 .collect();
601
602 IntrospectionType {
603 kind: TypeKind::Interface,
604 name: Some(interface_def.name.clone()),
605 description: interface_def.description.clone(),
606 fields: Some(fields),
607 interfaces: None,
608 possible_types: if possible_types.is_empty() {
609 None
610 } else {
611 Some(possible_types)
612 },
613 enum_values: None,
614 input_fields: None,
615 of_type: None,
616 specified_by_u_r_l: None,
617 }
618 }
619
620 fn build_union_type(union_def: &UnionDefinition) -> IntrospectionType {
622 let possible_types: Vec<IntrospectionTypeRef> = union_def
624 .member_types
625 .iter()
626 .map(|name| IntrospectionTypeRef { name: name.clone() })
627 .collect();
628
629 IntrospectionType {
630 kind: TypeKind::Union,
631 name: Some(union_def.name.clone()),
632 description: union_def.description.clone(),
633 fields: None, interfaces: None,
635 possible_types: if possible_types.is_empty() {
636 None
637 } else {
638 Some(possible_types)
639 },
640 enum_values: None,
641 input_fields: None,
642 of_type: None,
643 specified_by_u_r_l: None,
644 }
645 }
646
647 fn build_field(field: &FieldDefinition) -> IntrospectionField {
649 IntrospectionField {
650 name: field.output_name().to_string(),
651 description: field.description.clone(),
652 args: vec![], field_type: Self::field_type_to_introspection(
654 &field.field_type,
655 field.nullable,
656 ),
657 is_deprecated: field.is_deprecated(),
658 deprecation_reason: field.deprecation_reason().map(ToString::to_string),
659 }
660 }
661
662 fn field_type_to_introspection(field_type: &FieldType, nullable: bool) -> IntrospectionType {
664 let inner = match field_type {
665 FieldType::Int => Self::type_ref("Int"),
666 FieldType::Float => Self::type_ref("Float"),
667 FieldType::String => Self::type_ref("String"),
668 FieldType::Boolean => Self::type_ref("Boolean"),
669 FieldType::Id => Self::type_ref("ID"),
670 FieldType::DateTime => Self::type_ref("DateTime"),
671 FieldType::Date => Self::type_ref("Date"),
672 FieldType::Time => Self::type_ref("Time"),
673 FieldType::Uuid => Self::type_ref("UUID"),
674 FieldType::Json => Self::type_ref("JSON"),
675 FieldType::Decimal => Self::type_ref("Decimal"),
676 FieldType::Object(name) => Self::type_ref(name),
677 FieldType::Enum(name) => Self::type_ref(name),
678 FieldType::Input(name) => Self::type_ref(name),
679 FieldType::Interface(name) => Self::type_ref(name),
680 FieldType::Union(name) => Self::type_ref(name),
681 FieldType::Scalar(name) => Self::type_ref(name), FieldType::List(inner) => IntrospectionType {
683 kind: TypeKind::List,
684 name: None,
685 description: None,
686 fields: None,
687 interfaces: None,
688 possible_types: None,
689 enum_values: None,
690 input_fields: None,
691 of_type: Some(Box::new(Self::field_type_to_introspection(inner, true))),
692 specified_by_u_r_l: None,
693 },
694 FieldType::Vector => Self::type_ref("JSON"), };
696
697 if nullable {
698 inner
699 } else {
700 IntrospectionType {
702 kind: TypeKind::NonNull,
703 name: None,
704 description: None,
705 fields: None,
706 interfaces: None,
707 possible_types: None,
708 enum_values: None,
709 input_fields: None,
710 of_type: Some(Box::new(inner)),
711 specified_by_u_r_l: None,
712 }
713 }
714 }
715
716 fn type_ref(name: &str) -> IntrospectionType {
718 IntrospectionType {
719 kind: TypeKind::Scalar, name: Some(name.to_string()),
721 description: None,
722 fields: None,
723 interfaces: None,
724 possible_types: None,
725 enum_values: None,
726 input_fields: None,
727 of_type: None,
728 specified_by_u_r_l: None,
729 }
730 }
731
732 fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
734 let fields: Vec<IntrospectionField> =
735 schema.queries.iter().map(|q| Self::build_query_field(q)).collect();
736
737 IntrospectionType {
738 kind: TypeKind::Object,
739 name: Some("Query".to_string()),
740 description: Some("Root query type".to_string()),
741 fields: Some(fields),
742 interfaces: Some(vec![]),
743 possible_types: None,
744 enum_values: None,
745 input_fields: None,
746 of_type: None,
747 specified_by_u_r_l: None,
748 }
749 }
750
751 fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
753 let fields: Vec<IntrospectionField> =
754 schema.mutations.iter().map(|m| Self::build_mutation_field(m)).collect();
755
756 IntrospectionType {
757 kind: TypeKind::Object,
758 name: Some("Mutation".to_string()),
759 description: Some("Root mutation type".to_string()),
760 fields: Some(fields),
761 interfaces: Some(vec![]),
762 possible_types: None,
763 enum_values: None,
764 input_fields: None,
765 of_type: None,
766 specified_by_u_r_l: None,
767 }
768 }
769
770 fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
772 let fields: Vec<IntrospectionField> =
773 schema.subscriptions.iter().map(|s| Self::build_subscription_field(s)).collect();
774
775 IntrospectionType {
776 kind: TypeKind::Object,
777 name: Some("Subscription".to_string()),
778 description: Some("Root subscription type".to_string()),
779 fields: Some(fields),
780 interfaces: Some(vec![]),
781 possible_types: None,
782 enum_values: None,
783 input_fields: None,
784 of_type: None,
785 specified_by_u_r_l: None,
786 }
787 }
788
789 fn build_query_field(query: &QueryDefinition) -> IntrospectionField {
791 let return_type = Self::type_ref(&query.return_type);
792 let return_type = if query.returns_list {
793 IntrospectionType {
794 kind: TypeKind::List,
795 name: None,
796 description: None,
797 fields: None,
798 interfaces: None,
799 possible_types: None,
800 enum_values: None,
801 input_fields: None,
802 of_type: Some(Box::new(return_type)),
803 specified_by_u_r_l: None,
804 }
805 } else {
806 return_type
807 };
808
809 let return_type = if query.nullable {
810 return_type
811 } else {
812 IntrospectionType {
813 kind: TypeKind::NonNull,
814 name: None,
815 description: None,
816 fields: None,
817 interfaces: None,
818 possible_types: None,
819 enum_values: None,
820 input_fields: None,
821 of_type: Some(Box::new(return_type)),
822 specified_by_u_r_l: None,
823 }
824 };
825
826 let args: Vec<IntrospectionInputValue> = query
828 .arguments
829 .iter()
830 .map(|arg| IntrospectionInputValue {
831 name: arg.name.clone(),
832 description: arg.description.clone(),
833 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
834 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
835 is_deprecated: arg.is_deprecated(),
836 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
837 })
838 .collect();
839
840 IntrospectionField {
841 name: query.name.clone(),
842 description: query.description.clone(),
843 args,
844 field_type: return_type,
845 is_deprecated: query.is_deprecated(),
846 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
847 }
848 }
849
850 fn build_mutation_field(mutation: &super::MutationDefinition) -> IntrospectionField {
852 let return_type = Self::type_ref(&mutation.return_type);
854
855 let args: Vec<IntrospectionInputValue> = mutation
857 .arguments
858 .iter()
859 .map(|arg| IntrospectionInputValue {
860 name: arg.name.clone(),
861 description: arg.description.clone(),
862 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
863 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
864 is_deprecated: arg.is_deprecated(),
865 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
866 })
867 .collect();
868
869 IntrospectionField {
870 name: mutation.name.clone(),
871 description: mutation.description.clone(),
872 args,
873 field_type: return_type,
874 is_deprecated: mutation.is_deprecated(),
875 deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
876 }
877 }
878
879 fn build_subscription_field(
881 subscription: &super::SubscriptionDefinition,
882 ) -> IntrospectionField {
883 let return_type = Self::type_ref(&subscription.return_type);
885
886 let args: Vec<IntrospectionInputValue> = subscription
888 .arguments
889 .iter()
890 .map(|arg| IntrospectionInputValue {
891 name: arg.name.clone(),
892 description: arg.description.clone(),
893 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
894 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
895 is_deprecated: arg.is_deprecated(),
896 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
897 })
898 .collect();
899
900 IntrospectionField {
901 name: subscription.name.clone(),
902 description: subscription.description.clone(),
903 args,
904 field_type: return_type,
905 is_deprecated: subscription.is_deprecated(),
906 deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
907 }
908 }
909
910 fn builtin_directives() -> Vec<IntrospectionDirective> {
912 vec![
913 IntrospectionDirective {
914 name: "skip".to_string(),
915 description: Some(
916 "Directs the executor to skip this field or fragment when the `if` argument is true."
917 .to_string(),
918 ),
919 locations: vec![
920 DirectiveLocation::Field,
921 DirectiveLocation::FragmentSpread,
922 DirectiveLocation::InlineFragment,
923 ],
924 args: vec![IntrospectionInputValue {
925 name: "if".to_string(),
926 description: Some("Skipped when true.".to_string()),
927 input_type: IntrospectionType {
928 kind: TypeKind::NonNull,
929 name: None,
930 description: None,
931 fields: None,
932 interfaces: None,
933 possible_types: None,
934 enum_values: None,
935 input_fields: None,
936 of_type: Some(Box::new(Self::type_ref("Boolean"))),
937 specified_by_u_r_l: None,
938 },
939 default_value: None,
940 is_deprecated: false,
941 deprecation_reason: None,
942 }],
943 is_repeatable: false,
944 },
945 IntrospectionDirective {
946 name: "include".to_string(),
947 description: Some(
948 "Directs the executor to include this field or fragment only when the `if` argument is true."
949 .to_string(),
950 ),
951 locations: vec![
952 DirectiveLocation::Field,
953 DirectiveLocation::FragmentSpread,
954 DirectiveLocation::InlineFragment,
955 ],
956 args: vec![IntrospectionInputValue {
957 name: "if".to_string(),
958 description: Some("Included when true.".to_string()),
959 input_type: IntrospectionType {
960 kind: TypeKind::NonNull,
961 name: None,
962 description: None,
963 fields: None,
964 interfaces: None,
965 possible_types: None,
966 enum_values: None,
967 input_fields: None,
968 of_type: Some(Box::new(Self::type_ref("Boolean"))),
969 specified_by_u_r_l: None,
970 },
971 default_value: None,
972 is_deprecated: false,
973 deprecation_reason: None,
974 }],
975 is_repeatable: false,
976 },
977 IntrospectionDirective {
978 name: "deprecated".to_string(),
979 description: Some(
980 "Marks an element of a GraphQL schema as no longer supported.".to_string(),
981 ),
982 locations: vec![
983 DirectiveLocation::FieldDefinition,
984 DirectiveLocation::EnumValue,
985 DirectiveLocation::ArgumentDefinition,
986 DirectiveLocation::InputFieldDefinition,
987 ],
988 args: vec![IntrospectionInputValue {
989 name: "reason".to_string(),
990 description: Some(
991 "Explains why this element was deprecated.".to_string(),
992 ),
993 input_type: Self::type_ref("String"),
994 default_value: Some("\"No longer supported\"".to_string()),
995 is_deprecated: false,
996 deprecation_reason: None,
997 }],
998 is_repeatable: false,
999 },
1000 ]
1001 }
1002
1003 fn build_custom_directives(directives: &[DirectiveDefinition]) -> Vec<IntrospectionDirective> {
1005 directives.iter().map(|d| Self::build_custom_directive(d)).collect()
1006 }
1007
1008 fn build_custom_directive(directive: &DirectiveDefinition) -> IntrospectionDirective {
1010 let locations: Vec<DirectiveLocation> =
1011 directive.locations.iter().map(|loc| DirectiveLocation::from(*loc)).collect();
1012
1013 let args: Vec<IntrospectionInputValue> = directive
1014 .arguments
1015 .iter()
1016 .map(|arg| IntrospectionInputValue {
1017 name: arg.name.clone(),
1018 description: arg.description.clone(),
1019 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1020 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
1021 is_deprecated: arg.is_deprecated(),
1022 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1023 })
1024 .collect();
1025
1026 IntrospectionDirective {
1027 name: directive.name.clone(),
1028 description: directive.description.clone(),
1029 locations,
1030 args,
1031 is_repeatable: directive.is_repeatable,
1032 }
1033 }
1034}
1035
1036#[derive(Debug, Clone)]
1042pub struct IntrospectionResponses {
1043 pub schema_response: String,
1045 pub type_responses: HashMap<String, String>,
1047}
1048
1049impl IntrospectionResponses {
1050 #[must_use]
1054 pub fn build(schema: &CompiledSchema) -> Self {
1055 let introspection = IntrospectionBuilder::build(schema);
1056 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1057
1058 let schema_response = serde_json::json!({
1060 "data": {
1061 "__schema": introspection
1062 }
1063 })
1064 .to_string();
1065
1066 let mut type_responses = HashMap::new();
1068 for (name, t) in type_map {
1069 let response = serde_json::json!({
1070 "data": {
1071 "__type": t
1072 }
1073 })
1074 .to_string();
1075 type_responses.insert(name, response);
1076 }
1077
1078 Self {
1079 schema_response,
1080 type_responses,
1081 }
1082 }
1083
1084 #[must_use]
1086 pub fn get_type_response(&self, type_name: &str) -> String {
1087 self.type_responses.get(type_name).cloned().unwrap_or_else(|| {
1088 serde_json::json!({
1089 "data": {
1090 "__type": null
1091 }
1092 })
1093 .to_string()
1094 })
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101 use crate::schema::{AutoParams, FieldType};
1102
1103 fn test_schema() -> CompiledSchema {
1104 let mut schema = CompiledSchema::new();
1105
1106 schema.types.push(
1108 TypeDefinition::new("User", "v_user")
1109 .with_field(FieldDefinition::new("id", FieldType::Id))
1110 .with_field(FieldDefinition::new("name", FieldType::String))
1111 .with_field(FieldDefinition::nullable("email", FieldType::String))
1112 .with_description("A user in the system"),
1113 );
1114
1115 schema.queries.push(QueryDefinition {
1117 name: "users".to_string(),
1118 return_type: "User".to_string(),
1119 returns_list: true,
1120 nullable: false,
1121 arguments: vec![],
1122 sql_source: Some("v_user".to_string()),
1123 description: Some("Get all users".to_string()),
1124 auto_params: AutoParams::default(),
1125 deprecation: None,
1126 });
1127
1128 schema.queries.push(QueryDefinition {
1130 name: "user".to_string(),
1131 return_type: "User".to_string(),
1132 returns_list: false,
1133 nullable: true,
1134 arguments: vec![crate::schema::ArgumentDefinition {
1135 name: "id".to_string(),
1136 arg_type: FieldType::Id,
1137 nullable: false, default_value: None,
1139 description: Some("User ID".to_string()),
1140 deprecation: None,
1141 }],
1142 sql_source: Some("v_user".to_string()),
1143 description: Some("Get user by ID".to_string()),
1144 auto_params: AutoParams::default(),
1145 deprecation: None,
1146 });
1147
1148 schema
1149 }
1150
1151 #[test]
1152 fn test_build_introspection_schema() {
1153 let schema = test_schema();
1154 let introspection = IntrospectionBuilder::build(&schema);
1155
1156 assert_eq!(introspection.query_type.name, "Query");
1158
1159 assert!(introspection.mutation_type.is_none());
1161
1162 let scalar_names: Vec<_> = introspection
1164 .types
1165 .iter()
1166 .filter(|t| t.kind == TypeKind::Scalar)
1167 .filter_map(|t| t.name.as_ref())
1168 .collect();
1169 assert!(scalar_names.contains(&&"Int".to_string()));
1170 assert!(scalar_names.contains(&&"String".to_string()));
1171 assert!(scalar_names.contains(&&"Boolean".to_string()));
1172
1173 let user_type = introspection
1175 .types
1176 .iter()
1177 .find(|t| t.name.as_ref() == Some(&"User".to_string()));
1178 assert!(user_type.is_some());
1179 let user_type = user_type.unwrap();
1180 assert_eq!(user_type.kind, TypeKind::Object);
1181 assert!(user_type.fields.is_some());
1182 assert_eq!(user_type.fields.as_ref().unwrap().len(), 3);
1183 }
1184
1185 #[test]
1186 fn test_build_introspection_responses() {
1187 let schema = test_schema();
1188 let responses = IntrospectionResponses::build(&schema);
1189
1190 assert!(responses.schema_response.contains("__schema"));
1192 assert!(responses.schema_response.contains("Query"));
1193
1194 assert!(responses.type_responses.contains_key("User"));
1196 assert!(responses.type_responses.contains_key("Query"));
1197 assert!(responses.type_responses.contains_key("Int"));
1198
1199 let unknown = responses.get_type_response("Unknown");
1201 assert!(unknown.contains("null"));
1202 }
1203
1204 #[test]
1205 fn test_query_field_introspection() {
1206 let schema = test_schema();
1207 let introspection = IntrospectionBuilder::build(&schema);
1208
1209 let query_type = introspection
1210 .types
1211 .iter()
1212 .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1213 .unwrap();
1214
1215 let fields = query_type.fields.as_ref().unwrap();
1216
1217 let users_field = fields.iter().find(|f| f.name == "users").unwrap();
1219 assert_eq!(users_field.field_type.kind, TypeKind::NonNull);
1220 assert!(users_field.args.is_empty());
1221
1222 let user_field = fields.iter().find(|f| f.name == "user").unwrap();
1224 assert!(!user_field.args.is_empty());
1225 assert_eq!(user_field.args[0].name, "id");
1226 }
1227
1228 #[test]
1229 fn test_field_type_non_null() {
1230 let schema = test_schema();
1231 let introspection = IntrospectionBuilder::build(&schema);
1232
1233 let user_type = introspection
1234 .types
1235 .iter()
1236 .find(|t| t.name.as_ref() == Some(&"User".to_string()))
1237 .unwrap();
1238
1239 let fields = user_type.fields.as_ref().unwrap();
1240
1241 let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1243 assert_eq!(id_field.field_type.kind, TypeKind::NonNull);
1244
1245 let email_field = fields.iter().find(|f| f.name == "email").unwrap();
1247 assert_ne!(email_field.field_type.kind, TypeKind::NonNull);
1248 }
1249
1250 #[test]
1251 fn test_deprecated_field_introspection() {
1252 use crate::schema::DeprecationInfo;
1253
1254 let mut schema = CompiledSchema::new();
1256 schema.types.push(TypeDefinition {
1257 name: "Product".to_string(),
1258 sql_source: "products".to_string(),
1259 jsonb_column: "data".to_string(),
1260 description: None,
1261 sql_projection_hint: None,
1262 implements: vec![],
1263 fields: vec![
1264 FieldDefinition::new("id", FieldType::Id),
1265 FieldDefinition {
1266 name: "oldSku".to_string(),
1267 field_type: FieldType::String,
1268 nullable: false,
1269 description: Some("Legacy SKU field".to_string()),
1270 default_value: None,
1271 vector_config: None,
1272 alias: None,
1273 deprecation: Some(DeprecationInfo {
1274 reason: Some("Use 'sku' instead".to_string()),
1275 }),
1276 requires_scope: None,
1277 },
1278 FieldDefinition::new("sku", FieldType::String),
1279 ],
1280 });
1281
1282 let introspection = IntrospectionBuilder::build(&schema);
1283
1284 let product_type = introspection
1286 .types
1287 .iter()
1288 .find(|t| t.name.as_ref() == Some(&"Product".to_string()))
1289 .unwrap();
1290
1291 let fields = product_type.fields.as_ref().unwrap();
1292
1293 let old_sku_field = fields.iter().find(|f| f.name == "oldSku").unwrap();
1295 assert!(old_sku_field.is_deprecated);
1296 assert_eq!(old_sku_field.deprecation_reason, Some("Use 'sku' instead".to_string()));
1297
1298 let sku_field = fields.iter().find(|f| f.name == "sku").unwrap();
1300 assert!(!sku_field.is_deprecated);
1301 assert!(sku_field.deprecation_reason.is_none());
1302
1303 let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1305 assert!(!id_field.is_deprecated);
1306 assert!(id_field.deprecation_reason.is_none());
1307 }
1308
1309 #[test]
1310 fn test_enum_type_introspection() {
1311 use crate::schema::{EnumDefinition, EnumValueDefinition};
1312
1313 let mut schema = CompiledSchema::new();
1314
1315 schema.enums.push(EnumDefinition {
1317 name: "OrderStatus".to_string(),
1318 description: Some("Status of an order".to_string()),
1319 values: vec![
1320 EnumValueDefinition {
1321 name: "PENDING".to_string(),
1322 description: Some("Order is pending".to_string()),
1323 deprecation: None,
1324 },
1325 EnumValueDefinition {
1326 name: "PROCESSING".to_string(),
1327 description: None,
1328 deprecation: None,
1329 },
1330 EnumValueDefinition {
1331 name: "SHIPPED".to_string(),
1332 description: None,
1333 deprecation: None,
1334 },
1335 EnumValueDefinition {
1336 name: "CANCELLED".to_string(),
1337 description: Some("Order was cancelled".to_string()),
1338 deprecation: Some(crate::schema::DeprecationInfo {
1339 reason: Some("Use REFUNDED instead".to_string()),
1340 }),
1341 },
1342 ],
1343 });
1344
1345 let introspection = IntrospectionBuilder::build(&schema);
1346
1347 let order_status = introspection
1349 .types
1350 .iter()
1351 .find(|t| t.name.as_ref() == Some(&"OrderStatus".to_string()))
1352 .unwrap();
1353
1354 assert_eq!(order_status.kind, TypeKind::Enum);
1355 assert_eq!(order_status.description, Some("Status of an order".to_string()));
1356
1357 let enum_values = order_status.enum_values.as_ref().unwrap();
1359 assert_eq!(enum_values.len(), 4);
1360
1361 let pending = enum_values.iter().find(|v| v.name == "PENDING").unwrap();
1363 assert_eq!(pending.description, Some("Order is pending".to_string()));
1364 assert!(!pending.is_deprecated);
1365 assert!(pending.deprecation_reason.is_none());
1366
1367 let cancelled = enum_values.iter().find(|v| v.name == "CANCELLED").unwrap();
1369 assert!(cancelled.is_deprecated);
1370 assert_eq!(cancelled.deprecation_reason, Some("Use REFUNDED instead".to_string()));
1371
1372 assert!(order_status.fields.is_none());
1374 }
1375
1376 #[test]
1377 fn test_input_object_introspection() {
1378 use crate::schema::{InputFieldDefinition, InputObjectDefinition};
1379
1380 let mut schema = CompiledSchema::new();
1381
1382 schema.input_types.push(InputObjectDefinition {
1384 name: "UserFilter".to_string(),
1385 description: Some("Filter for user queries".to_string()),
1386 fields: vec![
1387 InputFieldDefinition {
1388 name: "name".to_string(),
1389 field_type: "String".to_string(),
1390 description: Some("Filter by name".to_string()),
1391 default_value: None,
1392 deprecation: None,
1393 },
1394 InputFieldDefinition {
1395 name: "email".to_string(),
1396 field_type: "String".to_string(),
1397 description: None,
1398 default_value: None,
1399 deprecation: None,
1400 },
1401 InputFieldDefinition {
1402 name: "limit".to_string(),
1403 field_type: "Int".to_string(),
1404 description: Some("Max results".to_string()),
1405 default_value: Some("10".to_string()),
1406 deprecation: None,
1407 },
1408 ],
1409 });
1410
1411 let introspection = IntrospectionBuilder::build(&schema);
1412
1413 let user_filter = introspection
1415 .types
1416 .iter()
1417 .find(|t| t.name.as_ref() == Some(&"UserFilter".to_string()))
1418 .unwrap();
1419
1420 assert_eq!(user_filter.kind, TypeKind::InputObject);
1421 assert_eq!(user_filter.description, Some("Filter for user queries".to_string()));
1422
1423 let input_fields = user_filter.input_fields.as_ref().unwrap();
1425 assert_eq!(input_fields.len(), 3);
1426
1427 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1429 assert_eq!(name_field.description, Some("Filter by name".to_string()));
1430 assert!(name_field.default_value.is_none());
1431
1432 let limit_field = input_fields.iter().find(|f| f.name == "limit").unwrap();
1434 assert_eq!(limit_field.description, Some("Max results".to_string()));
1435 assert_eq!(limit_field.default_value, Some("10".to_string()));
1436
1437 assert!(user_filter.fields.is_none());
1439 }
1440
1441 #[test]
1442 fn test_enum_in_type_map() {
1443 use crate::schema::EnumDefinition;
1444
1445 let mut schema = CompiledSchema::new();
1446 schema.enums.push(EnumDefinition {
1447 name: "Status".to_string(),
1448 description: None,
1449 values: vec![],
1450 });
1451
1452 let introspection = IntrospectionBuilder::build(&schema);
1453 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1454
1455 assert!(type_map.contains_key("Status"));
1457 let status = type_map.get("Status").unwrap();
1458 assert_eq!(status.kind, TypeKind::Enum);
1459 }
1460
1461 #[test]
1462 fn test_input_object_in_type_map() {
1463 use crate::schema::InputObjectDefinition;
1464
1465 let mut schema = CompiledSchema::new();
1466 schema.input_types.push(InputObjectDefinition {
1467 name: "CreateUserInput".to_string(),
1468 description: None,
1469 fields: vec![],
1470 });
1471
1472 let introspection = IntrospectionBuilder::build(&schema);
1473 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1474
1475 assert!(type_map.contains_key("CreateUserInput"));
1477 let input = type_map.get("CreateUserInput").unwrap();
1478 assert_eq!(input.kind, TypeKind::InputObject);
1479 }
1480
1481 #[test]
1482 fn test_interface_introspection() {
1483 use crate::schema::InterfaceDefinition;
1484
1485 let mut schema = CompiledSchema::new();
1486
1487 schema.interfaces.push(InterfaceDefinition {
1489 name: "Node".to_string(),
1490 description: Some("An object with a globally unique ID".to_string()),
1491 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1492 });
1493
1494 schema.types.push(TypeDefinition {
1496 name: "User".to_string(),
1497 sql_source: "users".to_string(),
1498 jsonb_column: "data".to_string(),
1499 description: Some("A user".to_string()),
1500 sql_projection_hint: None,
1501 implements: vec!["Node".to_string()],
1502 fields: vec![
1503 FieldDefinition::new("id", FieldType::Id),
1504 FieldDefinition::new("name", FieldType::String),
1505 ],
1506 });
1507
1508 schema.types.push(TypeDefinition {
1509 name: "Post".to_string(),
1510 sql_source: "posts".to_string(),
1511 jsonb_column: "data".to_string(),
1512 description: Some("A blog post".to_string()),
1513 sql_projection_hint: None,
1514 implements: vec!["Node".to_string()],
1515 fields: vec![
1516 FieldDefinition::new("id", FieldType::Id),
1517 FieldDefinition::new("title", FieldType::String),
1518 ],
1519 });
1520
1521 let introspection = IntrospectionBuilder::build(&schema);
1522
1523 let node = introspection
1525 .types
1526 .iter()
1527 .find(|t| t.name.as_ref() == Some(&"Node".to_string()))
1528 .unwrap();
1529
1530 assert_eq!(node.kind, TypeKind::Interface);
1531 assert_eq!(node.description, Some("An object with a globally unique ID".to_string()));
1532
1533 let fields = node.fields.as_ref().unwrap();
1535 assert_eq!(fields.len(), 1);
1536 assert_eq!(fields[0].name, "id");
1537
1538 let possible_types = node.possible_types.as_ref().unwrap();
1540 assert_eq!(possible_types.len(), 2);
1541 assert!(possible_types.iter().any(|t| t.name == "User"));
1542 assert!(possible_types.iter().any(|t| t.name == "Post"));
1543
1544 assert!(node.enum_values.is_none());
1546 assert!(node.input_fields.is_none());
1547 }
1548
1549 #[test]
1550 fn test_type_implements_interface() {
1551 use crate::schema::InterfaceDefinition;
1552
1553 let mut schema = CompiledSchema::new();
1554
1555 schema.interfaces.push(InterfaceDefinition {
1557 name: "Node".to_string(),
1558 description: None,
1559 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1560 });
1561
1562 schema.interfaces.push(InterfaceDefinition {
1563 name: "Timestamped".to_string(),
1564 description: None,
1565 fields: vec![FieldDefinition::new("createdAt", FieldType::DateTime)],
1566 });
1567
1568 schema.types.push(TypeDefinition {
1570 name: "Comment".to_string(),
1571 sql_source: "comments".to_string(),
1572 jsonb_column: "data".to_string(),
1573 description: None,
1574 sql_projection_hint: None,
1575 implements: vec!["Node".to_string(), "Timestamped".to_string()],
1576 fields: vec![
1577 FieldDefinition::new("id", FieldType::Id),
1578 FieldDefinition::new("createdAt", FieldType::DateTime),
1579 FieldDefinition::new("text", FieldType::String),
1580 ],
1581 });
1582
1583 let introspection = IntrospectionBuilder::build(&schema);
1584
1585 let comment = introspection
1587 .types
1588 .iter()
1589 .find(|t| t.name.as_ref() == Some(&"Comment".to_string()))
1590 .unwrap();
1591
1592 assert_eq!(comment.kind, TypeKind::Object);
1593
1594 let interfaces = comment.interfaces.as_ref().unwrap();
1596 assert_eq!(interfaces.len(), 2);
1597 assert!(interfaces.iter().any(|i| i.name == "Node"));
1598 assert!(interfaces.iter().any(|i| i.name == "Timestamped"));
1599 }
1600
1601 #[test]
1602 fn test_interface_in_type_map() {
1603 use crate::schema::InterfaceDefinition;
1604
1605 let mut schema = CompiledSchema::new();
1606 schema.interfaces.push(InterfaceDefinition {
1607 name: "Searchable".to_string(),
1608 description: None,
1609 fields: vec![],
1610 });
1611
1612 let introspection = IntrospectionBuilder::build(&schema);
1613 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1614
1615 assert!(type_map.contains_key("Searchable"));
1617 let interface = type_map.get("Searchable").unwrap();
1618 assert_eq!(interface.kind, TypeKind::Interface);
1619 }
1620
1621 #[test]
1622 fn test_filter_deprecated_fields() {
1623 let introspection_type = IntrospectionType {
1625 kind: TypeKind::Object,
1626 name: Some("TestType".to_string()),
1627 description: None,
1628 fields: Some(vec![
1629 IntrospectionField {
1630 name: "id".to_string(),
1631 description: None,
1632 args: vec![],
1633 field_type: IntrospectionBuilder::type_ref("ID"),
1634 is_deprecated: false,
1635 deprecation_reason: None,
1636 },
1637 IntrospectionField {
1638 name: "oldField".to_string(),
1639 description: None,
1640 args: vec![],
1641 field_type: IntrospectionBuilder::type_ref("String"),
1642 is_deprecated: true,
1643 deprecation_reason: Some("Use newField instead".to_string()),
1644 },
1645 IntrospectionField {
1646 name: "newField".to_string(),
1647 description: None,
1648 args: vec![],
1649 field_type: IntrospectionBuilder::type_ref("String"),
1650 is_deprecated: false,
1651 deprecation_reason: None,
1652 },
1653 ]),
1654 interfaces: None,
1655 possible_types: None,
1656 enum_values: None,
1657 input_fields: None,
1658 of_type: None,
1659 specified_by_u_r_l: None,
1660 };
1661
1662 let filtered = introspection_type.filter_deprecated_fields(false);
1664 let fields = filtered.fields.as_ref().unwrap();
1665 assert_eq!(fields.len(), 2);
1666 assert!(fields.iter().any(|f| f.name == "id"));
1667 assert!(fields.iter().any(|f| f.name == "newField"));
1668 assert!(!fields.iter().any(|f| f.name == "oldField"));
1669
1670 let unfiltered = introspection_type.filter_deprecated_fields(true);
1672 let fields = unfiltered.fields.as_ref().unwrap();
1673 assert_eq!(fields.len(), 3);
1674 }
1675
1676 #[test]
1677 fn test_filter_deprecated_enum_values() {
1678 let introspection_type = IntrospectionType {
1680 kind: TypeKind::Enum,
1681 name: Some("Status".to_string()),
1682 description: None,
1683 fields: None,
1684 interfaces: None,
1685 possible_types: None,
1686 enum_values: Some(vec![
1687 IntrospectionEnumValue {
1688 name: "ACTIVE".to_string(),
1689 description: None,
1690 is_deprecated: false,
1691 deprecation_reason: None,
1692 },
1693 IntrospectionEnumValue {
1694 name: "INACTIVE".to_string(),
1695 description: None,
1696 is_deprecated: true,
1697 deprecation_reason: Some("Use DISABLED instead".to_string()),
1698 },
1699 IntrospectionEnumValue {
1700 name: "DISABLED".to_string(),
1701 description: None,
1702 is_deprecated: false,
1703 deprecation_reason: None,
1704 },
1705 ]),
1706 input_fields: None,
1707 of_type: None,
1708 specified_by_u_r_l: None,
1709 };
1710
1711 let filtered = introspection_type.filter_deprecated_enum_values(false);
1713 let values = filtered.enum_values.as_ref().unwrap();
1714 assert_eq!(values.len(), 2);
1715 assert!(values.iter().any(|v| v.name == "ACTIVE"));
1716 assert!(values.iter().any(|v| v.name == "DISABLED"));
1717 assert!(!values.iter().any(|v| v.name == "INACTIVE"));
1718
1719 let unfiltered = introspection_type.filter_deprecated_enum_values(true);
1721 let values = unfiltered.enum_values.as_ref().unwrap();
1722 assert_eq!(values.len(), 3);
1723 }
1724
1725 #[test]
1726 fn test_specified_by_url_for_custom_scalars() {
1727 let schema = CompiledSchema::new();
1728 let introspection = IntrospectionBuilder::build(&schema);
1729
1730 let datetime = introspection
1732 .types
1733 .iter()
1734 .find(|t| t.name.as_ref() == Some(&"DateTime".to_string()))
1735 .unwrap();
1736
1737 assert_eq!(datetime.kind, TypeKind::Scalar);
1738 assert!(datetime.specified_by_u_r_l.is_some());
1739 assert!(datetime.specified_by_u_r_l.as_ref().unwrap().contains("date-time"));
1740
1741 let uuid = introspection
1743 .types
1744 .iter()
1745 .find(|t| t.name.as_ref() == Some(&"UUID".to_string()))
1746 .unwrap();
1747
1748 assert_eq!(uuid.kind, TypeKind::Scalar);
1749 assert!(uuid.specified_by_u_r_l.is_some());
1750 assert!(uuid.specified_by_u_r_l.as_ref().unwrap().contains("rfc4122"));
1751
1752 let int = introspection
1754 .types
1755 .iter()
1756 .find(|t| t.name.as_ref() == Some(&"Int".to_string()))
1757 .unwrap();
1758
1759 assert_eq!(int.kind, TypeKind::Scalar);
1760 assert!(int.specified_by_u_r_l.is_none());
1761 }
1762
1763 #[test]
1764 fn test_deprecated_query_introspection() {
1765 use crate::schema::{ArgumentDefinition, AutoParams, DeprecationInfo};
1766
1767 let mut schema = CompiledSchema::new();
1768
1769 schema.queries.push(QueryDefinition {
1771 name: "oldUsers".to_string(),
1772 return_type: "User".to_string(),
1773 returns_list: true,
1774 nullable: false,
1775 arguments: vec![],
1776 sql_source: Some("v_user".to_string()),
1777 description: Some("Old way to get users".to_string()),
1778 auto_params: AutoParams::default(),
1779 deprecation: Some(DeprecationInfo {
1780 reason: Some("Use 'users' instead".to_string()),
1781 }),
1782 });
1783
1784 schema.queries.push(QueryDefinition {
1786 name: "users".to_string(),
1787 return_type: "User".to_string(),
1788 returns_list: true,
1789 nullable: false,
1790 arguments: vec![
1791 ArgumentDefinition {
1792 name: "first".to_string(),
1793 arg_type: FieldType::Int,
1794 nullable: true,
1795 default_value: None,
1796 description: Some("Number of results to return".to_string()),
1797 deprecation: None,
1798 },
1799 ArgumentDefinition {
1800 name: "limit".to_string(),
1801 arg_type: FieldType::Int,
1802 nullable: true,
1803 default_value: None,
1804 description: Some("Old parameter for limiting results".to_string()),
1805 deprecation: Some(DeprecationInfo {
1806 reason: Some("Use 'first' instead".to_string()),
1807 }),
1808 },
1809 ],
1810 sql_source: Some("v_user".to_string()),
1811 description: Some("Get users with pagination".to_string()),
1812 auto_params: AutoParams::default(),
1813 deprecation: None,
1814 });
1815
1816 let introspection = IntrospectionBuilder::build(&schema);
1817
1818 let query_type = introspection
1820 .types
1821 .iter()
1822 .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1823 .unwrap();
1824
1825 let fields = query_type.fields.as_ref().unwrap();
1826
1827 let old_users = fields.iter().find(|f| f.name == "oldUsers").unwrap();
1829 assert!(old_users.is_deprecated);
1830 assert_eq!(old_users.deprecation_reason, Some("Use 'users' instead".to_string()));
1831
1832 let users = fields.iter().find(|f| f.name == "users").unwrap();
1834 assert!(!users.is_deprecated);
1835 assert!(users.deprecation_reason.is_none());
1836
1837 assert_eq!(users.args.len(), 2);
1839
1840 let first_arg = users.args.iter().find(|a| a.name == "first").unwrap();
1842 assert!(!first_arg.is_deprecated);
1843 assert!(first_arg.deprecation_reason.is_none());
1844
1845 let limit_arg = users.args.iter().find(|a| a.name == "limit").unwrap();
1847 assert!(limit_arg.is_deprecated);
1848 assert_eq!(limit_arg.deprecation_reason, Some("Use 'first' instead".to_string()));
1849 }
1850
1851 #[test]
1852 fn test_deprecated_input_field_introspection() {
1853 use crate::schema::{DeprecationInfo, InputFieldDefinition, InputObjectDefinition};
1854
1855 let mut schema = CompiledSchema::new();
1856
1857 schema.input_types.push(InputObjectDefinition {
1859 name: "CreateUserInput".to_string(),
1860 description: Some("Input for creating a user".to_string()),
1861 fields: vec![
1862 InputFieldDefinition {
1863 name: "name".to_string(),
1864 field_type: "String!".to_string(),
1865 default_value: None,
1866 description: Some("User name".to_string()),
1867 deprecation: None,
1868 },
1869 InputFieldDefinition {
1870 name: "oldEmail".to_string(),
1871 field_type: "String".to_string(),
1872 default_value: None,
1873 description: Some("Legacy email field".to_string()),
1874 deprecation: Some(DeprecationInfo {
1875 reason: Some("Use 'email' instead".to_string()),
1876 }),
1877 },
1878 ],
1879 });
1880
1881 let introspection = IntrospectionBuilder::build(&schema);
1882
1883 let create_user_input = introspection
1885 .types
1886 .iter()
1887 .find(|t| t.name.as_ref() == Some(&"CreateUserInput".to_string()))
1888 .unwrap();
1889
1890 let input_fields = create_user_input.input_fields.as_ref().unwrap();
1891
1892 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1894 assert!(!name_field.is_deprecated);
1895 assert!(name_field.deprecation_reason.is_none());
1896
1897 let old_email = input_fields.iter().find(|f| f.name == "oldEmail").unwrap();
1899 assert!(old_email.is_deprecated);
1900 assert_eq!(old_email.deprecation_reason, Some("Use 'email' instead".to_string()));
1901 }
1902}