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 fields: vec![
1529 FieldDefinition::new("id", FieldType::Id),
1530 FieldDefinition {
1531 name: "oldSku".to_string(),
1532 field_type: FieldType::String,
1533 nullable: false,
1534 description: Some("Legacy SKU field".to_string()),
1535 default_value: None,
1536 vector_config: None,
1537 alias: None,
1538 deprecation: Some(DeprecationInfo {
1539 reason: Some("Use 'sku' instead".to_string()),
1540 }),
1541 requires_scope: None,
1542 encryption: None,
1543 },
1544 FieldDefinition::new("sku", FieldType::String),
1545 ],
1546 });
1547
1548 let introspection = IntrospectionBuilder::build(&schema);
1549
1550 let product_type = introspection
1552 .types
1553 .iter()
1554 .find(|t| t.name.as_ref() == Some(&"Product".to_string()))
1555 .unwrap();
1556
1557 let fields = product_type.fields.as_ref().unwrap();
1558
1559 let old_sku_field = fields.iter().find(|f| f.name == "oldSku").unwrap();
1561 assert!(old_sku_field.is_deprecated);
1562 assert_eq!(old_sku_field.deprecation_reason, Some("Use 'sku' instead".to_string()));
1563
1564 let sku_field = fields.iter().find(|f| f.name == "sku").unwrap();
1566 assert!(!sku_field.is_deprecated);
1567 assert!(sku_field.deprecation_reason.is_none());
1568
1569 let id_field = fields.iter().find(|f| f.name == "id").unwrap();
1571 assert!(!id_field.is_deprecated);
1572 assert!(id_field.deprecation_reason.is_none());
1573 }
1574
1575 #[test]
1576 fn test_enum_type_introspection() {
1577 use crate::schema::{EnumDefinition, EnumValueDefinition};
1578
1579 let mut schema = CompiledSchema::new();
1580
1581 schema.enums.push(EnumDefinition {
1583 name: "OrderStatus".to_string(),
1584 description: Some("Status of an order".to_string()),
1585 values: vec![
1586 EnumValueDefinition {
1587 name: "PENDING".to_string(),
1588 description: Some("Order is pending".to_string()),
1589 deprecation: None,
1590 },
1591 EnumValueDefinition {
1592 name: "PROCESSING".to_string(),
1593 description: None,
1594 deprecation: None,
1595 },
1596 EnumValueDefinition {
1597 name: "SHIPPED".to_string(),
1598 description: None,
1599 deprecation: None,
1600 },
1601 EnumValueDefinition {
1602 name: "CANCELLED".to_string(),
1603 description: Some("Order was cancelled".to_string()),
1604 deprecation: Some(crate::schema::DeprecationInfo {
1605 reason: Some("Use REFUNDED instead".to_string()),
1606 }),
1607 },
1608 ],
1609 });
1610
1611 let introspection = IntrospectionBuilder::build(&schema);
1612
1613 let order_status = introspection
1615 .types
1616 .iter()
1617 .find(|t| t.name.as_ref() == Some(&"OrderStatus".to_string()))
1618 .unwrap();
1619
1620 assert_eq!(order_status.kind, TypeKind::Enum);
1621 assert_eq!(order_status.description, Some("Status of an order".to_string()));
1622
1623 let enum_values = order_status.enum_values.as_ref().unwrap();
1625 assert_eq!(enum_values.len(), 4);
1626
1627 let pending = enum_values.iter().find(|v| v.name == "PENDING").unwrap();
1629 assert_eq!(pending.description, Some("Order is pending".to_string()));
1630 assert!(!pending.is_deprecated);
1631 assert!(pending.deprecation_reason.is_none());
1632
1633 let cancelled = enum_values.iter().find(|v| v.name == "CANCELLED").unwrap();
1635 assert!(cancelled.is_deprecated);
1636 assert_eq!(cancelled.deprecation_reason, Some("Use REFUNDED instead".to_string()));
1637
1638 assert!(order_status.fields.is_none());
1640 }
1641
1642 #[test]
1643 fn test_input_object_introspection() {
1644 use crate::schema::{InputFieldDefinition, InputObjectDefinition};
1645
1646 let mut schema = CompiledSchema::new();
1647
1648 schema.input_types.push(InputObjectDefinition {
1650 name: "UserFilter".to_string(),
1651 description: Some("Filter for user queries".to_string()),
1652 fields: vec![
1653 InputFieldDefinition {
1654 name: "name".to_string(),
1655 field_type: "String".to_string(),
1656 description: Some("Filter by name".to_string()),
1657 default_value: None,
1658 deprecation: None,
1659 validation_rules: Vec::new(),
1660 },
1661 InputFieldDefinition {
1662 name: "email".to_string(),
1663 field_type: "String".to_string(),
1664 description: None,
1665 default_value: None,
1666 deprecation: None,
1667 validation_rules: Vec::new(),
1668 },
1669 InputFieldDefinition {
1670 name: "limit".to_string(),
1671 field_type: "Int".to_string(),
1672 description: Some("Max results".to_string()),
1673 default_value: Some("10".to_string()),
1674 deprecation: None,
1675 validation_rules: Vec::new(),
1676 },
1677 ],
1678 metadata: None,
1679 });
1680
1681 let introspection = IntrospectionBuilder::build(&schema);
1682
1683 let user_filter = introspection
1685 .types
1686 .iter()
1687 .find(|t| t.name.as_ref() == Some(&"UserFilter".to_string()))
1688 .unwrap();
1689
1690 assert_eq!(user_filter.kind, TypeKind::InputObject);
1691 assert_eq!(user_filter.description, Some("Filter for user queries".to_string()));
1692
1693 let input_fields = user_filter.input_fields.as_ref().unwrap();
1695 assert_eq!(input_fields.len(), 3);
1696
1697 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
1699 assert_eq!(name_field.description, Some("Filter by name".to_string()));
1700 assert!(name_field.default_value.is_none());
1701
1702 let limit_field = input_fields.iter().find(|f| f.name == "limit").unwrap();
1704 assert_eq!(limit_field.description, Some("Max results".to_string()));
1705 assert_eq!(limit_field.default_value, Some("10".to_string()));
1706
1707 assert!(user_filter.fields.is_none());
1709 }
1710
1711 #[test]
1712 fn test_enum_in_type_map() {
1713 use crate::schema::EnumDefinition;
1714
1715 let mut schema = CompiledSchema::new();
1716 schema.enums.push(EnumDefinition {
1717 name: "Status".to_string(),
1718 description: None,
1719 values: vec![],
1720 });
1721
1722 let introspection = IntrospectionBuilder::build(&schema);
1723 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1724
1725 assert!(type_map.contains_key("Status"));
1727 let status = type_map.get("Status").unwrap();
1728 assert_eq!(status.kind, TypeKind::Enum);
1729 }
1730
1731 #[test]
1732 fn test_input_object_in_type_map() {
1733 use crate::schema::InputObjectDefinition;
1734
1735 let mut schema = CompiledSchema::new();
1736 schema.input_types.push(InputObjectDefinition {
1737 name: "CreateUserInput".to_string(),
1738 description: None,
1739 fields: vec![],
1740 metadata: None,
1741 });
1742
1743 let introspection = IntrospectionBuilder::build(&schema);
1744 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1745
1746 assert!(type_map.contains_key("CreateUserInput"));
1748 let input = type_map.get("CreateUserInput").unwrap();
1749 assert_eq!(input.kind, TypeKind::InputObject);
1750 }
1751
1752 #[test]
1753 fn test_interface_introspection() {
1754 use crate::schema::InterfaceDefinition;
1755
1756 let mut schema = CompiledSchema::new();
1757
1758 schema.interfaces.push(InterfaceDefinition {
1760 name: "Node".to_string(),
1761 description: Some("An object with a globally unique ID".to_string()),
1762 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1763 });
1764
1765 schema.types.push(TypeDefinition {
1767 name: "User".to_string(),
1768 sql_source: "users".to_string(),
1769 jsonb_column: "data".to_string(),
1770 description: Some("A user".to_string()),
1771 sql_projection_hint: None,
1772 implements: vec!["Node".to_string()],
1773 fields: vec![
1774 FieldDefinition::new("id", FieldType::Id),
1775 FieldDefinition::new("name", FieldType::String),
1776 ],
1777 });
1778
1779 schema.types.push(TypeDefinition {
1780 name: "Post".to_string(),
1781 sql_source: "posts".to_string(),
1782 jsonb_column: "data".to_string(),
1783 description: Some("A blog post".to_string()),
1784 sql_projection_hint: None,
1785 implements: vec!["Node".to_string()],
1786 fields: vec![
1787 FieldDefinition::new("id", FieldType::Id),
1788 FieldDefinition::new("title", FieldType::String),
1789 ],
1790 });
1791
1792 let introspection = IntrospectionBuilder::build(&schema);
1793
1794 let node = introspection
1796 .types
1797 .iter()
1798 .find(|t| t.name.as_ref() == Some(&"Node".to_string()))
1799 .unwrap();
1800
1801 assert_eq!(node.kind, TypeKind::Interface);
1802 assert_eq!(node.description, Some("An object with a globally unique ID".to_string()));
1803
1804 let fields = node.fields.as_ref().unwrap();
1806 assert_eq!(fields.len(), 1);
1807 assert_eq!(fields[0].name, "id");
1808
1809 let possible_types = node.possible_types.as_ref().unwrap();
1811 assert_eq!(possible_types.len(), 2);
1812 assert!(possible_types.iter().any(|t| t.name == "User"));
1813 assert!(possible_types.iter().any(|t| t.name == "Post"));
1814
1815 assert!(node.enum_values.is_none());
1817 assert!(node.input_fields.is_none());
1818 }
1819
1820 #[test]
1821 fn test_type_implements_interface() {
1822 use crate::schema::InterfaceDefinition;
1823
1824 let mut schema = CompiledSchema::new();
1825
1826 schema.interfaces.push(InterfaceDefinition {
1828 name: "Node".to_string(),
1829 description: None,
1830 fields: vec![FieldDefinition::new("id", FieldType::Id)],
1831 });
1832
1833 schema.interfaces.push(InterfaceDefinition {
1834 name: "Timestamped".to_string(),
1835 description: None,
1836 fields: vec![FieldDefinition::new("createdAt", FieldType::DateTime)],
1837 });
1838
1839 schema.types.push(TypeDefinition {
1841 name: "Comment".to_string(),
1842 sql_source: "comments".to_string(),
1843 jsonb_column: "data".to_string(),
1844 description: None,
1845 sql_projection_hint: None,
1846 implements: vec!["Node".to_string(), "Timestamped".to_string()],
1847 fields: vec![
1848 FieldDefinition::new("id", FieldType::Id),
1849 FieldDefinition::new("createdAt", FieldType::DateTime),
1850 FieldDefinition::new("text", FieldType::String),
1851 ],
1852 });
1853
1854 let introspection = IntrospectionBuilder::build(&schema);
1855
1856 let comment = introspection
1858 .types
1859 .iter()
1860 .find(|t| t.name.as_ref() == Some(&"Comment".to_string()))
1861 .unwrap();
1862
1863 assert_eq!(comment.kind, TypeKind::Object);
1864
1865 let interfaces = comment.interfaces.as_ref().unwrap();
1867 assert_eq!(interfaces.len(), 2);
1868 assert!(interfaces.iter().any(|i| i.name == "Node"));
1869 assert!(interfaces.iter().any(|i| i.name == "Timestamped"));
1870 }
1871
1872 #[test]
1873 fn test_interface_in_type_map() {
1874 use crate::schema::InterfaceDefinition;
1875
1876 let mut schema = CompiledSchema::new();
1877 schema.interfaces.push(InterfaceDefinition {
1878 name: "Searchable".to_string(),
1879 description: None,
1880 fields: vec![],
1881 });
1882
1883 let introspection = IntrospectionBuilder::build(&schema);
1884 let type_map = IntrospectionBuilder::build_type_map(&introspection);
1885
1886 assert!(type_map.contains_key("Searchable"));
1888 let interface = type_map.get("Searchable").unwrap();
1889 assert_eq!(interface.kind, TypeKind::Interface);
1890 }
1891
1892 #[test]
1893 fn test_filter_deprecated_fields() {
1894 let introspection_type = IntrospectionType {
1896 kind: TypeKind::Object,
1897 name: Some("TestType".to_string()),
1898 description: None,
1899 fields: Some(vec![
1900 IntrospectionField {
1901 name: "id".to_string(),
1902 description: None,
1903 args: vec![],
1904 field_type: IntrospectionBuilder::type_ref("ID"),
1905 is_deprecated: false,
1906 deprecation_reason: None,
1907 },
1908 IntrospectionField {
1909 name: "oldField".to_string(),
1910 description: None,
1911 args: vec![],
1912 field_type: IntrospectionBuilder::type_ref("String"),
1913 is_deprecated: true,
1914 deprecation_reason: Some("Use newField instead".to_string()),
1915 },
1916 IntrospectionField {
1917 name: "newField".to_string(),
1918 description: None,
1919 args: vec![],
1920 field_type: IntrospectionBuilder::type_ref("String"),
1921 is_deprecated: false,
1922 deprecation_reason: None,
1923 },
1924 ]),
1925 interfaces: None,
1926 possible_types: None,
1927 enum_values: None,
1928 input_fields: None,
1929 of_type: None,
1930 specified_by_u_r_l: None,
1931 };
1932
1933 let filtered = introspection_type.filter_deprecated_fields(false);
1935 let fields = filtered.fields.as_ref().unwrap();
1936 assert_eq!(fields.len(), 2);
1937 assert!(fields.iter().any(|f| f.name == "id"));
1938 assert!(fields.iter().any(|f| f.name == "newField"));
1939 assert!(!fields.iter().any(|f| f.name == "oldField"));
1940
1941 let unfiltered = introspection_type.filter_deprecated_fields(true);
1943 let fields = unfiltered.fields.as_ref().unwrap();
1944 assert_eq!(fields.len(), 3);
1945 }
1946
1947 #[test]
1948 fn test_filter_deprecated_enum_values() {
1949 let introspection_type = IntrospectionType {
1951 kind: TypeKind::Enum,
1952 name: Some("Status".to_string()),
1953 description: None,
1954 fields: None,
1955 interfaces: None,
1956 possible_types: None,
1957 enum_values: Some(vec![
1958 IntrospectionEnumValue {
1959 name: "ACTIVE".to_string(),
1960 description: None,
1961 is_deprecated: false,
1962 deprecation_reason: None,
1963 },
1964 IntrospectionEnumValue {
1965 name: "INACTIVE".to_string(),
1966 description: None,
1967 is_deprecated: true,
1968 deprecation_reason: Some("Use DISABLED instead".to_string()),
1969 },
1970 IntrospectionEnumValue {
1971 name: "DISABLED".to_string(),
1972 description: None,
1973 is_deprecated: false,
1974 deprecation_reason: None,
1975 },
1976 ]),
1977 input_fields: None,
1978 of_type: None,
1979 specified_by_u_r_l: None,
1980 };
1981
1982 let filtered = introspection_type.filter_deprecated_enum_values(false);
1984 let values = filtered.enum_values.as_ref().unwrap();
1985 assert_eq!(values.len(), 2);
1986 assert!(values.iter().any(|v| v.name == "ACTIVE"));
1987 assert!(values.iter().any(|v| v.name == "DISABLED"));
1988 assert!(!values.iter().any(|v| v.name == "INACTIVE"));
1989
1990 let unfiltered = introspection_type.filter_deprecated_enum_values(true);
1992 let values = unfiltered.enum_values.as_ref().unwrap();
1993 assert_eq!(values.len(), 3);
1994 }
1995
1996 #[test]
1997 fn test_specified_by_url_for_custom_scalars() {
1998 let schema = CompiledSchema::new();
1999 let introspection = IntrospectionBuilder::build(&schema);
2000
2001 let datetime = introspection
2003 .types
2004 .iter()
2005 .find(|t| t.name.as_ref() == Some(&"DateTime".to_string()))
2006 .unwrap();
2007
2008 assert_eq!(datetime.kind, TypeKind::Scalar);
2009 assert!(datetime.specified_by_u_r_l.is_some());
2010 assert!(datetime.specified_by_u_r_l.as_ref().unwrap().contains("date-time"));
2011
2012 let uuid = introspection
2014 .types
2015 .iter()
2016 .find(|t| t.name.as_ref() == Some(&"UUID".to_string()))
2017 .unwrap();
2018
2019 assert_eq!(uuid.kind, TypeKind::Scalar);
2020 assert!(uuid.specified_by_u_r_l.is_some());
2021 assert!(uuid.specified_by_u_r_l.as_ref().unwrap().contains("rfc4122"));
2022
2023 let int = introspection
2025 .types
2026 .iter()
2027 .find(|t| t.name.as_ref() == Some(&"Int".to_string()))
2028 .unwrap();
2029
2030 assert_eq!(int.kind, TypeKind::Scalar);
2031 assert!(int.specified_by_u_r_l.is_none());
2032 }
2033
2034 #[test]
2035 fn test_deprecated_query_introspection() {
2036 use crate::schema::{ArgumentDefinition, AutoParams, DeprecationInfo};
2037
2038 let mut schema = CompiledSchema::new();
2039
2040 schema.queries.push(QueryDefinition {
2042 name: "oldUsers".to_string(),
2043 return_type: "User".to_string(),
2044 returns_list: true,
2045 nullable: false,
2046 arguments: vec![],
2047 sql_source: Some("v_user".to_string()),
2048 description: Some("Old way to get users".to_string()),
2049 auto_params: AutoParams::default(),
2050 deprecation: Some(DeprecationInfo {
2051 reason: Some("Use 'users' instead".to_string()),
2052 }),
2053 jsonb_column: "data".to_string(),
2054 });
2055
2056 schema.queries.push(QueryDefinition {
2058 name: "users".to_string(),
2059 return_type: "User".to_string(),
2060 returns_list: true,
2061 nullable: false,
2062 arguments: vec![
2063 ArgumentDefinition {
2064 name: "first".to_string(),
2065 arg_type: FieldType::Int,
2066 nullable: true,
2067 default_value: None,
2068 description: Some("Number of results to return".to_string()),
2069 deprecation: None,
2070 },
2071 ArgumentDefinition {
2072 name: "limit".to_string(),
2073 arg_type: FieldType::Int,
2074 nullable: true,
2075 default_value: None,
2076 description: Some("Old parameter for limiting results".to_string()),
2077 deprecation: Some(DeprecationInfo {
2078 reason: Some("Use 'first' instead".to_string()),
2079 }),
2080 },
2081 ],
2082 sql_source: Some("v_user".to_string()),
2083 description: Some("Get users with pagination".to_string()),
2084 auto_params: AutoParams::default(),
2085 deprecation: None,
2086 jsonb_column: "data".to_string(),
2087 });
2088
2089 let introspection = IntrospectionBuilder::build(&schema);
2090
2091 let query_type = introspection
2093 .types
2094 .iter()
2095 .find(|t| t.name.as_ref() == Some(&"Query".to_string()))
2096 .unwrap();
2097
2098 let fields = query_type.fields.as_ref().unwrap();
2099
2100 let old_users = fields.iter().find(|f| f.name == "oldUsers").unwrap();
2102 assert!(old_users.is_deprecated);
2103 assert_eq!(old_users.deprecation_reason, Some("Use 'users' instead".to_string()));
2104
2105 let users = fields.iter().find(|f| f.name == "users").unwrap();
2107 assert!(!users.is_deprecated);
2108 assert!(users.deprecation_reason.is_none());
2109
2110 assert_eq!(users.args.len(), 2);
2112
2113 let first_arg = users.args.iter().find(|a| a.name == "first").unwrap();
2115 assert!(!first_arg.is_deprecated);
2116 assert!(first_arg.deprecation_reason.is_none());
2117
2118 let limit_arg = users.args.iter().find(|a| a.name == "limit").unwrap();
2120 assert!(limit_arg.is_deprecated);
2121 assert_eq!(limit_arg.deprecation_reason, Some("Use 'first' instead".to_string()));
2122 }
2123
2124 #[test]
2125 fn test_deprecated_input_field_introspection() {
2126 use crate::schema::{DeprecationInfo, InputFieldDefinition, InputObjectDefinition};
2127
2128 let mut schema = CompiledSchema::new();
2129
2130 schema.input_types.push(InputObjectDefinition {
2132 name: "CreateUserInput".to_string(),
2133 description: Some("Input for creating a user".to_string()),
2134 fields: vec![
2135 InputFieldDefinition {
2136 name: "name".to_string(),
2137 field_type: "String!".to_string(),
2138 default_value: None,
2139 description: Some("User name".to_string()),
2140 deprecation: None,
2141 validation_rules: Vec::new(),
2142 },
2143 InputFieldDefinition {
2144 name: "oldEmail".to_string(),
2145 field_type: "String".to_string(),
2146 default_value: None,
2147 description: Some("Legacy email field".to_string()),
2148 deprecation: Some(DeprecationInfo {
2149 reason: Some("Use 'email' instead".to_string()),
2150 }),
2151 validation_rules: Vec::new(),
2152 },
2153 ],
2154 metadata: None,
2155 });
2156
2157 let introspection = IntrospectionBuilder::build(&schema);
2158
2159 let create_user_input = introspection
2161 .types
2162 .iter()
2163 .find(|t| t.name.as_ref() == Some(&"CreateUserInput".to_string()))
2164 .unwrap();
2165
2166 let input_fields = create_user_input.input_fields.as_ref().unwrap();
2167
2168 let name_field = input_fields.iter().find(|f| f.name == "name").unwrap();
2170 assert!(!name_field.is_deprecated);
2171 assert!(name_field.deprecation_reason.is_none());
2172
2173 let old_email = input_fields.iter().find(|f| f.name == "oldEmail").unwrap();
2175 assert!(old_email.is_deprecated);
2176 assert_eq!(old_email.deprecation_reason, Some("Use 'email' instead".to_string()));
2177 }
2178}