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}