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 IntrospectionValidationRule {
191 pub rule_type: String,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub pattern: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub pattern_message: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub min: Option<i64>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub max: Option<i64>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub allowed_values: Option<Vec<String>>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub algorithm: Option<String>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub field_reference: Option<String>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub operator: Option<String>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub field_list: Option<Vec<String>>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub description: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct IntrospectionInputValue {
242 pub name: String,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub description: Option<String>,
248
249 #[serde(rename = "type")]
251 pub input_type: IntrospectionType,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub default_value: Option<String>,
256
257 pub is_deprecated: bool,
259
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub deprecation_reason: Option<String>,
263
264 #[serde(default)]
266 pub validation_rules: Vec<IntrospectionValidationRule>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct IntrospectionEnumValue {
273 pub name: String,
275
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub description: Option<String>,
279
280 pub is_deprecated: bool,
282
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub deprecation_reason: Option<String>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct IntrospectionDirective {
292 pub name: String,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub description: Option<String>,
298
299 pub locations: Vec<DirectiveLocation>,
301
302 pub args: Vec<IntrospectionInputValue>,
304
305 #[serde(default)]
307 pub is_repeatable: bool,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
313pub enum TypeKind {
314 Scalar,
316 Object,
318 Interface,
320 Union,
322 Enum,
324 InputObject,
326 List,
328 NonNull,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
335pub enum DirectiveLocation {
336 Query,
338 Mutation,
340 Subscription,
342 Field,
344 FragmentDefinition,
346 FragmentSpread,
348 InlineFragment,
350 VariableDefinition,
352 Schema,
354 Scalar,
356 Object,
358 FieldDefinition,
360 ArgumentDefinition,
362 Interface,
364 Union,
366 Enum,
368 EnumValue,
370 InputObject,
372 InputFieldDefinition,
374}
375
376impl From<DirectiveLocationKind> for DirectiveLocation {
377 fn from(kind: DirectiveLocationKind) -> Self {
378 match kind {
379 DirectiveLocationKind::Query => Self::Query,
380 DirectiveLocationKind::Mutation => Self::Mutation,
381 DirectiveLocationKind::Subscription => Self::Subscription,
382 DirectiveLocationKind::Field => Self::Field,
383 DirectiveLocationKind::FragmentDefinition => Self::FragmentDefinition,
384 DirectiveLocationKind::FragmentSpread => Self::FragmentSpread,
385 DirectiveLocationKind::InlineFragment => Self::InlineFragment,
386 DirectiveLocationKind::VariableDefinition => Self::VariableDefinition,
387 DirectiveLocationKind::Schema => Self::Schema,
388 DirectiveLocationKind::Scalar => Self::Scalar,
389 DirectiveLocationKind::Object => Self::Object,
390 DirectiveLocationKind::FieldDefinition => Self::FieldDefinition,
391 DirectiveLocationKind::ArgumentDefinition => Self::ArgumentDefinition,
392 DirectiveLocationKind::Interface => Self::Interface,
393 DirectiveLocationKind::Union => Self::Union,
394 DirectiveLocationKind::Enum => Self::Enum,
395 DirectiveLocationKind::EnumValue => Self::EnumValue,
396 DirectiveLocationKind::InputObject => Self::InputObject,
397 DirectiveLocationKind::InputFieldDefinition => Self::InputFieldDefinition,
398 }
399 }
400}
401
402pub struct IntrospectionBuilder;
408
409impl IntrospectionBuilder {
410 #[must_use]
412 pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
413 let mut types = Vec::new();
414
415 types.extend(Self::builtin_scalars());
417
418 for type_def in &schema.types {
420 types.push(Self::build_object_type(type_def));
421 }
422
423 for enum_def in &schema.enums {
425 types.push(Self::build_enum_type(enum_def));
426 }
427
428 for input_def in &schema.input_types {
430 types.push(Self::build_input_object_type(input_def));
431 }
432
433 for interface_def in &schema.interfaces {
435 types.push(Self::build_interface_type(interface_def, schema));
436 }
437
438 for union_def in &schema.unions {
440 types.push(Self::build_union_type(union_def));
441 }
442
443 types.push(Self::build_query_type(schema));
445
446 if !schema.mutations.is_empty() {
448 types.push(Self::build_mutation_type(schema));
449 }
450
451 if !schema.subscriptions.is_empty() {
453 types.push(Self::build_subscription_type(schema));
454 }
455
456 let mut directives = Self::builtin_directives();
458 directives.extend(Self::build_custom_directives(&schema.directives));
459
460 IntrospectionSchema {
461 description: Some("FraiseQL GraphQL Schema".to_string()),
462 types,
463 query_type: IntrospectionTypeRef {
464 name: "Query".to_string(),
465 },
466 mutation_type: if schema.mutations.is_empty() {
467 None
468 } else {
469 Some(IntrospectionTypeRef {
470 name: "Mutation".to_string(),
471 })
472 },
473 subscription_type: if schema.subscriptions.is_empty() {
474 None
475 } else {
476 Some(IntrospectionTypeRef {
477 name: "Subscription".to_string(),
478 })
479 },
480 directives,
481 }
482 }
483
484 #[must_use]
486 pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
487 let mut map = HashMap::new();
488 for t in &schema.types {
489 if let Some(ref name) = t.name {
490 map.insert(name.clone(), t.clone());
491 }
492 }
493 map
494 }
495
496 fn builtin_scalars() -> Vec<IntrospectionType> {
498 vec![
499 Self::scalar_type("Int", "Built-in Int scalar"),
500 Self::scalar_type("Float", "Built-in Float scalar"),
501 Self::scalar_type("String", "Built-in String scalar"),
502 Self::scalar_type("Boolean", "Built-in Boolean scalar"),
503 Self::scalar_type("ID", "Built-in ID scalar"),
504 Self::scalar_type_with_url(
506 "DateTime",
507 "ISO-8601 datetime string",
508 Some("https://scalars.graphql.org/andimarek/date-time"),
509 ),
510 Self::scalar_type_with_url(
511 "Date",
512 "ISO-8601 date string",
513 Some("https://scalars.graphql.org/andimarek/local-date"),
514 ),
515 Self::scalar_type_with_url(
516 "Time",
517 "ISO-8601 time string",
518 Some("https://scalars.graphql.org/andimarek/local-time"),
519 ),
520 Self::scalar_type_with_url(
521 "UUID",
522 "UUID string",
523 Some("https://tools.ietf.org/html/rfc4122"),
524 ),
525 Self::scalar_type_with_url(
526 "JSON",
527 "Arbitrary JSON value",
528 Some("https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"),
529 ),
530 Self::scalar_type("Decimal", "Decimal number"),
531 ]
532 }
533
534 fn scalar_type(name: &str, description: &str) -> IntrospectionType {
536 Self::scalar_type_with_url(name, description, None)
537 }
538
539 fn scalar_type_with_url(
541 name: &str,
542 description: &str,
543 specified_by_url: Option<&str>,
544 ) -> IntrospectionType {
545 IntrospectionType {
546 kind: TypeKind::Scalar,
547 name: Some(name.to_string()),
548 description: Some(description.to_string()),
549 fields: None,
550 interfaces: None,
551 possible_types: None,
552 enum_values: None,
553 input_fields: None,
554 of_type: None,
555 specified_by_u_r_l: specified_by_url.map(ToString::to_string),
556 }
557 }
558
559 fn build_object_type(type_def: &TypeDefinition) -> IntrospectionType {
561 let fields = type_def.fields.iter().map(|f| Self::build_field(f)).collect();
562
563 let interfaces: Vec<IntrospectionTypeRef> = type_def
565 .implements
566 .iter()
567 .map(|name| IntrospectionTypeRef { name: name.clone() })
568 .collect();
569
570 IntrospectionType {
571 kind: TypeKind::Object,
572 name: Some(type_def.name.clone()),
573 description: type_def.description.clone(),
574 fields: Some(fields),
575 interfaces: Some(interfaces),
576 possible_types: None,
577 enum_values: None,
578 input_fields: None,
579 of_type: None,
580 specified_by_u_r_l: None,
581 }
582 }
583
584 fn build_enum_type(enum_def: &EnumDefinition) -> IntrospectionType {
586 let enum_values = enum_def
587 .values
588 .iter()
589 .map(|v| IntrospectionEnumValue {
590 name: v.name.clone(),
591 description: v.description.clone(),
592 is_deprecated: v.deprecation.is_some(),
593 deprecation_reason: v.deprecation.as_ref().and_then(|d| d.reason.clone()),
594 })
595 .collect();
596
597 IntrospectionType {
598 kind: TypeKind::Enum,
599 name: Some(enum_def.name.clone()),
600 description: enum_def.description.clone(),
601 fields: None,
602 interfaces: None,
603 possible_types: None,
604 enum_values: Some(enum_values),
605 input_fields: None,
606 of_type: None,
607 specified_by_u_r_l: None,
608 }
609 }
610
611 fn build_input_object_type(input_def: &InputObjectDefinition) -> IntrospectionType {
613 let input_fields = input_def
614 .fields
615 .iter()
616 .map(|f| {
617 let validation_rules = f
618 .validation_rules
619 .iter()
620 .map(|rule| Self::build_validation_rule(rule))
621 .collect();
622
623 IntrospectionInputValue {
624 name: f.name.clone(),
625 description: f.description.clone(),
626 input_type: Self::type_ref(&f.field_type),
627 default_value: f.default_value.clone(),
628 is_deprecated: f.is_deprecated(),
629 deprecation_reason: f.deprecation.as_ref().and_then(|d| d.reason.clone()),
630 validation_rules,
631 }
632 })
633 .collect();
634
635 IntrospectionType {
636 kind: TypeKind::InputObject,
637 name: Some(input_def.name.clone()),
638 description: input_def.description.clone(),
639 fields: None,
640 interfaces: None,
641 possible_types: None,
642 enum_values: None,
643 input_fields: Some(input_fields),
644 of_type: None,
645 specified_by_u_r_l: None,
646 }
647 }
648
649 fn build_interface_type(
651 interface_def: &InterfaceDefinition,
652 schema: &CompiledSchema,
653 ) -> IntrospectionType {
654 let fields = interface_def.fields.iter().map(|f| Self::build_field(f)).collect();
656
657 let possible_types: Vec<IntrospectionTypeRef> = schema
659 .find_implementors(&interface_def.name)
660 .iter()
661 .map(|t| IntrospectionTypeRef {
662 name: t.name.clone(),
663 })
664 .collect();
665
666 IntrospectionType {
667 kind: TypeKind::Interface,
668 name: Some(interface_def.name.clone()),
669 description: interface_def.description.clone(),
670 fields: Some(fields),
671 interfaces: None,
672 possible_types: if possible_types.is_empty() {
673 None
674 } else {
675 Some(possible_types)
676 },
677 enum_values: None,
678 input_fields: None,
679 of_type: None,
680 specified_by_u_r_l: None,
681 }
682 }
683
684 fn build_union_type(union_def: &UnionDefinition) -> IntrospectionType {
686 let possible_types: Vec<IntrospectionTypeRef> = union_def
688 .member_types
689 .iter()
690 .map(|name| IntrospectionTypeRef { name: name.clone() })
691 .collect();
692
693 IntrospectionType {
694 kind: TypeKind::Union,
695 name: Some(union_def.name.clone()),
696 description: union_def.description.clone(),
697 fields: None, interfaces: None,
699 possible_types: if possible_types.is_empty() {
700 None
701 } else {
702 Some(possible_types)
703 },
704 enum_values: None,
705 input_fields: None,
706 of_type: None,
707 specified_by_u_r_l: None,
708 }
709 }
710
711 fn build_field(field: &FieldDefinition) -> IntrospectionField {
713 IntrospectionField {
714 name: field.output_name().to_string(),
715 description: field.description.clone(),
716 args: vec![], field_type: Self::field_type_to_introspection(
718 &field.field_type,
719 field.nullable,
720 ),
721 is_deprecated: field.is_deprecated(),
722 deprecation_reason: field.deprecation_reason().map(ToString::to_string),
723 }
724 }
725
726 fn field_type_to_introspection(field_type: &FieldType, nullable: bool) -> IntrospectionType {
728 let inner = match field_type {
729 FieldType::Int => Self::type_ref("Int"),
730 FieldType::Float => Self::type_ref("Float"),
731 FieldType::String => Self::type_ref("String"),
732 FieldType::Boolean => Self::type_ref("Boolean"),
733 FieldType::Id => Self::type_ref("ID"),
734 FieldType::DateTime => Self::type_ref("DateTime"),
735 FieldType::Date => Self::type_ref("Date"),
736 FieldType::Time => Self::type_ref("Time"),
737 FieldType::Uuid => Self::type_ref("UUID"),
738 FieldType::Json => Self::type_ref("JSON"),
739 FieldType::Decimal => Self::type_ref("Decimal"),
740 FieldType::Object(name) => Self::type_ref(name),
741 FieldType::Enum(name) => Self::type_ref(name),
742 FieldType::Input(name) => Self::type_ref(name),
743 FieldType::Interface(name) => Self::type_ref(name),
744 FieldType::Union(name) => Self::type_ref(name),
745 FieldType::Scalar(name) => Self::type_ref(name), FieldType::List(inner) => IntrospectionType {
747 kind: TypeKind::List,
748 name: None,
749 description: None,
750 fields: None,
751 interfaces: None,
752 possible_types: None,
753 enum_values: None,
754 input_fields: None,
755 of_type: Some(Box::new(Self::field_type_to_introspection(inner, true))),
756 specified_by_u_r_l: None,
757 },
758 FieldType::Vector => Self::type_ref("JSON"), };
760
761 if nullable {
762 inner
763 } else {
764 IntrospectionType {
766 kind: TypeKind::NonNull,
767 name: None,
768 description: None,
769 fields: None,
770 interfaces: None,
771 possible_types: None,
772 enum_values: None,
773 input_fields: None,
774 of_type: Some(Box::new(inner)),
775 specified_by_u_r_l: None,
776 }
777 }
778 }
779
780 fn type_ref(name: &str) -> IntrospectionType {
782 IntrospectionType {
783 kind: TypeKind::Scalar, name: Some(name.to_string()),
785 description: None,
786 fields: None,
787 interfaces: None,
788 possible_types: None,
789 enum_values: None,
790 input_fields: None,
791 of_type: None,
792 specified_by_u_r_l: None,
793 }
794 }
795
796 fn build_validation_rule(
798 rule: &crate::validation::rules::ValidationRule,
799 ) -> IntrospectionValidationRule {
800 use crate::validation::rules::ValidationRule;
801
802 match rule {
803 ValidationRule::Required => IntrospectionValidationRule {
804 rule_type: "required".to_string(),
805 pattern: None,
806 pattern_message: None,
807 min: None,
808 max: None,
809 allowed_values: None,
810 algorithm: None,
811 field_reference: None,
812 operator: None,
813 field_list: None,
814 description: Some("Field is required".to_string()),
815 },
816 ValidationRule::Pattern { pattern, message } => IntrospectionValidationRule {
817 rule_type: "pattern".to_string(),
818 pattern: Some(pattern.clone()),
819 pattern_message: message.clone(),
820 min: None,
821 max: None,
822 allowed_values: None,
823 algorithm: None,
824 field_reference: None,
825 operator: None,
826 field_list: None,
827 description: message.clone(),
828 },
829 ValidationRule::Length { min, max } => IntrospectionValidationRule {
830 rule_type: "length".to_string(),
831 pattern: None,
832 pattern_message: None,
833 min: min.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
834 max: max.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
835 allowed_values: None,
836 algorithm: None,
837 field_reference: None,
838 operator: None,
839 field_list: None,
840 description: None,
841 },
842 ValidationRule::Range { min, max } => IntrospectionValidationRule {
843 rule_type: "range".to_string(),
844 pattern: None,
845 pattern_message: None,
846 min: *min,
847 max: *max,
848 allowed_values: None,
849 algorithm: None,
850 field_reference: None,
851 operator: None,
852 field_list: None,
853 description: None,
854 },
855 ValidationRule::Enum { values } => IntrospectionValidationRule {
856 rule_type: "enum".to_string(),
857 pattern: None,
858 pattern_message: None,
859 min: None,
860 max: None,
861 allowed_values: Some(values.clone()),
862 algorithm: None,
863 field_reference: None,
864 operator: None,
865 field_list: None,
866 description: None,
867 },
868 ValidationRule::Checksum { algorithm } => IntrospectionValidationRule {
869 rule_type: "checksum".to_string(),
870 pattern: None,
871 pattern_message: None,
872 min: None,
873 max: None,
874 allowed_values: None,
875 algorithm: Some(algorithm.clone()),
876 field_reference: None,
877 operator: None,
878 field_list: None,
879 description: None,
880 },
881 ValidationRule::CrossField { field, operator } => IntrospectionValidationRule {
882 rule_type: "cross_field".to_string(),
883 pattern: None,
884 pattern_message: None,
885 min: None,
886 max: None,
887 allowed_values: None,
888 algorithm: None,
889 field_reference: Some(field.clone()),
890 operator: Some(operator.clone()),
891 field_list: None,
892 description: None,
893 },
894 ValidationRule::Conditional { .. } => IntrospectionValidationRule {
895 rule_type: "conditional".to_string(),
896 pattern: None,
897 pattern_message: None,
898 min: None,
899 max: None,
900 allowed_values: None,
901 algorithm: None,
902 field_reference: None,
903 operator: None,
904 field_list: None,
905 description: Some("Conditional validation".to_string()),
906 },
907 ValidationRule::All(_) => IntrospectionValidationRule {
908 rule_type: "all".to_string(),
909 pattern: None,
910 pattern_message: None,
911 min: None,
912 max: None,
913 allowed_values: None,
914 algorithm: None,
915 field_reference: None,
916 operator: None,
917 field_list: None,
918 description: Some("All rules must pass".to_string()),
919 },
920 ValidationRule::Any(_) => IntrospectionValidationRule {
921 rule_type: "any".to_string(),
922 pattern: None,
923 pattern_message: None,
924 min: None,
925 max: None,
926 allowed_values: None,
927 algorithm: None,
928 field_reference: None,
929 operator: None,
930 field_list: None,
931 description: Some("At least one rule must pass".to_string()),
932 },
933 ValidationRule::OneOf { fields } => IntrospectionValidationRule {
934 rule_type: "one_of".to_string(),
935 pattern: None,
936 pattern_message: None,
937 min: None,
938 max: None,
939 allowed_values: None,
940 algorithm: None,
941 field_reference: None,
942 operator: None,
943 field_list: Some(fields.clone()),
944 description: None,
945 },
946 ValidationRule::AnyOf { fields } => IntrospectionValidationRule {
947 rule_type: "any_of".to_string(),
948 pattern: None,
949 pattern_message: None,
950 min: None,
951 max: None,
952 allowed_values: None,
953 algorithm: None,
954 field_reference: None,
955 operator: None,
956 field_list: Some(fields.clone()),
957 description: None,
958 },
959 ValidationRule::ConditionalRequired { .. } => IntrospectionValidationRule {
960 rule_type: "conditional_required".to_string(),
961 pattern: None,
962 pattern_message: None,
963 min: None,
964 max: None,
965 allowed_values: None,
966 algorithm: None,
967 field_reference: None,
968 operator: None,
969 field_list: None,
970 description: None,
971 },
972 ValidationRule::RequiredIfAbsent { .. } => IntrospectionValidationRule {
973 rule_type: "required_if_absent".to_string(),
974 pattern: None,
975 pattern_message: None,
976 min: None,
977 max: None,
978 allowed_values: None,
979 algorithm: None,
980 field_reference: None,
981 operator: None,
982 field_list: None,
983 description: None,
984 },
985 }
986 }
987
988 fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
990 let fields: Vec<IntrospectionField> =
991 schema.queries.iter().map(|q| Self::build_query_field(q)).collect();
992
993 IntrospectionType {
994 kind: TypeKind::Object,
995 name: Some("Query".to_string()),
996 description: Some("Root query type".to_string()),
997 fields: Some(fields),
998 interfaces: Some(vec![]),
999 possible_types: None,
1000 enum_values: None,
1001 input_fields: None,
1002 of_type: None,
1003 specified_by_u_r_l: None,
1004 }
1005 }
1006
1007 fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
1009 let fields: Vec<IntrospectionField> =
1010 schema.mutations.iter().map(|m| Self::build_mutation_field(m)).collect();
1011
1012 IntrospectionType {
1013 kind: TypeKind::Object,
1014 name: Some("Mutation".to_string()),
1015 description: Some("Root mutation type".to_string()),
1016 fields: Some(fields),
1017 interfaces: Some(vec![]),
1018 possible_types: None,
1019 enum_values: None,
1020 input_fields: None,
1021 of_type: None,
1022 specified_by_u_r_l: None,
1023 }
1024 }
1025
1026 fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
1028 let fields: Vec<IntrospectionField> =
1029 schema.subscriptions.iter().map(|s| Self::build_subscription_field(s)).collect();
1030
1031 IntrospectionType {
1032 kind: TypeKind::Object,
1033 name: Some("Subscription".to_string()),
1034 description: Some("Root subscription type".to_string()),
1035 fields: Some(fields),
1036 interfaces: Some(vec![]),
1037 possible_types: None,
1038 enum_values: None,
1039 input_fields: None,
1040 of_type: None,
1041 specified_by_u_r_l: None,
1042 }
1043 }
1044
1045 fn build_query_field(query: &QueryDefinition) -> IntrospectionField {
1047 let return_type = Self::type_ref(&query.return_type);
1048 let return_type = if query.returns_list {
1049 IntrospectionType {
1050 kind: TypeKind::List,
1051 name: None,
1052 description: None,
1053 fields: None,
1054 interfaces: None,
1055 possible_types: None,
1056 enum_values: None,
1057 input_fields: None,
1058 of_type: Some(Box::new(return_type)),
1059 specified_by_u_r_l: None,
1060 }
1061 } else {
1062 return_type
1063 };
1064
1065 let return_type = if query.nullable {
1066 return_type
1067 } else {
1068 IntrospectionType {
1069 kind: TypeKind::NonNull,
1070 name: None,
1071 description: None,
1072 fields: None,
1073 interfaces: None,
1074 possible_types: None,
1075 enum_values: None,
1076 input_fields: None,
1077 of_type: Some(Box::new(return_type)),
1078 specified_by_u_r_l: None,
1079 }
1080 };
1081
1082 let args: Vec<IntrospectionInputValue> = query
1084 .arguments
1085 .iter()
1086 .map(|arg| IntrospectionInputValue {
1087 name: arg.name.clone(),
1088 description: arg.description.clone(),
1089 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1090 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
1091 is_deprecated: arg.is_deprecated(),
1092 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1093 validation_rules: vec![],
1094 })
1095 .collect();
1096
1097 IntrospectionField {
1098 name: query.name.clone(),
1099 description: query.description.clone(),
1100 args,
1101 field_type: return_type,
1102 is_deprecated: query.is_deprecated(),
1103 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
1104 }
1105 }
1106
1107 fn build_mutation_field(mutation: &super::MutationDefinition) -> IntrospectionField {
1109 let return_type = Self::type_ref(&mutation.return_type);
1111
1112 let args: Vec<IntrospectionInputValue> = mutation
1114 .arguments
1115 .iter()
1116 .map(|arg| IntrospectionInputValue {
1117 name: arg.name.clone(),
1118 description: arg.description.clone(),
1119 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1120 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
1121 is_deprecated: arg.is_deprecated(),
1122 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1123 validation_rules: vec![],
1124 })
1125 .collect();
1126
1127 IntrospectionField {
1128 name: mutation.name.clone(),
1129 description: mutation.description.clone(),
1130 args,
1131 field_type: return_type,
1132 is_deprecated: mutation.is_deprecated(),
1133 deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
1134 }
1135 }
1136
1137 fn build_subscription_field(
1139 subscription: &super::SubscriptionDefinition,
1140 ) -> IntrospectionField {
1141 let return_type = Self::type_ref(&subscription.return_type);
1143
1144 let args: Vec<IntrospectionInputValue> = subscription
1146 .arguments
1147 .iter()
1148 .map(|arg| IntrospectionInputValue {
1149 name: arg.name.clone(),
1150 description: arg.description.clone(),
1151 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1152 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
1153 is_deprecated: arg.is_deprecated(),
1154 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1155 validation_rules: vec![],
1156 })
1157 .collect();
1158
1159 IntrospectionField {
1160 name: subscription.name.clone(),
1161 description: subscription.description.clone(),
1162 args,
1163 field_type: return_type,
1164 is_deprecated: subscription.is_deprecated(),
1165 deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
1166 }
1167 }
1168
1169 fn builtin_directives() -> Vec<IntrospectionDirective> {
1171 vec![
1172 IntrospectionDirective {
1173 name: "skip".to_string(),
1174 description: Some(
1175 "Directs the executor to skip this field or fragment when the `if` argument is true."
1176 .to_string(),
1177 ),
1178 locations: vec![
1179 DirectiveLocation::Field,
1180 DirectiveLocation::FragmentSpread,
1181 DirectiveLocation::InlineFragment,
1182 ],
1183 args: vec![IntrospectionInputValue {
1184 name: "if".to_string(),
1185 description: Some("Skipped when true.".to_string()),
1186 input_type: IntrospectionType {
1187 kind: TypeKind::NonNull,
1188 name: None,
1189 description: None,
1190 fields: None,
1191 interfaces: None,
1192 possible_types: None,
1193 enum_values: None,
1194 input_fields: None,
1195 of_type: Some(Box::new(Self::type_ref("Boolean"))),
1196 specified_by_u_r_l: None,
1197 },
1198 default_value: None,
1199 is_deprecated: false,
1200 deprecation_reason: None,
1201 validation_rules: vec![],
1202 }],
1203 is_repeatable: false,
1204 },
1205 IntrospectionDirective {
1206 name: "include".to_string(),
1207 description: Some(
1208 "Directs the executor to include this field or fragment only when the `if` argument is true."
1209 .to_string(),
1210 ),
1211 locations: vec![
1212 DirectiveLocation::Field,
1213 DirectiveLocation::FragmentSpread,
1214 DirectiveLocation::InlineFragment,
1215 ],
1216 args: vec![IntrospectionInputValue {
1217 name: "if".to_string(),
1218 description: Some("Included when true.".to_string()),
1219 input_type: IntrospectionType {
1220 kind: TypeKind::NonNull,
1221 name: None,
1222 description: None,
1223 fields: None,
1224 interfaces: None,
1225 possible_types: None,
1226 enum_values: None,
1227 input_fields: None,
1228 of_type: Some(Box::new(Self::type_ref("Boolean"))),
1229 specified_by_u_r_l: None,
1230 },
1231 default_value: None,
1232 is_deprecated: false,
1233 deprecation_reason: None,
1234 validation_rules: vec![],
1235 }],
1236 is_repeatable: false,
1237 },
1238 IntrospectionDirective {
1239 name: "deprecated".to_string(),
1240 description: Some(
1241 "Marks an element of a GraphQL schema as no longer supported.".to_string(),
1242 ),
1243 locations: vec![
1244 DirectiveLocation::FieldDefinition,
1245 DirectiveLocation::EnumValue,
1246 DirectiveLocation::ArgumentDefinition,
1247 DirectiveLocation::InputFieldDefinition,
1248 ],
1249 args: vec![IntrospectionInputValue {
1250 name: "reason".to_string(),
1251 description: Some(
1252 "Explains why this element was deprecated.".to_string(),
1253 ),
1254 input_type: Self::type_ref("String"),
1255 default_value: Some("\"No longer supported\"".to_string()),
1256 is_deprecated: false,
1257 deprecation_reason: None,
1258 validation_rules: vec![],
1259 }],
1260 is_repeatable: false,
1261 },
1262 ]
1263 }
1264
1265 fn build_custom_directives(directives: &[DirectiveDefinition]) -> Vec<IntrospectionDirective> {
1267 directives.iter().map(|d| Self::build_custom_directive(d)).collect()
1268 }
1269
1270 fn build_custom_directive(directive: &DirectiveDefinition) -> IntrospectionDirective {
1272 let locations: Vec<DirectiveLocation> =
1273 directive.locations.iter().map(|loc| DirectiveLocation::from(*loc)).collect();
1274
1275 let args: Vec<IntrospectionInputValue> = directive
1276 .arguments
1277 .iter()
1278 .map(|arg| IntrospectionInputValue {
1279 name: arg.name.clone(),
1280 description: arg.description.clone(),
1281 input_type: Self::field_type_to_introspection(&arg.arg_type, arg.nullable),
1282 default_value: arg.default_value.as_ref().map(|v| v.to_string()),
1283 is_deprecated: arg.is_deprecated(),
1284 deprecation_reason: arg.deprecation_reason().map(ToString::to_string),
1285 validation_rules: vec![],
1286 })
1287 .collect();
1288
1289 IntrospectionDirective {
1290 name: directive.name.clone(),
1291 description: directive.description.clone(),
1292 locations,
1293 args,
1294 is_repeatable: directive.is_repeatable,
1295 }
1296 }
1297}
1298
1299#[derive(Debug, Clone)]
1305pub struct IntrospectionResponses {
1306 pub schema_response: String,
1308 pub type_responses: HashMap<String, String>,
1310}
1311
1312impl IntrospectionResponses {
1313 #[must_use]
1317 pub fn build(schema: &CompiledSchema) -> Self {
1318 let introspection = IntrospectionBuilder::build(schema);
1319 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1320
1321 let schema_response = serde_json::json!({
1323 "data": {
1324 "__schema": introspection
1325 }
1326 })
1327 .to_string();
1328
1329 let mut type_responses = HashMap::new();
1331 for (name, t) in type_map {
1332 let response = serde_json::json!({
1333 "data": {
1334 "__type": t
1335 }
1336 })
1337 .to_string();
1338 type_responses.insert(name, response);
1339 }
1340
1341 Self {
1342 schema_response,
1343 type_responses,
1344 }
1345 }
1346
1347 #[must_use]
1349 pub fn get_type_response(&self, type_name: &str) -> String {
1350 self.type_responses.get(type_name).cloned().unwrap_or_else(|| {
1351 serde_json::json!({
1352 "data": {
1353 "__type": null
1354 }
1355 })
1356 .to_string()
1357 })
1358 }
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363 use super::*;
1364 use crate::schema::{AutoParams, FieldType};
1365
1366 fn test_schema() -> CompiledSchema {
1367 let mut schema = CompiledSchema::new();
1368
1369 schema.types.push(
1371 TypeDefinition::new("User", "v_user")
1372 .with_field(FieldDefinition::new("id", FieldType::Id))
1373 .with_field(FieldDefinition::new("name", FieldType::String))
1374 .with_field(FieldDefinition::nullable("email", FieldType::String))
1375 .with_description("A user in the system"),
1376 );
1377
1378 schema.queries.push(QueryDefinition {
1380 name: "users".to_string(),
1381 return_type: "User".to_string(),
1382 returns_list: true,
1383 nullable: false,
1384 arguments: vec![],
1385 sql_source: Some("v_user".to_string()),
1386 description: Some("Get all users".to_string()),
1387 auto_params: AutoParams::default(),
1388 deprecation: None,
1389 jsonb_column: "data".to_string(),
1390 });
1391
1392 schema.queries.push(QueryDefinition {
1394 name: "user".to_string(),
1395 return_type: "User".to_string(),
1396 returns_list: false,
1397 nullable: true,
1398 arguments: vec![crate::schema::ArgumentDefinition {
1399 name: "id".to_string(),
1400 arg_type: FieldType::Id,
1401 nullable: false, default_value: None,
1403 description: Some("User ID".to_string()),
1404 deprecation: None,
1405 }],
1406 sql_source: Some("v_user".to_string()),
1407 description: Some("Get user by ID".to_string()),
1408 auto_params: AutoParams::default(),
1409 deprecation: None,
1410 jsonb_column: "data".to_string(),
1411 });
1412
1413 schema
1414 }
1415
1416 #[test]
1417 fn test_build_introspection_schema() {
1418 let schema = test_schema();
1419 let introspection = IntrospectionBuilder::build(&schema);
1420
1421 assert_eq!(introspection.query_type.name, "Query");
1423
1424 assert!(introspection.mutation_type.is_none());
1426
1427 let scalar_names: Vec<_> = introspection
1429 .types
1430 .iter()
1431 .filter(|t| t.kind == TypeKind::Scalar)
1432 .filter_map(|t| t.name.as_ref())
1433 .collect();
1434 assert!(scalar_names.contains(&&"Int".to_string()));
1435 assert!(scalar_names.contains(&&"String".to_string()));
1436 assert!(scalar_names.contains(&&"Boolean".to_string()));
1437
1438 let user_type = introspection
1440 .types
1441 .iter()
1442 .find(|t| t.name.as_ref() == Some(&"User".to_string()));
1443 assert!(user_type.is_some());
1444 let user_type = user_type.unwrap();
1445 assert_eq!(user_type.kind, TypeKind::Object);
1446 assert!(user_type.fields.is_some());
1447 assert_eq!(user_type.fields.as_ref().unwrap().len(), 3);
1448 }
1449
1450 #[test]
1451 fn test_build_introspection_responses() {
1452 let schema = test_schema();
1453 let responses = IntrospectionResponses::build(&schema);
1454
1455 assert!(responses.schema_response.contains("__schema"));
1457 assert!(responses.schema_response.contains("Query"));
1458
1459 assert!(responses.type_responses.contains_key("User"));
1461 assert!(responses.type_responses.contains_key("Query"));
1462 assert!(responses.type_responses.contains_key("Int"));
1463
1464 let unknown = responses.get_type_response("Unknown");
1466 assert!(unknown.contains("null"));
1467 }
1468
1469 #[test]
1470 fn test_query_field_introspection() {
1471 let schema = test_schema();
1472 let introspection = IntrospectionBuilder::build(&schema);
1473
1474 let query_type = introspection
1475 .types
1476 .iter()
1477 .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
1478 .unwrap();
1479
1480 let fields = query_type.fields.as_ref().unwrap();
1481
1482 let users_field = fields.iter().find(|f| f.name == "users").unwrap();
1484 assert_eq!(users_field.field_type.kind, TypeKind::NonNull);
1485 assert!(users_field.args.is_empty());
1486
1487 let user_field = fields.iter().find(|f| f.name == "user").unwrap();
1489 assert!(!user_field.args.is_empty());
1490 assert_eq!(user_field.args[0].name, "id");
1491 }
1492
1493 #[test]
1494 fn test_field_type_non_null() {
1495 let schema = test_schema();
1496 let introspection = IntrospectionBuilder::build(&schema);
1497
1498 let user_type = introspection
1499 .types
1500 .iter()
1501 .find(|t| t.name.as_ref() == Some(&"User".to_string()))
1502 .unwrap();
1503
1504 let fields = user_type.fields.as_ref().unwrap();
1505
1506 let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1508 assert_eq!(id_field.field_type.kind, TypeKind::NonNull);
1509
1510 let email_field = fields.iter().find(|f| f.name == "email").unwrap();
1512 assert_ne!(email_field.field_type.kind, TypeKind::NonNull);
1513 }
1514
1515 #[test]
1516 fn test_deprecated_field_introspection() {
1517 use crate::schema::DeprecationInfo;
1518
1519 let mut schema = CompiledSchema::new();
1521 schema.types.push(TypeDefinition {
1522 name: "Product".to_string(),
1523 sql_source: "products".to_string(),
1524 jsonb_column: "data".to_string(),
1525 description: None,
1526 sql_projection_hint: None,
1527 implements: vec![],
1528 is_error: false,
1529 fields: vec![
1530 FieldDefinition::new("id", FieldType::Id),
1531 FieldDefinition {
1532 name: "oldSku".to_string(),
1533 field_type: FieldType::String,
1534 nullable: false,
1535 description: Some("Legacy SKU field".to_string()),
1536 default_value: None,
1537 vector_config: None,
1538 alias: None,
1539 deprecation: Some(DeprecationInfo {
1540 reason: Some("Use 'sku' instead".to_string()),
1541 }),
1542 requires_scope: None,
1543 encryption: None,
1544 },
1545 FieldDefinition::new("sku", FieldType::String),
1546 ],
1547 });
1548
1549 let introspection = IntrospectionBuilder::build(&schema);
1550
1551 let product_type = introspection
1553 .types
1554 .iter()
1555 .find(|t| t.name.as_ref() == Some(&"Product".to_string()))
1556 .unwrap();
1557
1558 let fields = product_type.fields.as_ref().unwrap();
1559
1560 let old_sku_field = fields.iter().find(|f| f.name == "oldSku").unwrap();
1562 assert!(old_sku_field.is_deprecated);
1563 assert_eq!(old_sku_field.deprecation_reason, Some("Use 'sku' instead".to_string()));
1564
1565 let sku_field = fields.iter().find(|f| f.name == "sku").unwrap();
1567 assert!(!sku_field.is_deprecated);
1568 assert!(sku_field.deprecation_reason.is_none());
1569
1570 let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1572 assert!(!id_field.is_deprecated);
1573 assert!(id_field.deprecation_reason.is_none());
1574 }
1575
1576 #[test]
1577 fn test_enum_type_introspection() {
1578 use crate::schema::{EnumDefinition, EnumValueDefinition};
1579
1580 let mut schema = CompiledSchema::new();
1581
1582 schema.enums.push(EnumDefinition {
1584 name: "OrderStatus".to_string(),
1585 description: Some("Status of an order".to_string()),
1586 values: vec![
1587 EnumValueDefinition {
1588 name: "PENDING".to_string(),
1589 description: Some("Order is pending".to_string()),
1590 deprecation: None,
1591 },
1592 EnumValueDefinition {
1593 name: "PROCESSING".to_string(),
1594 description: None,
1595 deprecation: None,
1596 },
1597 EnumValueDefinition {
1598 name: "SHIPPED".to_string(),
1599 description: None,
1600 deprecation: None,
1601 },
1602 EnumValueDefinition {
1603 name: "CANCELLED".to_string(),
1604 description: Some("Order was cancelled".to_string()),
1605 deprecation: Some(crate::schema::DeprecationInfo {
1606 reason: Some("Use REFUNDED instead".to_string()),
1607 }),
1608 },
1609 ],
1610 });
1611
1612 let introspection = IntrospectionBuilder::build(&schema);
1613
1614 let order_status = introspection
1616 .types
1617 .iter()
1618 .find(|t| t.name.as_ref() == Some(&"OrderStatus".to_string()))
1619 .unwrap();
1620
1621 assert_eq!(order_status.kind, TypeKind::Enum);
1622 assert_eq!(order_status.description, Some("Status of an order".to_string()));
1623
1624 let enum_values = order_status.enum_values.as_ref().unwrap();
1626 assert_eq!(enum_values.len(), 4);
1627
1628 let pending = enum_values.iter().find(|v| v.name == "PENDING").unwrap();
1630 assert_eq!(pending.description, Some("Order is pending".to_string()));
1631 assert!(!pending.is_deprecated);
1632 assert!(pending.deprecation_reason.is_none());
1633
1634 let cancelled = enum_values.iter().find(|v| v.name == "CANCELLED").unwrap();
1636 assert!(cancelled.is_deprecated);
1637 assert_eq!(cancelled.deprecation_reason, Some("Use REFUNDED instead".to_string()));
1638
1639 assert!(order_status.fields.is_none());
1641 }
1642
1643 #[test]
1644 fn test_input_object_introspection() {
1645 use crate::schema::{InputFieldDefinition, InputObjectDefinition};
1646
1647 let mut schema = CompiledSchema::new();
1648
1649 schema.input_types.push(InputObjectDefinition {
1651 name: "UserFilter".to_string(),
1652 description: Some("Filter for user queries".to_string()),
1653 fields: vec![
1654 InputFieldDefinition {
1655 name: "name".to_string(),
1656 field_type: "String".to_string(),
1657 description: Some("Filter by name".to_string()),
1658 default_value: None,
1659 deprecation: None,
1660 validation_rules: Vec::new(),
1661 },
1662 InputFieldDefinition {
1663 name: "email".to_string(),
1664 field_type: "String".to_string(),
1665 description: None,
1666 default_value: None,
1667 deprecation: None,
1668 validation_rules: Vec::new(),
1669 },
1670 InputFieldDefinition {
1671 name: "limit".to_string(),
1672 field_type: "Int".to_string(),
1673 description: Some("Max results".to_string()),
1674 default_value: Some("10".to_string()),
1675 deprecation: None,
1676 validation_rules: Vec::new(),
1677 },
1678 ],
1679 metadata: None,
1680 });
1681
1682 let introspection = IntrospectionBuilder::build(&schema);
1683
1684 let user_filter = introspection
1686 .types
1687 .iter()
1688 .find(|t| t.name.as_ref() == Some(&"UserFilter".to_string()))
1689 .unwrap();
1690
1691 assert_eq!(user_filter.kind, TypeKind::InputObject);
1692 assert_eq!(user_filter.description, Some("Filter for user queries".to_string()));
1693
1694 let input_fields = user_filter.input_fields.as_ref().unwrap();
1696 assert_eq!(input_fields.len(), 3);
1697
1698 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1700 assert_eq!(name_field.description, Some("Filter by name".to_string()));
1701 assert!(name_field.default_value.is_none());
1702
1703 let limit_field = input_fields.iter().find(|f| f.name == "limit").unwrap();
1705 assert_eq!(limit_field.description, Some("Max results".to_string()));
1706 assert_eq!(limit_field.default_value, Some("10".to_string()));
1707
1708 assert!(user_filter.fields.is_none());
1710 }
1711
1712 #[test]
1713 fn test_enum_in_type_map() {
1714 use crate::schema::EnumDefinition;
1715
1716 let mut schema = CompiledSchema::new();
1717 schema.enums.push(EnumDefinition {
1718 name: "Status".to_string(),
1719 description: None,
1720 values: vec![],
1721 });
1722
1723 let introspection = IntrospectionBuilder::build(&schema);
1724 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1725
1726 assert!(type_map.contains_key("Status"));
1728 let status = type_map.get("Status").unwrap();
1729 assert_eq!(status.kind, TypeKind::Enum);
1730 }
1731
1732 #[test]
1733 fn test_input_object_in_type_map() {
1734 use crate::schema::InputObjectDefinition;
1735
1736 let mut schema = CompiledSchema::new();
1737 schema.input_types.push(InputObjectDefinition {
1738 name: "CreateUserInput".to_string(),
1739 description: None,
1740 fields: vec![],
1741 metadata: None,
1742 });
1743
1744 let introspection = IntrospectionBuilder::build(&schema);
1745 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1746
1747 assert!(type_map.contains_key("CreateUserInput"));
1749 let input = type_map.get("CreateUserInput").unwrap();
1750 assert_eq!(input.kind, TypeKind::InputObject);
1751 }
1752
1753 #[test]
1754 fn test_interface_introspection() {
1755 use crate::schema::InterfaceDefinition;
1756
1757 let mut schema = CompiledSchema::new();
1758
1759 schema.interfaces.push(InterfaceDefinition {
1761 name: "Node".to_string(),
1762 description: Some("An object with a globally unique ID".to_string()),
1763 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1764 });
1765
1766 schema.types.push(TypeDefinition {
1768 name: "User".to_string(),
1769 sql_source: "users".to_string(),
1770 jsonb_column: "data".to_string(),
1771 description: Some("A user".to_string()),
1772 sql_projection_hint: None,
1773 implements: vec!["Node".to_string()],
1774 is_error: false,
1775 fields: vec![
1776 FieldDefinition::new("id", FieldType::Id),
1777 FieldDefinition::new("name", FieldType::String),
1778 ],
1779 });
1780
1781 schema.types.push(TypeDefinition {
1782 name: "Post".to_string(),
1783 sql_source: "posts".to_string(),
1784 jsonb_column: "data".to_string(),
1785 description: Some("A blog post".to_string()),
1786 sql_projection_hint: None,
1787 implements: vec!["Node".to_string()],
1788 is_error: false,
1789 fields: vec![
1790 FieldDefinition::new("id", FieldType::Id),
1791 FieldDefinition::new("title", FieldType::String),
1792 ],
1793 });
1794
1795 let introspection = IntrospectionBuilder::build(&schema);
1796
1797 let node = introspection
1799 .types
1800 .iter()
1801 .find(|t| t.name.as_ref() == Some(&"Node".to_string()))
1802 .unwrap();
1803
1804 assert_eq!(node.kind, TypeKind::Interface);
1805 assert_eq!(node.description, Some("An object with a globally unique ID".to_string()));
1806
1807 let fields = node.fields.as_ref().unwrap();
1809 assert_eq!(fields.len(), 1);
1810 assert_eq!(fields[0].name, "id");
1811
1812 let possible_types = node.possible_types.as_ref().unwrap();
1814 assert_eq!(possible_types.len(), 2);
1815 assert!(possible_types.iter().any(|t| t.name == "User"));
1816 assert!(possible_types.iter().any(|t| t.name == "Post"));
1817
1818 assert!(node.enum_values.is_none());
1820 assert!(node.input_fields.is_none());
1821 }
1822
1823 #[test]
1824 fn test_type_implements_interface() {
1825 use crate::schema::InterfaceDefinition;
1826
1827 let mut schema = CompiledSchema::new();
1828
1829 schema.interfaces.push(InterfaceDefinition {
1831 name: "Node".to_string(),
1832 description: None,
1833 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1834 });
1835
1836 schema.interfaces.push(InterfaceDefinition {
1837 name: "Timestamped".to_string(),
1838 description: None,
1839 fields: vec![FieldDefinition::new("createdAt", FieldType::DateTime)],
1840 });
1841
1842 schema.types.push(TypeDefinition {
1844 name: "Comment".to_string(),
1845 sql_source: "comments".to_string(),
1846 jsonb_column: "data".to_string(),
1847 description: None,
1848 sql_projection_hint: None,
1849 implements: vec!["Node".to_string(), "Timestamped".to_string()],
1850 is_error: false,
1851 fields: vec![
1852 FieldDefinition::new("id", FieldType::Id),
1853 FieldDefinition::new("createdAt", FieldType::DateTime),
1854 FieldDefinition::new("text", FieldType::String),
1855 ],
1856 });
1857
1858 let introspection = IntrospectionBuilder::build(&schema);
1859
1860 let comment = introspection
1862 .types
1863 .iter()
1864 .find(|t| t.name.as_ref() == Some(&"Comment".to_string()))
1865 .unwrap();
1866
1867 assert_eq!(comment.kind, TypeKind::Object);
1868
1869 let interfaces = comment.interfaces.as_ref().unwrap();
1871 assert_eq!(interfaces.len(), 2);
1872 assert!(interfaces.iter().any(|i| i.name == "Node"));
1873 assert!(interfaces.iter().any(|i| i.name == "Timestamped"));
1874 }
1875
1876 #[test]
1877 fn test_interface_in_type_map() {
1878 use crate::schema::InterfaceDefinition;
1879
1880 let mut schema = CompiledSchema::new();
1881 schema.interfaces.push(InterfaceDefinition {
1882 name: "Searchable".to_string(),
1883 description: None,
1884 fields: vec![],
1885 });
1886
1887 let introspection = IntrospectionBuilder::build(&schema);
1888 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1889
1890 assert!(type_map.contains_key("Searchable"));
1892 let interface = type_map.get("Searchable").unwrap();
1893 assert_eq!(interface.kind, TypeKind::Interface);
1894 }
1895
1896 #[test]
1897 fn test_filter_deprecated_fields() {
1898 let introspection_type = IntrospectionType {
1900 kind: TypeKind::Object,
1901 name: Some("TestType".to_string()),
1902 description: None,
1903 fields: Some(vec![
1904 IntrospectionField {
1905 name: "id".to_string(),
1906 description: None,
1907 args: vec![],
1908 field_type: IntrospectionBuilder::type_ref("ID"),
1909 is_deprecated: false,
1910 deprecation_reason: None,
1911 },
1912 IntrospectionField {
1913 name: "oldField".to_string(),
1914 description: None,
1915 args: vec![],
1916 field_type: IntrospectionBuilder::type_ref("String"),
1917 is_deprecated: true,
1918 deprecation_reason: Some("Use newField instead".to_string()),
1919 },
1920 IntrospectionField {
1921 name: "newField".to_string(),
1922 description: None,
1923 args: vec![],
1924 field_type: IntrospectionBuilder::type_ref("String"),
1925 is_deprecated: false,
1926 deprecation_reason: None,
1927 },
1928 ]),
1929 interfaces: None,
1930 possible_types: None,
1931 enum_values: None,
1932 input_fields: None,
1933 of_type: None,
1934 specified_by_u_r_l: None,
1935 };
1936
1937 let filtered = introspection_type.filter_deprecated_fields(false);
1939 let fields = filtered.fields.as_ref().unwrap();
1940 assert_eq!(fields.len(), 2);
1941 assert!(fields.iter().any(|f| f.name == "id"));
1942 assert!(fields.iter().any(|f| f.name == "newField"));
1943 assert!(!fields.iter().any(|f| f.name == "oldField"));
1944
1945 let unfiltered = introspection_type.filter_deprecated_fields(true);
1947 let fields = unfiltered.fields.as_ref().unwrap();
1948 assert_eq!(fields.len(), 3);
1949 }
1950
1951 #[test]
1952 fn test_filter_deprecated_enum_values() {
1953 let introspection_type = IntrospectionType {
1955 kind: TypeKind::Enum,
1956 name: Some("Status".to_string()),
1957 description: None,
1958 fields: None,
1959 interfaces: None,
1960 possible_types: None,
1961 enum_values: Some(vec![
1962 IntrospectionEnumValue {
1963 name: "ACTIVE".to_string(),
1964 description: None,
1965 is_deprecated: false,
1966 deprecation_reason: None,
1967 },
1968 IntrospectionEnumValue {
1969 name: "INACTIVE".to_string(),
1970 description: None,
1971 is_deprecated: true,
1972 deprecation_reason: Some("Use DISABLED instead".to_string()),
1973 },
1974 IntrospectionEnumValue {
1975 name: "DISABLED".to_string(),
1976 description: None,
1977 is_deprecated: false,
1978 deprecation_reason: None,
1979 },
1980 ]),
1981 input_fields: None,
1982 of_type: None,
1983 specified_by_u_r_l: None,
1984 };
1985
1986 let filtered = introspection_type.filter_deprecated_enum_values(false);
1988 let values = filtered.enum_values.as_ref().unwrap();
1989 assert_eq!(values.len(), 2);
1990 assert!(values.iter().any(|v| v.name == "ACTIVE"));
1991 assert!(values.iter().any(|v| v.name == "DISABLED"));
1992 assert!(!values.iter().any(|v| v.name == "INACTIVE"));
1993
1994 let unfiltered = introspection_type.filter_deprecated_enum_values(true);
1996 let values = unfiltered.enum_values.as_ref().unwrap();
1997 assert_eq!(values.len(), 3);
1998 }
1999
2000 #[test]
2001 fn test_specified_by_url_for_custom_scalars() {
2002 let schema = CompiledSchema::new();
2003 let introspection = IntrospectionBuilder::build(&schema);
2004
2005 let datetime = introspection
2007 .types
2008 .iter()
2009 .find(|t| t.name.as_ref() == Some(&"DateTime".to_string()))
2010 .unwrap();
2011
2012 assert_eq!(datetime.kind, TypeKind::Scalar);
2013 assert!(datetime.specified_by_u_r_l.is_some());
2014 assert!(datetime.specified_by_u_r_l.as_ref().unwrap().contains("date-time"));
2015
2016 let uuid = introspection
2018 .types
2019 .iter()
2020 .find(|t| t.name.as_ref() == Some(&"UUID".to_string()))
2021 .unwrap();
2022
2023 assert_eq!(uuid.kind, TypeKind::Scalar);
2024 assert!(uuid.specified_by_u_r_l.is_some());
2025 assert!(uuid.specified_by_u_r_l.as_ref().unwrap().contains("rfc4122"));
2026
2027 let int = introspection
2029 .types
2030 .iter()
2031 .find(|t| t.name.as_ref() == Some(&"Int".to_string()))
2032 .unwrap();
2033
2034 assert_eq!(int.kind, TypeKind::Scalar);
2035 assert!(int.specified_by_u_r_l.is_none());
2036 }
2037
2038 #[test]
2039 fn test_deprecated_query_introspection() {
2040 use crate::schema::{ArgumentDefinition, AutoParams, DeprecationInfo};
2041
2042 let mut schema = CompiledSchema::new();
2043
2044 schema.queries.push(QueryDefinition {
2046 name: "oldUsers".to_string(),
2047 return_type: "User".to_string(),
2048 returns_list: true,
2049 nullable: false,
2050 arguments: vec![],
2051 sql_source: Some("v_user".to_string()),
2052 description: Some("Old way to get users".to_string()),
2053 auto_params: AutoParams::default(),
2054 deprecation: Some(DeprecationInfo {
2055 reason: Some("Use 'users' instead".to_string()),
2056 }),
2057 jsonb_column: "data".to_string(),
2058 });
2059
2060 schema.queries.push(QueryDefinition {
2062 name: "users".to_string(),
2063 return_type: "User".to_string(),
2064 returns_list: true,
2065 nullable: false,
2066 arguments: vec![
2067 ArgumentDefinition {
2068 name: "first".to_string(),
2069 arg_type: FieldType::Int,
2070 nullable: true,
2071 default_value: None,
2072 description: Some("Number of results to return".to_string()),
2073 deprecation: None,
2074 },
2075 ArgumentDefinition {
2076 name: "limit".to_string(),
2077 arg_type: FieldType::Int,
2078 nullable: true,
2079 default_value: None,
2080 description: Some("Old parameter for limiting results".to_string()),
2081 deprecation: Some(DeprecationInfo {
2082 reason: Some("Use 'first' instead".to_string()),
2083 }),
2084 },
2085 ],
2086 sql_source: Some("v_user".to_string()),
2087 description: Some("Get users with pagination".to_string()),
2088 auto_params: AutoParams::default(),
2089 deprecation: None,
2090 jsonb_column: "data".to_string(),
2091 });
2092
2093 let introspection = IntrospectionBuilder::build(&schema);
2094
2095 let query_type = introspection
2097 .types
2098 .iter()
2099 .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
2100 .unwrap();
2101
2102 let fields = query_type.fields.as_ref().unwrap();
2103
2104 let old_users = fields.iter().find(|f| f.name == "oldUsers").unwrap();
2106 assert!(old_users.is_deprecated);
2107 assert_eq!(old_users.deprecation_reason, Some("Use 'users' instead".to_string()));
2108
2109 let users = fields.iter().find(|f| f.name == "users").unwrap();
2111 assert!(!users.is_deprecated);
2112 assert!(users.deprecation_reason.is_none());
2113
2114 assert_eq!(users.args.len(), 2);
2116
2117 let first_arg = users.args.iter().find(|a| a.name == "first").unwrap();
2119 assert!(!first_arg.is_deprecated);
2120 assert!(first_arg.deprecation_reason.is_none());
2121
2122 let limit_arg = users.args.iter().find(|a| a.name == "limit").unwrap();
2124 assert!(limit_arg.is_deprecated);
2125 assert_eq!(limit_arg.deprecation_reason, Some("Use 'first' instead".to_string()));
2126 }
2127
2128 #[test]
2129 fn test_deprecated_input_field_introspection() {
2130 use crate::schema::{DeprecationInfo, InputFieldDefinition, InputObjectDefinition};
2131
2132 let mut schema = CompiledSchema::new();
2133
2134 schema.input_types.push(InputObjectDefinition {
2136 name: "CreateUserInput".to_string(),
2137 description: Some("Input for creating a user".to_string()),
2138 fields: vec![
2139 InputFieldDefinition {
2140 name: "name".to_string(),
2141 field_type: "String!".to_string(),
2142 default_value: None,
2143 description: Some("User name".to_string()),
2144 deprecation: None,
2145 validation_rules: Vec::new(),
2146 },
2147 InputFieldDefinition {
2148 name: "oldEmail".to_string(),
2149 field_type: "String".to_string(),
2150 default_value: None,
2151 description: Some("Legacy email field".to_string()),
2152 deprecation: Some(DeprecationInfo {
2153 reason: Some("Use 'email' instead".to_string()),
2154 }),
2155 validation_rules: Vec::new(),
2156 },
2157 ],
2158 metadata: None,
2159 });
2160
2161 let introspection = IntrospectionBuilder::build(&schema);
2162
2163 let create_user_input = introspection
2165 .types
2166 .iter()
2167 .find(|t| t.name.as_ref() == Some(&"CreateUserInput".to_string()))
2168 .unwrap();
2169
2170 let input_fields = create_user_input.input_fields.as_ref().unwrap();
2171
2172 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
2174 assert!(!name_field.is_deprecated);
2175 assert!(name_field.deprecation_reason.is_none());
2176
2177 let old_email = input_fields.iter().find(|f| f.name == "oldEmail").unwrap();
2179 assert!(old_email.is_deprecated);
2180 assert_eq!(old_email.deprecation_reason, Some("Use 'email' instead".to_string()));
2181 }
2182}