Skip to main content

apollo_federation/connectors/
models.rs

1mod headers;
2mod http_json_transport;
3mod keys;
4mod problem_location;
5mod source;
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use apollo_compiler::Name;
11use apollo_compiler::Schema;
12use apollo_compiler::collections::HashSet;
13use apollo_compiler::collections::IndexMap;
14use apollo_compiler::collections::IndexSet;
15use apollo_compiler::executable::FieldSet;
16use apollo_compiler::schema::ExtendedType;
17use apollo_compiler::validation::Valid;
18use keys::make_key_field_set_from_variables;
19use serde_json::Value;
20
21pub use self::headers::Header;
22pub(crate) use self::headers::HeaderParseError;
23pub use self::headers::HeaderSource;
24pub use self::headers::OriginatingDirective;
25pub use self::http_json_transport::HTTPMethod;
26pub use self::http_json_transport::HttpJsonTransport;
27pub use self::http_json_transport::MakeUriError;
28pub use self::problem_location::ProblemLocation;
29pub use self::source::SourceName;
30use super::ConnectId;
31use super::JSONSelection;
32use super::PathSelection;
33use super::id::ConnectorPosition;
34use super::json_selection::VarPaths;
35use super::spec::connect::ConnectBatchArguments;
36use super::spec::connect::ConnectDirectiveArguments;
37use super::spec::errors::ErrorsArguments;
38use super::spec::source::SourceDirectiveArguments;
39use super::variable::Namespace;
40use super::variable::VariableReference;
41use crate::connectors::ConnectSpec;
42use crate::connectors::spec::ConnectLink;
43use crate::connectors::spec::extract_connect_directive_arguments;
44use crate::connectors::spec::extract_source_directive_arguments;
45use crate::error::FederationError;
46use crate::error::SingleFederationError;
47
48// --- Connector ---------------------------------------------------------------
49
50#[derive(Debug, Clone)]
51pub struct Connector {
52    pub id: ConnectId,
53    /// HTTP transport configuration. `None` for mapping-only connectors (where `http` is omitted).
54    pub transport: Option<HttpJsonTransport>,
55    pub selection: JSONSelection,
56    pub config: Option<CustomConfiguration>,
57    pub max_requests: Option<usize>,
58
59    /// The type of entity resolver to use for this connector
60    pub entity_resolver: Option<EntityResolver>,
61    /// Which version of the connect spec is this connector using?
62    pub spec: ConnectSpec,
63
64    /// All supertype-subtype(s) relationships from the source schema.
65    pub schema_subtypes_map: IndexMap<String, IndexSet<String>>,
66
67    /// The request headers referenced in the connectors request mapping
68    pub request_headers: HashSet<String>,
69    /// The request or response headers referenced in the connectors response mapping
70    pub response_headers: HashSet<String>,
71    /// Environment and context variable keys referenced in the connector
72    pub request_variable_keys: IndexMap<Namespace, IndexSet<String>>,
73    pub response_variable_keys: IndexMap<Namespace, IndexSet<String>>,
74
75    pub batch_settings: Option<ConnectBatchArguments>,
76
77    pub error_settings: ConnectorErrorsSettings,
78
79    /// A label for use in debugging and logging. Includes ID, transport method, and path.
80    pub label: Label,
81}
82
83#[derive(Debug, Clone, Default)]
84pub struct ConnectorErrorsSettings {
85    pub message: Option<JSONSelection>,
86    pub source_extensions: Option<JSONSelection>,
87    pub connect_extensions: Option<JSONSelection>,
88    pub connect_is_success: Option<JSONSelection>,
89}
90
91impl ConnectorErrorsSettings {
92    fn from_directive(
93        connect_errors: Option<&ErrorsArguments>,
94        source_errors: Option<&ErrorsArguments>,
95        connect_is_success: Option<&JSONSelection>,
96    ) -> Self {
97        let message = connect_errors
98            .and_then(|e| e.message.as_ref())
99            .or_else(|| source_errors.and_then(|e| e.message.as_ref()))
100            .cloned();
101        let source_extensions = source_errors.and_then(|e| e.extensions.as_ref()).cloned();
102        let connect_extensions = connect_errors.and_then(|e| e.extensions.as_ref()).cloned();
103        let connect_is_success = connect_is_success.cloned();
104        Self {
105            message,
106            source_extensions,
107            connect_extensions,
108            connect_is_success,
109        }
110    }
111
112    pub fn variable_references(&self) -> impl Iterator<Item = VariableReference<Namespace>> + '_ {
113        self.message
114            .as_ref()
115            .into_iter()
116            .flat_map(|m| m.variable_references())
117            .chain(
118                self.source_extensions
119                    .as_ref()
120                    .into_iter()
121                    .flat_map(|m| m.variable_references()),
122            )
123            .chain(
124                self.connect_extensions
125                    .as_ref()
126                    .into_iter()
127                    .flat_map(|m| m.variable_references()),
128            )
129            .chain(
130                self.connect_is_success
131                    .as_ref()
132                    .into_iter()
133                    .flat_map(|m| m.variable_references()),
134            )
135    }
136}
137
138pub type CustomConfiguration = Arc<HashMap<String, Value>>;
139
140/// Entity resolver type
141///
142/// A connector can be used as a potential entity resolver for a type, with
143/// extra validation rules based on the transport args and field position within
144/// a schema.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum EntityResolver {
147    /// The user defined a connector on a field that acts as an entity resolver
148    Explicit,
149
150    /// The user defined a connector on a field of a type, so we need an entity resolver for that type
151    Implicit,
152
153    /// The user defined a connector on the type directly and uses the $batch variable
154    TypeBatch,
155
156    /// The user defined a connector on the type directly and uses the $this variable
157    TypeSingle,
158}
159
160impl Connector {
161    /// Get a map of connectors from an apollo_compiler::Schema.
162    ///
163    /// Note: the function assumes that we've checked that the schema is valid
164    /// before calling this function. We can't take a `Valid<Schema>` or `ValidFederationSchema`
165    /// because we use this code in validation, which occurs before we've augmented
166    /// the schema with types from `@link` directives.
167    pub fn from_schema(schema: &Schema, subgraph_name: &str) -> Result<Vec<Self>, FederationError> {
168        let Some(link) = ConnectLink::new(schema) else {
169            return Ok(Default::default());
170        };
171        let link = link.map_err(|message| SingleFederationError::UnknownLinkVersion {
172            message: message.message,
173        })?;
174
175        let source_arguments =
176            extract_source_directive_arguments(schema, &link.source_directive_name)?;
177
178        let connect_arguments =
179            extract_connect_directive_arguments(schema, &link.connect_directive_name)?;
180
181        connect_arguments
182            .into_iter()
183            .map(|args| {
184                Self::from_directives(schema, subgraph_name, link.spec, args, &source_arguments)
185            })
186            .collect::<Result<Vec<_>, _>>()
187    }
188
189    fn from_directives(
190        schema: &Schema,
191        subgraph_name: &str,
192        spec: ConnectSpec,
193        connect: ConnectDirectiveArguments,
194        source_arguments: &[SourceDirectiveArguments],
195    ) -> Result<Self, FederationError> {
196        let source = connect
197            .source
198            .and_then(|name| source_arguments.iter().find(|s| s.name == name));
199        let source_name = source.map(|s| s.name.clone());
200
201        // Create our transport (absent for mapping-only connectors)
202        let source_http = source.map(|s| &s.http);
203        let transport = connect
204            .http
205            .map(|connect_http| HttpJsonTransport::from_directive(connect_http, source_http, spec))
206            .transpose()?;
207
208        // Get our batch and error settings
209        let batch_settings = connect.batch;
210        let connect_errors = connect.errors.as_ref();
211        let source_errors = source.and_then(|s| s.errors.as_ref());
212        // Use the connector setting if available, otherwise, use source setting
213        let is_success = connect
214            .is_success
215            .as_ref()
216            .or_else(|| source.and_then(|s| s.is_success.as_ref()));
217
218        let error_settings =
219            ConnectorErrorsSettings::from_directive(connect_errors, source_errors, is_success);
220
221        // Collect all variables and subselections used in the request mappings
222        let request_references: IndexSet<VariableReference<Namespace>> = transport
223            .as_ref()
224            .map(|t| t.variable_references().collect())
225            .unwrap_or_default();
226
227        // Collect all variables and subselections used in response mappings (including errors.message and errors.extensions)
228        let response_references: IndexSet<VariableReference<Namespace>> = connect
229            .selection
230            .variable_references()
231            .chain(error_settings.variable_references())
232            .collect();
233
234        // Store a map of variable names and the set of first-level of keys so we can
235        // more efficiently clone values for mappings (especially for $context and $env)
236        let request_variable_keys = extract_variable_key_references(request_references.iter());
237        let response_variable_keys = extract_variable_key_references(response_references.iter());
238
239        // Store a set of header names referenced in mappings (these are second-level keys)
240        let request_headers = extract_header_references(&request_references); // $request in request mappings
241        let response_headers = extract_header_references(&response_references); // $request or $response in response mappings
242
243        // Last couple of items here!
244        let entity_resolver = determine_entity_resolver(
245            &connect.position,
246            connect.entity,
247            schema,
248            &request_variable_keys,
249        );
250        let label = Label::new(
251            subgraph_name,
252            source_name.as_ref(),
253            transport.as_ref(),
254            entity_resolver.as_ref(),
255        );
256        let id = ConnectId {
257            subgraph_name: subgraph_name.to_string(),
258            source_name,
259            named: connect.connector_id,
260            directive: connect.position,
261        };
262
263        Ok(Connector {
264            id,
265            transport,
266            selection: connect.selection,
267            entity_resolver,
268            config: None,
269            max_requests: None,
270            spec,
271            schema_subtypes_map: Connector::subtypes_map_from_schema(schema),
272            request_headers,
273            response_headers,
274            request_variable_keys,
275            response_variable_keys,
276            batch_settings,
277            error_settings,
278            label,
279        })
280    }
281
282    pub fn subtypes_map_from_schema(schema: &Schema) -> IndexMap<String, IndexSet<String>> {
283        let mut subtypes_map: IndexMap<String, IndexSet<String>> = IndexMap::default();
284
285        // Find any `implements` relationships in object or interface types.
286        for (name, ty) in schema.types.iter() {
287            match ty {
288                ExtendedType::Object(o) => {
289                    for supertype in &o.implements_interfaces {
290                        subtypes_map
291                            .entry(supertype.to_string())
292                            .or_default()
293                            .insert(name.to_string());
294                    }
295                }
296                ExtendedType::Interface(i) => {
297                    for supertype in &i.implements_interfaces {
298                        subtypes_map
299                            .entry(supertype.to_string())
300                            .or_default()
301                            .insert(name.to_string());
302                    }
303                }
304                ExtendedType::Union(u) => {
305                    for member in &u.members {
306                        subtypes_map
307                            .entry(u.name.to_string())
308                            .or_default()
309                            .insert(member.to_string());
310                    }
311                }
312                _ => {
313                    // No other types have .implements_interfaces
314                }
315            }
316        }
317
318        subtypes_map
319    }
320
321    pub(crate) fn variable_references(&self) -> impl Iterator<Item = VariableReference<Namespace>> {
322        let transport_refs = self
323            .transport
324            .as_ref()
325            .into_iter()
326            .flat_map(|t| t.variable_references());
327        let selection_refs = self
328            .selection
329            .external_var_paths()
330            .into_iter()
331            .flat_map(PathSelection::variable_reference);
332        transport_refs.chain(selection_refs)
333    }
334
335    /// Create a field set for a `@key` using `$args`, `$this`, or `$batch` variables.
336    pub fn resolvable_key(&self, schema: &Schema) -> Result<Option<Valid<FieldSet>>, String> {
337        match &self.entity_resolver {
338            None => Ok(None),
339            Some(EntityResolver::Explicit) => {
340                make_key_field_set_from_variables(
341                    schema,
342                    &self.id.directive.base_type_name(schema).ok_or_else(|| {
343                        format!("Missing field {}", self.id.directive.coordinate())
344                    })?,
345                    self.variable_references(),
346                    Namespace::Args,
347                )
348            }
349            Some(EntityResolver::Implicit) => {
350                make_key_field_set_from_variables(
351                    schema,
352                    &self.id.directive.parent_type_name().ok_or_else(|| {
353                        format!("Missing type {}", self.id.directive.coordinate())
354                    })?,
355                    self.variable_references(),
356                    Namespace::This,
357                )
358            }
359            Some(EntityResolver::TypeBatch) => {
360                make_key_field_set_from_variables(
361                    schema,
362                    &self.id.directive.base_type_name(schema).ok_or_else(|| {
363                        format!("Missing type {}", self.id.directive.coordinate())
364                    })?,
365                    self.variable_references(),
366                    Namespace::Batch,
367                )
368            }
369            Some(EntityResolver::TypeSingle) => {
370                make_key_field_set_from_variables(
371                    schema,
372                    &self.id.directive.base_type_name(schema).ok_or_else(|| {
373                        format!("Missing type {}", self.id.directive.coordinate())
374                    })?,
375                    self.variable_references(),
376                    Namespace::This,
377                )
378            }
379        }
380        .map_err(|_| {
381            format!(
382                "Failed to create key for connector {}",
383                self.id.coordinate()
384            )
385        })
386    }
387
388    /// Create an identifier for this connector that can be used for configuration and service identification
389    /// `source_name` will be `None` here when we are using a "sourceless" connector. In this situation, we'll use
390    /// the `synthetic_name` instead so that we have some kind of a unique identifier for this source.
391    pub fn source_config_key(&self) -> String {
392        if let Some(source_name) = &self.id.source_name {
393            format!("{}.{}", self.id.subgraph_name, source_name)
394        } else {
395            format!("{}.{}", self.id.subgraph_name, self.id.synthetic_name())
396        }
397    }
398
399    /// Get the name of the `@connect` directive associated with this [`Connector`] instance.
400    ///
401    /// The [`Name`] can be used to help locate the connector within a source file.
402    pub fn name(&self) -> Name {
403        match &self.id.directive {
404            ConnectorPosition::Field(field_position) => field_position.directive_name.clone(),
405            ConnectorPosition::Type(type_position) => type_position.directive_name.clone(),
406        }
407    }
408
409    /// Get the `id`` of the `@connect` directive associated with this [`Connector`] instance.
410    pub fn id(&self) -> String {
411        self.id.name()
412    }
413
414    /// Get the set of abstract type names from the schema subtypes map
415    pub fn abstract_types(&self) -> IndexSet<String> {
416        self.schema_subtypes_map.keys().cloned().collect()
417    }
418}
419
420/// A descriptive label for a connector, used for debugging and logging.
421#[derive(Debug, Clone)]
422pub struct Label(pub String);
423
424impl Label {
425    fn new(
426        subgraph_name: &str,
427        source: Option<&SourceName>,
428        transport: Option<&HttpJsonTransport>,
429        entity_resolver: Option<&EntityResolver>,
430    ) -> Self {
431        let source = source.map(SourceName::as_str).unwrap_or_default();
432        let batch = match entity_resolver {
433            Some(EntityResolver::TypeBatch) => "[BATCH] ",
434            _ => "",
435        };
436        let transport_label = transport
437            .map(|t| t.label())
438            .unwrap_or_else(|| "mappingOnly".to_string());
439        Self(format!("{batch}{subgraph_name}.{source} {transport_label}"))
440    }
441}
442
443impl From<&str> for Label {
444    fn from(label: &str) -> Self {
445        Self(label.to_string())
446    }
447}
448
449impl AsRef<str> for Label {
450    fn as_ref(&self) -> &str {
451        &self.0
452    }
453}
454
455fn determine_entity_resolver(
456    position: &ConnectorPosition,
457    entity: bool,
458    schema: &Schema,
459    request_variables: &IndexMap<Namespace, IndexSet<String>>,
460) -> Option<EntityResolver> {
461    match position {
462        ConnectorPosition::Field(_) => {
463            match (entity, position.on_root_type(schema)) {
464                (true, _) => Some(EntityResolver::Explicit), // Query.foo @connect(entity: true)
465                (_, false) => Some(EntityResolver::Implicit), // Foo.bar @connect
466                _ => None,
467            }
468        }
469        ConnectorPosition::Type(_) => {
470            if request_variables.contains_key(&Namespace::Batch) {
471                Some(EntityResolver::TypeBatch) // Foo @connect($batch)
472            } else {
473                Some(EntityResolver::TypeSingle) // Foo @connect($this)
474            }
475        }
476    }
477}
478
479/// Get any headers referenced in the variable references by looking at both Request and Response namespaces.
480fn extract_header_references(
481    variable_references: &IndexSet<VariableReference<Namespace>>,
482) -> HashSet<String> {
483    variable_references
484        .iter()
485        .flat_map(|var_ref| {
486            if var_ref.namespace.namespace != Namespace::Request
487                && var_ref.namespace.namespace != Namespace::Response
488            {
489                Vec::new()
490            } else {
491                var_ref
492                    .selection
493                    .get("headers")
494                    .map(|headers_subtrie| headers_subtrie.keys().cloned().collect())
495                    .unwrap_or_default()
496            }
497        })
498        .collect()
499}
500
501/// Create a map of variable namespaces like env and context to a set of the
502/// root keys referenced in the connector
503fn extract_variable_key_references<'a>(
504    references: impl Iterator<Item = &'a VariableReference<Namespace>>,
505) -> IndexMap<Namespace, IndexSet<String>> {
506    let mut variable_keys: IndexMap<Namespace, IndexSet<String>> = IndexMap::default();
507
508    for var_ref in references {
509        // make there there's a key for each namespace
510        let set = variable_keys
511            .entry(var_ref.namespace.namespace)
512            .or_default();
513
514        for key in var_ref.selection.keys() {
515            set.insert(key.to_string());
516        }
517    }
518
519    variable_keys
520}
521
522#[cfg(test)]
523mod tests {
524    use apollo_compiler::Schema;
525    use insta::assert_debug_snapshot;
526
527    use super::*;
528    use crate::ValidFederationSubgraphs;
529    use crate::schema::FederationSchema;
530    use crate::supergraph::extract_subgraphs_from_supergraph;
531
532    static SIMPLE_SUPERGRAPH: &str = include_str!("./tests/schemas/simple.graphql");
533    static SIMPLE_SUPERGRAPH_V0_2: &str = include_str!("./tests/schemas/simple_v0_2.graphql");
534
535    fn get_subgraphs(supergraph_sdl: &str) -> ValidFederationSubgraphs {
536        let schema = Schema::parse(supergraph_sdl, "supergraph.graphql").unwrap();
537        let supergraph_schema = FederationSchema::new(schema).unwrap();
538        extract_subgraphs_from_supergraph(&supergraph_schema, Some(true)).unwrap()
539    }
540
541    #[test]
542    fn test_from_schema() {
543        let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
544        let subgraph = subgraphs.get("connectors").unwrap();
545        let connectors = Connector::from_schema(subgraph.schema.schema(), "connectors").unwrap();
546        assert_debug_snapshot!(&connectors, @r#"
547        [
548            Connector {
549                id: ConnectId {
550                    subgraph_name: "connectors",
551                    source_name: Some(
552                        "json",
553                    ),
554                    named: None,
555                    directive: Field(
556                        ObjectOrInterfaceFieldDirectivePosition {
557                            field: Object(Query.users),
558                            directive_name: "connect",
559                            directive_index: 0,
560                        },
561                    ),
562                },
563                transport: Some(
564                    HttpJsonTransport {
565                        source_template: Some(
566                            StringTemplate {
567                                parts: [
568                                    Constant(
569                                        Constant {
570                                            value: "https://jsonplaceholder.typicode.com/",
571                                            location: 0..37,
572                                        },
573                                    ),
574                                ],
575                            },
576                        ),
577                        connect_template: StringTemplate {
578                            parts: [
579                                Constant(
580                                    Constant {
581                                        value: "/users",
582                                        location: 0..6,
583                                    },
584                                ),
585                            ],
586                        },
587                        method: Get,
588                        headers: [
589                            Header {
590                                name: "authtoken",
591                                source: From(
592                                    "x-auth-token",
593                                ),
594                            },
595                            Header {
596                                name: "user-agent",
597                                source: Value(
598                                    HeaderValue(
599                                        StringTemplate {
600                                            parts: [
601                                                Constant(
602                                                    Constant {
603                                                        value: "Firefox",
604                                                        location: 0..7,
605                                                    },
606                                                ),
607                                            ],
608                                        },
609                                    ),
610                                ),
611                            },
612                        ],
613                        body: None,
614                        source_path: None,
615                        source_query_params: None,
616                        connect_path: None,
617                        connect_query_params: None,
618                    },
619                ),
620                selection: JSONSelection {
621                    inner: Named(
622                        SubSelection {
623                            selections: [
624                                NamedSelection {
625                                    prefix: None,
626                                    path: WithRange {
627                                        node: Path(
628                                            PathSelection {
629                                                path: WithRange {
630                                                    node: Key(
631                                                        WithRange {
632                                                            node: Field(
633                                                                "id",
634                                                            ),
635                                                            range: Some(
636                                                                0..2,
637                                                            ),
638                                                        },
639                                                        WithRange {
640                                                            node: Empty,
641                                                            range: Some(
642                                                                2..2,
643                                                            ),
644                                                        },
645                                                    ),
646                                                    range: Some(
647                                                        0..2,
648                                                    ),
649                                                },
650                                            },
651                                        ),
652                                        range: Some(
653                                            0..2,
654                                        ),
655                                    },
656                                },
657                                NamedSelection {
658                                    prefix: None,
659                                    path: WithRange {
660                                        node: Path(
661                                            PathSelection {
662                                                path: WithRange {
663                                                    node: Key(
664                                                        WithRange {
665                                                            node: Field(
666                                                                "name",
667                                                            ),
668                                                            range: Some(
669                                                                3..7,
670                                                            ),
671                                                        },
672                                                        WithRange {
673                                                            node: Empty,
674                                                            range: Some(
675                                                                7..7,
676                                                            ),
677                                                        },
678                                                    ),
679                                                    range: Some(
680                                                        3..7,
681                                                    ),
682                                                },
683                                            },
684                                        ),
685                                        range: Some(
686                                            3..7,
687                                        ),
688                                    },
689                                },
690                            ],
691                            range: Some(
692                                0..7,
693                            ),
694                        },
695                    ),
696                    spec: V0_1,
697                },
698                config: None,
699                max_requests: None,
700                entity_resolver: None,
701                spec: V0_1,
702                schema_subtypes_map: {
703                    "_Entity": {
704                        "User",
705                    },
706                },
707                request_headers: {},
708                response_headers: {},
709                request_variable_keys: {},
710                response_variable_keys: {},
711                batch_settings: None,
712                error_settings: ConnectorErrorsSettings {
713                    message: None,
714                    source_extensions: None,
715                    connect_extensions: None,
716                    connect_is_success: None,
717                },
718                label: Label(
719                    "connectors.json http: GET /users",
720                ),
721            },
722            Connector {
723                id: ConnectId {
724                    subgraph_name: "connectors",
725                    source_name: Some(
726                        "json",
727                    ),
728                    named: None,
729                    directive: Field(
730                        ObjectOrInterfaceFieldDirectivePosition {
731                            field: Object(Query.posts),
732                            directive_name: "connect",
733                            directive_index: 0,
734                        },
735                    ),
736                },
737                transport: Some(
738                    HttpJsonTransport {
739                        source_template: Some(
740                            StringTemplate {
741                                parts: [
742                                    Constant(
743                                        Constant {
744                                            value: "https://jsonplaceholder.typicode.com/",
745                                            location: 0..37,
746                                        },
747                                    ),
748                                ],
749                            },
750                        ),
751                        connect_template: StringTemplate {
752                            parts: [
753                                Constant(
754                                    Constant {
755                                        value: "/posts",
756                                        location: 0..6,
757                                    },
758                                ),
759                            ],
760                        },
761                        method: Get,
762                        headers: [
763                            Header {
764                                name: "authtoken",
765                                source: From(
766                                    "x-auth-token",
767                                ),
768                            },
769                            Header {
770                                name: "user-agent",
771                                source: Value(
772                                    HeaderValue(
773                                        StringTemplate {
774                                            parts: [
775                                                Constant(
776                                                    Constant {
777                                                        value: "Firefox",
778                                                        location: 0..7,
779                                                    },
780                                                ),
781                                            ],
782                                        },
783                                    ),
784                                ),
785                            },
786                        ],
787                        body: None,
788                        source_path: None,
789                        source_query_params: None,
790                        connect_path: None,
791                        connect_query_params: None,
792                    },
793                ),
794                selection: JSONSelection {
795                    inner: Named(
796                        SubSelection {
797                            selections: [
798                                NamedSelection {
799                                    prefix: None,
800                                    path: WithRange {
801                                        node: Path(
802                                            PathSelection {
803                                                path: WithRange {
804                                                    node: Key(
805                                                        WithRange {
806                                                            node: Field(
807                                                                "id",
808                                                            ),
809                                                            range: Some(
810                                                                0..2,
811                                                            ),
812                                                        },
813                                                        WithRange {
814                                                            node: Empty,
815                                                            range: Some(
816                                                                2..2,
817                                                            ),
818                                                        },
819                                                    ),
820                                                    range: Some(
821                                                        0..2,
822                                                    ),
823                                                },
824                                            },
825                                        ),
826                                        range: Some(
827                                            0..2,
828                                        ),
829                                    },
830                                },
831                                NamedSelection {
832                                    prefix: None,
833                                    path: WithRange {
834                                        node: Path(
835                                            PathSelection {
836                                                path: WithRange {
837                                                    node: Key(
838                                                        WithRange {
839                                                            node: Field(
840                                                                "title",
841                                                            ),
842                                                            range: Some(
843                                                                3..8,
844                                                            ),
845                                                        },
846                                                        WithRange {
847                                                            node: Empty,
848                                                            range: Some(
849                                                                8..8,
850                                                            ),
851                                                        },
852                                                    ),
853                                                    range: Some(
854                                                        3..8,
855                                                    ),
856                                                },
857                                            },
858                                        ),
859                                        range: Some(
860                                            3..8,
861                                        ),
862                                    },
863                                },
864                                NamedSelection {
865                                    prefix: None,
866                                    path: WithRange {
867                                        node: Path(
868                                            PathSelection {
869                                                path: WithRange {
870                                                    node: Key(
871                                                        WithRange {
872                                                            node: Field(
873                                                                "body",
874                                                            ),
875                                                            range: Some(
876                                                                9..13,
877                                                            ),
878                                                        },
879                                                        WithRange {
880                                                            node: Empty,
881                                                            range: Some(
882                                                                13..13,
883                                                            ),
884                                                        },
885                                                    ),
886                                                    range: Some(
887                                                        9..13,
888                                                    ),
889                                                },
890                                            },
891                                        ),
892                                        range: Some(
893                                            9..13,
894                                        ),
895                                    },
896                                },
897                            ],
898                            range: Some(
899                                0..13,
900                            ),
901                        },
902                    ),
903                    spec: V0_1,
904                },
905                config: None,
906                max_requests: None,
907                entity_resolver: None,
908                spec: V0_1,
909                schema_subtypes_map: {
910                    "_Entity": {
911                        "User",
912                    },
913                },
914                request_headers: {},
915                response_headers: {},
916                request_variable_keys: {},
917                response_variable_keys: {},
918                batch_settings: None,
919                error_settings: ConnectorErrorsSettings {
920                    message: None,
921                    source_extensions: None,
922                    connect_extensions: None,
923                    connect_is_success: None,
924                },
925                label: Label(
926                    "connectors.json http: GET /posts",
927                ),
928            },
929        ]
930        "#);
931    }
932
933    #[test]
934    fn test_from_schema_v0_2() {
935        let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH_V0_2);
936        let subgraph = subgraphs.get("connectors").unwrap();
937        let connectors = Connector::from_schema(subgraph.schema.schema(), "connectors").unwrap();
938        assert_debug_snapshot!(&connectors);
939    }
940}