1use std::borrow::Cow;
2use std::sync::Arc;
3use std::sync::LazyLock;
4
5use apollo_compiler::Name;
6use apollo_compiler::Schema;
7use apollo_compiler::collections::HashMap;
8use apollo_compiler::collections::IndexMap;
9use apollo_compiler::collections::IndexSet;
10use indexmap::map::Entry;
11
12use crate::bail;
13use crate::error::FederationError;
14use crate::error::MultipleFederationErrors;
15use crate::error::SingleFederationError;
16use crate::error::suggestion::did_you_mean;
17use crate::error::suggestion::suggestion_list;
18use crate::link::ElementName;
19use crate::link::Import;
20use crate::link::Link;
21use crate::link::link_spec_definition::CORE_VERSIONS;
22use crate::link::link_spec_definition::LINK_VERSIONS;
23use crate::link::link_spec_definition::LinkSpecDefinition;
24use crate::link::spec::Identity;
25use crate::link::spec::Url;
26use crate::link::spec_registry::SPEC_REGISTRY;
27use crate::schema::FederationSchema;
28use crate::schema::position::TypeDefinitionPosition;
29
30#[derive(Clone, Debug)]
32pub struct LinkedElement {
33 pub link: Arc<Link>,
35 pub name_in_spec: Option<Name>,
40}
41
42#[derive(Clone, Debug)]
51pub struct LinksMetadata {
52 link_itself: Arc<Link>,
54 link_spec_definition: &'static LinkSpecDefinition,
56 by_spec_name_in_schema: IndexMap<Arc<str>, Arc<Link>>,
59 by_identity: IndexMap<Identity, (Arc<Link>, IndexMap<ElementName, ElementName>)>,
62 by_import_element_name_in_schema: IndexMap<ElementName, (Arc<Link>, ElementName)>,
66}
67
68impl LinksMetadata {
69 pub fn from_schema(schema: &Schema) -> Result<Option<LinksMetadata>, FederationError> {
85 let mut bootstrap_directives = schema
89 .schema_definition
90 .directives
91 .iter()
92 .filter(|d| LinkSpecDefinition::is_bootstrap_directive(schema, d));
93 let Some(bootstrap_directive) = bootstrap_directives.next() else {
94 return Ok(None);
95 };
96 if let Some(extraneous_directive) = bootstrap_directives.next() {
98 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
99 message: format!(
100 "Cannot link the link/core feature itself in `{}` since it has already been linked in `{}`",
101 extraneous_directive.serialize().no_indent(),
102 bootstrap_directive.serialize().no_indent()
103 )
104 }.into());
105 }
106 let url =
109 Link::from_directive_application_when_link_spec_unknown(bootstrap_directive, schema)?
110 .url;
111 let link_spec_definition = if url.identity == Identity::link_identity() {
112 LINK_VERSIONS.find(&url.version).ok_or_else(|| {
113 SingleFederationError::UnknownLinkVersion {
114 message: format!(
115 r#"Schema uses unknown version {} of the {} spec"#,
116 url.version,
117 Identity::LINK_NAME,
118 ),
119 }
120 })
121 } else if url.identity == Identity::core_identity() {
122 CORE_VERSIONS.find(&url.version).ok_or_else(|| {
123 SingleFederationError::UnknownLinkVersion {
124 message: format!(
125 r#"Schema uses unknown version {} of the {} spec"#,
126 url.version,
127 Identity::CORE_NAME,
128 ),
129 }
130 })
131 } else {
132 bail!("Unexpectedly found non-link/core URL for bootstrap directive");
133 }?;
134
135 let link_directive_name_in_schema = &bootstrap_directive.name;
138 let mut link_itself: Option<_> = None;
139 let mut by_spec_name_in_schema = Default::default();
140 let mut by_identity = Default::default();
141 let mut by_import_element_name_in_schema = Default::default();
142 let mut first_conflict_by_spec_name_in_schema: IndexMap<Arc<str>, ElementName> =
161 Default::default();
162 for directive in schema.schema_definition.directives.iter() {
163 if &directive.name != link_directive_name_in_schema {
165 continue;
166 }
167 let link = Arc::new(link_spec_definition.link_from_directive(directive, schema)?);
169 if link.url == url && link_itself.replace(link.clone()).is_some() {
171 bail!("Unexpectedly multiple @link applications for the link/core feature");
172 }
173 Self::add_link(
174 link,
175 &mut by_spec_name_in_schema,
176 &mut by_identity,
177 &mut by_import_element_name_in_schema,
178 &mut first_conflict_by_spec_name_in_schema,
179 )?;
180 }
181 let Some(link_itself) = link_itself else {
182 bail!("Unexpectedly no @link applications for the link/core feature");
183 };
184
185 Ok(Some(LinksMetadata {
186 link_itself,
187 link_spec_definition,
188 by_spec_name_in_schema,
189 by_identity,
190 by_import_element_name_in_schema,
191 }))
192 }
193
194 fn add_link(
195 link: Arc<Link>,
196 by_spec_name_in_schema: &mut IndexMap<Arc<str>, Arc<Link>>,
197 by_identity: &mut IndexMap<Identity, (Arc<Link>, IndexMap<ElementName, ElementName>)>,
198 by_import_element_name_in_schema: &mut IndexMap<ElementName, (Arc<Link>, ElementName)>,
199 first_conflict_by_spec_name_in_schema: &mut IndexMap<Arc<str>, ElementName>,
200 ) -> Result<(), FederationError> {
201 let identity = &link.url.identity;
202 if by_identity.contains_key(identity) {
207 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
208 message: format!(
209 r#"Cannot link feature "{}" since it has already been linked in the schema."#,
210 identity,
211 ),
212 }
213 .into());
214 }
215
216 let spec_name_in_schema = link.spec_name_in_schema();
217 if !(identity == &Identity::tag_identity()
225 && spec_name_in_schema == "federation__tag"
226 && link.imports.is_empty()
227 || identity == &Identity::inaccessible_identity()
228 && spec_name_in_schema == "federation__inaccessible"
229 && link.imports.is_empty())
230 {
231 if spec_name_in_schema.contains("__") {
235 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
236 message: format!(
237 r#"Cannot link feature "{}" as "{}" since it contains "__". Please rename to a compliant name via "as"."#,
238 identity,
239 spec_name_in_schema,
240 ),
241 }.into());
242 }
243 }
244 if spec_name_in_schema.ends_with("_") {
248 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
249 message: format!(
250 r#"Cannot link feature "{}" as "{}" since it ends in "_". Please rename to a compliant name via "as"."#,
251 identity,
252 spec_name_in_schema,
253 ),
254 }.into());
255 }
256 if !SPEC_NAME_IN_SCHEMA_REGEX.is_match(&spec_name_in_schema) {
267 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
268 message: format!(
269 r#"Cannot link feature "{}" as "{}" since it is not a valid GraphQL name. Please rename to a compliant name via "as"."#,
270 identity,
271 spec_name_in_schema,
272 ),
273 }.into());
274 }
275 if let Some(conflict_import_element_name_in_schema) =
278 first_conflict_by_spec_name_in_schema.get(spec_name_in_schema.as_str())
279 {
280 let Some((conflict_link, conflict_import_element_name_in_spec)) =
281 by_import_element_name_in_schema.get(conflict_import_element_name_in_schema)
282 else {
283 bail!("Unexpectedly cannot find link for import");
284 };
285 let conflict_identity = &conflict_link.url.identity;
286 Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
287 let import_error_message = if conflict_import_element_name_in_spec
288 == conflict_import_element_name_in_schema
289 {
290 format!(r#""{}""#, conflict_import_element_name_in_spec)
291 } else {
292 format!(
293 r#""{}" as "{}""#,
294 conflict_import_element_name_in_spec, conflict_import_element_name_in_schema
295 )
296 };
297 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
298 message: format!(
299 r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
300 import_error_message,
301 conflict_identity,
302 identity,
303 ),
304 }.into());
305 }
306 let conflict_import_element_name_in_schema = ElementName {
309 name: spec_name_in_schema.clone(),
310 is_directive: true,
311 };
312 if let Some((conflict_link, conflict_import_element_name_in_spec)) =
313 by_import_element_name_in_schema.get(&conflict_import_element_name_in_schema)
314 {
315 let conflict_identity = &conflict_link.url.identity;
316 Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
317 let import_error_message = if conflict_import_element_name_in_spec
318 == &conflict_import_element_name_in_schema
319 {
320 format!(r#""{}""#, conflict_import_element_name_in_spec)
321 } else {
322 format!(
323 r#""{}" as "{}""#,
324 conflict_import_element_name_in_spec, conflict_import_element_name_in_schema
325 )
326 };
327 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
328 message: format!(
329 r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
330 import_error_message,
331 conflict_identity,
332 identity
333 ),
334 }.into());
335 }
336 if let Some(existing_link) = by_spec_name_in_schema.get(spec_name_in_schema.as_str()) {
338 let existing_identity = &existing_link.url.identity;
339 Self::check_tag_inaccessible_conflict(existing_identity, identity)?;
340 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
341 message: format!(
342 r#"Cannot link feature {} as "{}" since another feature "{}" already uses that alias. Please rename the feature to avoid conflicts via "as"."#,
343 identity,
344 spec_name_in_schema,
345 existing_identity
346 ),
347 }.into());
348 }
349
350 let mut by_identity_imports: IndexMap<ElementName, ElementName> = Default::default();
351 let known_element_names_in_spec: Option<IndexSet<ElementName>> = SPEC_REGISTRY
352 .get_definition(&link.url)
353 .map(|spec_definition| spec_definition.all_element_names().collect());
354 for import in &link.imports {
355 let import_element_name_in_spec = import.element_name_in_spec();
356 let import_element_name_in_schema = import.element_name_in_schema();
357 let import_error_message =
358 if import_element_name_in_spec == import_element_name_in_schema {
359 format!(r#""{}""#, import_element_name_in_spec)
360 } else {
361 format!(
362 r#""{}" as "{}""#,
363 import_element_name_in_spec, import_element_name_in_schema
364 )
365 };
366
367 Self::validate_known_element_name_in_spec(
369 &import_element_name_in_spec,
370 &known_element_names_in_spec,
371 )?;
372 if let Some((split_spec_name_in_schema, split_name_in_spec)) =
375 Self::split_prefixed_name(&import_element_name_in_schema.name)
376 {
377 if split_spec_name_in_schema == spec_name_in_schema.as_str() {
378 if split_name_in_spec != import_element_name_in_spec.name.as_str() {
379 let split_element_name_in_spec = ElementName {
380 name: Name::new_unchecked(split_name_in_spec),
382 is_directive: import.is_directive,
383 };
384 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
385 message: format!(
386 r#"Cannot import {} from feature "{}" since it can be confused with the namespaced name for "{}". Please rename the import to avoid conflicts via "as"."#,
387 import_error_message,
388 identity,
389 split_element_name_in_spec
390 ),
391 }.into());
392 }
393 } else {
394 if let Some(conflict_link) =
395 by_spec_name_in_schema.get(split_spec_name_in_schema)
396 {
397 let conflict_identity = &conflict_link.url.identity;
398 Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
399 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
400 message: format!(
401 r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
402 import_error_message,
403 identity,
404 conflict_identity
405 ),
406 }.into());
407 } else {
408 first_conflict_by_spec_name_in_schema.insert(
412 split_spec_name_in_schema.into(),
413 import_element_name_in_schema.clone(),
414 );
415 }
416 }
417 }
418 if import.is_directive {
421 if import_element_name_in_schema.name == spec_name_in_schema {
422 if import_element_name_in_spec.name.as_str() != link.url.identity.name.as_ref()
423 {
424 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
425 message: format!(
426 r#"Cannot import {} from feature "{}" since it can be confused with the namespaced name for "@{}". Please rename the import to avoid conflicts via "as"."#,
427 import_error_message,
428 identity,
429 link.url.identity.name
430 ),
431 }.into());
432 }
433 } else {
434 if let Some(conflict_link) =
435 by_spec_name_in_schema.get(import_element_name_in_schema.name.as_str())
436 {
437 let conflict_identity = &conflict_link.url.identity;
438 Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
439 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
440 message: format!(
441 r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
442 import_error_message,
443 identity,
444 conflict_identity
445 ),
446 }.into());
447 }
448 }
449 }
450 match by_identity_imports.entry(import_element_name_in_spec.clone()) {
452 Entry::Occupied(entry) => {
453 let existing_element_name_in_schema = entry.get();
454 if existing_element_name_in_schema != &import_element_name_in_schema {
455 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
456 message: format!(
457 r#"Cannot import {} from feature "{}" since it was previously imported as "{}". Please remove one of these imports."#,
458 import_error_message,
459 identity,
460 existing_element_name_in_schema
461 ),
462 }.into());
463 }
464 }
465 Entry::Vacant(entry) => {
466 entry.insert(import_element_name_in_schema.clone());
467 }
468 }
469 match by_import_element_name_in_schema.entry(import_element_name_in_schema) {
471 Entry::Occupied(entry) => {
472 let (existing_link, existing_element_name_in_spec) = entry.get();
473 let existing_identity = &existing_link.url.identity;
474 if existing_identity != identity {
475 Self::check_tag_inaccessible_conflict(existing_identity, identity)?;
476 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
477 message: format!(
478 r#"Cannot import {} from feature "{}" since it was previously imported from feature "{}". Please rename the import to avoid conflicts via "as"."#,
479 import_error_message,
480 identity,
481 existing_identity
482 ),
483 }.into());
484 }
485 if existing_element_name_in_spec != &import_element_name_in_spec {
486 return Err(SingleFederationError::InvalidLinkDirectiveUsage {
487 message: format!(
488 r#"Cannot import {} from feature "{}" since it was previously imported for "{}". Please rename the import to avoid conflicts via "as"."#,
489 import_error_message,
490 identity,
491 existing_element_name_in_spec
492 ),
493 }.into());
494 }
495 }
496 Entry::Vacant(entry) => {
497 entry.insert((link.clone(), import_element_name_in_spec));
498 }
499 }
500 }
501 by_spec_name_in_schema.insert(spec_name_in_schema.into(), link.clone());
502 by_identity.insert(identity.clone(), (link, by_identity_imports));
503 Ok(())
504 }
505
506 pub(crate) fn is_spec_name_in_schema_valid(
510 &self,
511 spec_name_in_schema: &str,
512 identity: &Identity,
513 import_conflicts_by_identity: &ImportConflictsByIdentity,
514 ) -> bool {
515 if spec_name_in_schema.contains("__") {
519 return false;
520 }
521 if spec_name_in_schema.ends_with("_") {
523 return false;
524 }
525 if !GRAPHQL_NAME_REGEX.is_match(spec_name_in_schema) {
531 return false;
532 }
533 for (conflict_identity, import_conflicts) in import_conflicts_by_identity {
534 if identity == conflict_identity {
535 if import_conflicts.self_.contains(spec_name_in_schema) {
538 return false;
539 }
540 } else {
541 if import_conflicts.other.contains(spec_name_in_schema) {
544 return false;
545 }
546 }
547 }
548 if self
550 .by_spec_name_in_schema
551 .contains_key(spec_name_in_schema)
552 {
553 return false;
554 }
555 true
556 }
557
558 pub(crate) fn compute_spec_name_in_schema_conflicts<'a>(
587 spec_conflict_infos: impl IntoIterator<Item = SpecConflictInfo<'a>>,
588 all_element_names: impl IntoIterator<Item = Name>,
589 ) -> (ImportConflictsByIdentity, UniqueSpecNameInSchema) {
590 let mut trie_names: IndexSet<Arc<str>> = all_element_names
592 .into_iter()
593 .map(|n| n.clone().into())
594 .collect();
595 let mut import_conflicts_by_identity: ImportConflictsByIdentity = Default::default();
596 for SpecConflictInfo {
597 spec_name_in_schema,
598 url,
599 imports,
600 } in spec_conflict_infos
601 {
602 trie_names.insert(spec_name_in_schema.clone());
603 let mut self_: IndexSet<_> = Default::default();
604 let mut other: IndexSet<_> = Default::default();
605 for import in imports {
606 let import_name_in_spec = &import.element;
607 let import_name_in_schema = import.name_in_schema();
608 trie_names.insert(import_name_in_schema.clone().into());
609 if let Some((split_spec_name_in_schema, split_name_in_spec)) =
610 Self::split_prefixed_name(import_name_in_schema)
611 {
612 if split_name_in_spec != import_name_in_spec.as_str() {
613 self_.insert(split_spec_name_in_schema.into());
617 }
618 other.insert(split_spec_name_in_schema.into());
621 }
622 if import.is_directive {
623 if import_name_in_spec.as_str() != url.identity.name.as_ref() {
624 self_.insert(import_name_in_schema.clone().into());
628 }
629 other.insert(import_name_in_schema.clone().into());
633 }
634 }
635 import_conflicts_by_identity
636 .insert(url.identity.clone(), ImportConflict { self_, other });
637 }
638 let mut prefix: Option<String> = None;
640 let mut index: usize = 0;
641 let compute_unique_spec_name_in_schema = move |spec_name: &str| {
642 let prefix = prefix.get_or_insert_with(|| {
643 let mut root: TrieNode = Default::default();
644 for name in &trie_names {
646 let mut node = &mut root;
647 for char in name.chars() {
648 node = node.children.entry(char).or_default();
649 }
650 }
651 let mut nodes = vec![TrieTraversal {
655 node: &root,
656 parent: usize::MAX,
658 char_: '\0',
659 }];
660 let mut head: usize = 0;
661 loop {
662 let possible_chars = if head == 0 {
663 TRIE_SPEC_NAME_IN_SCHEMA_START
664 } else {
665 TRIE_SPEC_NAME_IN_SCHEMA_CONTINUE
666 };
667 let node = nodes[head].clone();
668 for char in possible_chars.chars() {
669 if let Some(child) = node.node.children.get(&char) {
670 nodes.push(TrieTraversal {
671 node: child,
672 parent: head,
673 char_: char,
674 })
675 } else {
676 let mut chars = vec![char];
677 let mut cur = &node;
678 while cur.parent != usize::MAX {
679 chars.push(cur.char_);
680 cur = &nodes[cur.parent];
681 }
682 return chars.into_iter().rev().collect();
683 }
684 }
685 head += 1;
686 }
687 });
688 let suffix: String = spec_name
689 .chars()
690 .filter(|c| c.is_ascii_alphabetic())
691 .collect();
692 let unique_spec_name_in_schema = format!("{prefix}{index}{suffix}");
693 index += 1;
694 Name::new_unchecked(&unique_spec_name_in_schema)
695 };
696 (
697 import_conflicts_by_identity,
698 Box::new(compute_unique_spec_name_in_schema),
699 )
700 }
701
702 fn check_tag_inaccessible_conflict(
707 identity1: &Identity,
708 identity2: &Identity,
709 ) -> Result<(), FederationError> {
710 let identities: IndexSet<_> = [identity1, identity2].into_iter().collect();
711 if !identities.contains(&Identity::federation_identity()) {
712 return Ok(());
713 }
714 let (directive, identity) = if identities.contains(&Identity::tag_identity()) {
715 (Identity::TAG_NAME, Identity::tag_identity())
716 } else if identities.contains(&Identity::inaccessible_identity()) {
717 (
718 Identity::INACCESSIBLE_NAME,
719 Identity::inaccessible_identity(),
720 )
721 } else {
722 return Ok(());
723 };
724 Err(SingleFederationError::InvalidLinkDirectiveUsage {
725 message: format!(
726 r#"Please import "@{}" from the feature "{}" instead of using "{}" to avoid potential unexpected behavior in the future."#,
727 directive,
728 Identity::federation_identity(),
729 identity,
730 ),
731 }.into())
732 }
733
734 fn validate_known_element_name_in_spec(
735 element_name_in_spec: &ElementName,
736 known_element_names_in_spec: &Option<IndexSet<ElementName>>,
737 ) -> Result<(), FederationError> {
738 let Some(known_element_names_in_spec) = known_element_names_in_spec else {
739 return Ok(());
740 };
741 if known_element_names_in_spec.contains(element_name_in_spec) {
742 return Ok(());
743 };
744 let mut message_parts = vec![format!(
745 r#"Cannot import unknown element "{}"."#,
746 element_name_in_spec
747 )];
748 if !element_name_in_spec.is_directive
749 && let known_element_name_in_spec = (ElementName {
750 name: element_name_in_spec.name.clone(),
751 is_directive: true,
752 })
753 && known_element_names_in_spec.contains(&known_element_name_in_spec)
754 {
755 message_parts.push(format!(
756 r#" Did you mean directive "{}"?"#,
757 known_element_name_in_spec
758 ));
759 } else if let suggestions = suggestion_list(
760 &element_name_in_spec.to_string(),
761 known_element_names_in_spec.iter().map(ToString::to_string),
762 ) && !suggestions.is_empty()
763 {
764 message_parts.push(did_you_mean(suggestions))
766 }
767 Err(SingleFederationError::InvalidLinkDirectiveUsage {
768 message: message_parts.join(""),
769 }
770 .into())
771 }
772
773 pub(crate) fn validate_no_shadowing_imports(
788 &self,
789 schema: &FederationSchema,
790 ) -> Result<(), FederationError> {
791 let mut used_shadowing_imports: Vec<(ShadowingImport, String)> = Vec::new();
792 for type_pos in schema.get_types() {
793 let element_name_in_schema = ElementName {
794 name: type_pos.type_name().clone(),
795 is_directive: false,
796 };
797 let Some(shadowing_import) = self.get_shadowing_import(&element_name_in_schema) else {
798 continue;
799 };
800 if self.has_non_shadowed_referencing_root_elements(schema, &type_pos) {
801 used_shadowing_imports.push((shadowing_import, type_pos.to_string()));
802 }
803 }
804 for directive_pos in schema.get_directive_definitions() {
805 let element_name_in_schema = ElementName {
806 name: directive_pos.directive_name.clone(),
807 is_directive: true,
808 };
809 let Some(shadowing_import) = self.get_shadowing_import(&element_name_in_schema) else {
810 continue;
811 };
812 if !schema
813 .referencers()
814 .get_directive(&directive_pos.directive_name)
815 .is_empty()
816 {
817 used_shadowing_imports.push((shadowing_import, directive_pos.to_string()));
818 };
819 }
820 if used_shadowing_imports.is_empty() {
821 return Ok(());
822 }
823 Err(FederationError::MultipleFederationErrors(MultipleFederationErrors {
824 errors: used_shadowing_imports.iter().map(|(shadowing_import, coordinate)| {
825 let import_error_message = if shadowing_import.import_element_name_in_spec ==
826 shadowing_import.import_element_name_in_schema
827 {
828 format!(r#""{}""#, shadowing_import.import_element_name_in_spec)
829 } else {
830 format!(
831 r#""{}" as "{}""#,
832 shadowing_import.import_element_name_in_spec,
833 shadowing_import.import_element_name_in_schema
834 )
835 };
836 SingleFederationError::InvalidLinkDirectiveUsage {
837 message: format!(
838 r#"Cannot import {} from feature "{}" since there's a used definition for the namespaced name "{}". Please switch usages of the namespaced name to the import name and remove the definition."#,
839 import_error_message,
840 shadowing_import.link.url.identity,
841 coordinate,
842 )
843 }
844 }).collect(),
845 }))
846 }
847
848 fn has_non_shadowed_referencing_root_elements(
849 &self,
850 schema: &FederationSchema,
851 type_definition_position: &TypeDefinitionPosition,
852 ) -> bool {
853 match type_definition_position {
854 TypeDefinitionPosition::Scalar(type_pos) => {
855 let Some(referencers) = schema.referencers().scalar_types.get(&type_pos.type_name)
856 else {
857 return false;
858 };
859 referencers
860 .object_fields
861 .iter()
862 .map(|field_pos| ElementName {
863 name: field_pos.type_name.clone(),
864 is_directive: false,
865 })
866 .chain(
867 referencers
868 .object_field_arguments
869 .iter()
870 .map(|arg_pos| ElementName {
871 name: arg_pos.type_name.clone(),
872 is_directive: false,
873 }),
874 )
875 .chain(
876 referencers
877 .interface_fields
878 .iter()
879 .map(|field_pos| ElementName {
880 name: field_pos.type_name.clone(),
881 is_directive: false,
882 }),
883 )
884 .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
885 ElementName {
886 name: arg_pos.type_name.clone(),
887 is_directive: false,
888 }
889 }))
890 .chain(
891 referencers
892 .union_fields
893 .iter()
894 .map(|field_pos| ElementName {
895 name: field_pos.type_name.clone(),
896 is_directive: false,
897 }),
898 )
899 .chain(
900 referencers
901 .input_object_fields
902 .iter()
903 .map(|field_pos| ElementName {
904 name: field_pos.type_name.clone(),
905 is_directive: false,
906 }),
907 )
908 .chain(
909 referencers
910 .directive_arguments
911 .iter()
912 .map(|arg_pos| ElementName {
913 name: arg_pos.directive_name.clone(),
914 is_directive: true,
915 }),
916 )
917 .any(|element_name_in_schema| {
918 self.get_shadowing_import(&element_name_in_schema).is_none()
919 })
920 }
921 TypeDefinitionPosition::Object(type_pos) => {
922 let Some(referencers) = schema.referencers().object_types.get(&type_pos.type_name)
923 else {
924 return false;
925 };
926 if !referencers.schema_roots.is_empty() {
927 return true;
928 }
929 referencers
930 .object_fields
931 .iter()
932 .map(|field_pos| ElementName {
933 name: field_pos.type_name.clone(),
934 is_directive: false,
935 })
936 .chain(
937 referencers
938 .interface_fields
939 .iter()
940 .map(|field_pos| ElementName {
941 name: field_pos.type_name.clone(),
942 is_directive: false,
943 }),
944 )
945 .chain(referencers.union_types.iter().map(|type_pos| ElementName {
946 name: type_pos.type_name.clone(),
947 is_directive: false,
948 }))
949 .any(|element_name_in_schema| {
950 self.get_shadowing_import(&element_name_in_schema).is_none()
951 })
952 }
953 TypeDefinitionPosition::Interface(type_pos) => {
954 let Some(referencers) = schema
955 .referencers()
956 .interface_types
957 .get(&type_pos.type_name)
958 else {
959 return false;
960 };
961 referencers
962 .object_types
963 .iter()
964 .map(|type_pos| ElementName {
965 name: type_pos.type_name.clone(),
966 is_directive: false,
967 })
968 .chain(
969 referencers
970 .object_fields
971 .iter()
972 .map(|field_pos| ElementName {
973 name: field_pos.type_name.clone(),
974 is_directive: false,
975 }),
976 )
977 .chain(
978 referencers
979 .interface_types
980 .iter()
981 .map(|type_pos| ElementName {
982 name: type_pos.type_name.clone(),
983 is_directive: false,
984 }),
985 )
986 .chain(
987 referencers
988 .interface_fields
989 .iter()
990 .map(|field_pos| ElementName {
991 name: field_pos.type_name.clone(),
992 is_directive: false,
993 }),
994 )
995 .any(|element_name_in_schema| {
996 self.get_shadowing_import(&element_name_in_schema).is_none()
997 })
998 }
999 TypeDefinitionPosition::Union(type_pos) => {
1000 let Some(referencers) = schema.referencers().union_types.get(&type_pos.type_name)
1001 else {
1002 return false;
1003 };
1004 referencers
1005 .object_fields
1006 .iter()
1007 .map(|field_pos| ElementName {
1008 name: field_pos.type_name.clone(),
1009 is_directive: false,
1010 })
1011 .chain(
1012 referencers
1013 .interface_fields
1014 .iter()
1015 .map(|field_pos| ElementName {
1016 name: field_pos.type_name.clone(),
1017 is_directive: false,
1018 }),
1019 )
1020 .any(|element_name_in_schema| {
1021 self.get_shadowing_import(&element_name_in_schema).is_none()
1022 })
1023 }
1024 TypeDefinitionPosition::Enum(type_pos) => {
1025 let Some(referencers) = schema.referencers().enum_types.get(&type_pos.type_name)
1026 else {
1027 return false;
1028 };
1029 referencers
1030 .object_fields
1031 .iter()
1032 .map(|field_pos| ElementName {
1033 name: field_pos.type_name.clone(),
1034 is_directive: false,
1035 })
1036 .chain(
1037 referencers
1038 .object_field_arguments
1039 .iter()
1040 .map(|arg_pos| ElementName {
1041 name: arg_pos.type_name.clone(),
1042 is_directive: false,
1043 }),
1044 )
1045 .chain(
1046 referencers
1047 .interface_fields
1048 .iter()
1049 .map(|field_pos| ElementName {
1050 name: field_pos.type_name.clone(),
1051 is_directive: false,
1052 }),
1053 )
1054 .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
1055 ElementName {
1056 name: arg_pos.type_name.clone(),
1057 is_directive: false,
1058 }
1059 }))
1060 .chain(
1061 referencers
1062 .input_object_fields
1063 .iter()
1064 .map(|field_pos| ElementName {
1065 name: field_pos.type_name.clone(),
1066 is_directive: false,
1067 }),
1068 )
1069 .chain(
1070 referencers
1071 .directive_arguments
1072 .iter()
1073 .map(|arg_pos| ElementName {
1074 name: arg_pos.directive_name.clone(),
1075 is_directive: true,
1076 }),
1077 )
1078 .any(|element_name_in_schema| {
1079 self.get_shadowing_import(&element_name_in_schema).is_none()
1080 })
1081 }
1082 TypeDefinitionPosition::InputObject(type_pos) => {
1083 let Some(referencers) = schema
1084 .referencers()
1085 .input_object_types
1086 .get(&type_pos.type_name)
1087 else {
1088 return false;
1089 };
1090 referencers
1091 .object_field_arguments
1092 .iter()
1093 .map(|arg_pos| ElementName {
1094 name: arg_pos.type_name.clone(),
1095 is_directive: false,
1096 })
1097 .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
1098 ElementName {
1099 name: arg_pos.type_name.clone(),
1100 is_directive: false,
1101 }
1102 }))
1103 .chain(
1104 referencers
1105 .input_object_fields
1106 .iter()
1107 .map(|field_pos| ElementName {
1108 name: field_pos.type_name.clone(),
1109 is_directive: false,
1110 }),
1111 )
1112 .chain(
1113 referencers
1114 .directive_arguments
1115 .iter()
1116 .map(|arg_pos| ElementName {
1117 name: arg_pos.directive_name.clone(),
1118 is_directive: true,
1119 }),
1120 )
1121 .any(|element_name_in_schema| {
1122 self.get_shadowing_import(&element_name_in_schema).is_none()
1123 })
1124 }
1125 }
1126 }
1127
1128 fn get_shadowing_import(
1129 &self,
1130 element_name_in_schema: &ElementName,
1131 ) -> Option<ShadowingImport<'_>> {
1132 let (link, element_name_in_spec) = self.source_default_name(element_name_in_schema)?;
1133 let element_name_in_spec = element_name_in_spec?;
1135 let import_element_name_in_schema =
1136 self.get_import_element_name_in_schema(link, &element_name_in_spec)?;
1137 if import_element_name_in_schema == element_name_in_schema {
1140 return None;
1141 }
1142 Some(ShadowingImport {
1143 link,
1144 import_element_name_in_spec: element_name_in_spec,
1145 import_element_name_in_schema: import_element_name_in_schema.clone(),
1146 })
1147 }
1148
1149 pub(crate) fn link_itself(&self) -> &Arc<Link> {
1151 &self.link_itself
1152 }
1153
1154 pub(crate) fn link_spec_definition(&self) -> &'static LinkSpecDefinition {
1155 self.link_spec_definition
1156 }
1157
1158 pub(crate) fn by_import_element_name_in_schema(
1159 &self,
1160 ) -> &IndexMap<ElementName, (Arc<Link>, ElementName)> {
1161 &self.by_import_element_name_in_schema
1162 }
1163
1164 pub fn all_links(&self) -> impl ExactSizeIterator<Item = &Arc<Link>> {
1166 self.by_identity.values().map(|(link, _)| link)
1167 }
1168
1169 pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
1170 self.by_identity.get(identity).map(|(link, _)| link.clone())
1171 }
1172
1173 pub fn source_link_of_type(&self, type_name: &Name) -> Option<LinkedElement> {
1174 let element_name_in_schema = ElementName {
1175 name: type_name.clone(),
1176 is_directive: false,
1177 };
1178 self.source_link(&element_name_in_schema)
1179 }
1180
1181 pub fn source_link_of_directive(&self, directive_name: &Name) -> Option<LinkedElement> {
1182 let element_name_in_schema = ElementName {
1183 name: directive_name.clone(),
1184 is_directive: true,
1185 };
1186 self.source_link(&element_name_in_schema)
1187 }
1188
1189 fn source_link(&self, element_name_in_schema: &ElementName) -> Option<LinkedElement> {
1191 if let Some((link, import_element_name_in_spec)) = self
1196 .by_import_element_name_in_schema
1197 .get(element_name_in_schema)
1198 {
1199 return Some(LinkedElement {
1200 link: link.clone(),
1201 name_in_spec: Some(import_element_name_in_spec.name.clone()),
1202 });
1203 }
1204 let (link, element_name_in_spec) = self.source_default_name(element_name_in_schema)?;
1206 Some(LinkedElement {
1207 link: link.clone(),
1208 name_in_spec: element_name_in_spec.and_then(|element_name_in_spec| {
1209 if self
1214 .get_import_element_name_in_schema(link, &element_name_in_spec)
1215 .is_none()
1216 {
1217 Some(element_name_in_spec.name)
1218 } else {
1219 None
1220 }
1221 }),
1222 })
1223 }
1224
1225 fn get_import_element_name_in_schema(
1229 &self,
1230 link: &Arc<Link>,
1231 element_name_in_spec: &ElementName,
1232 ) -> Option<&ElementName> {
1233 self.by_identity
1234 .get(&link.url.identity)
1235 .and_then(|(_, by_name_in_spec)| by_name_in_spec.get(element_name_in_spec))
1236 }
1237
1238 fn source_default_name(
1242 &self,
1243 element_name_in_schema: &ElementName,
1244 ) -> Option<(&Arc<Link>, Option<ElementName>)> {
1245 if let Some((spec_name_in_schema, name_in_spec)) =
1247 Self::split_prefixed_name(&element_name_in_schema.name)
1248 {
1249 if let Some(link) = self.by_spec_name_in_schema.get(spec_name_in_schema) {
1256 return Some((
1257 link,
1258 Name::new(name_in_spec).ok().map(|name| ElementName {
1259 name,
1260 is_directive: element_name_in_schema.is_directive,
1261 }),
1262 ));
1263 }
1264 }
1265 if !element_name_in_schema.is_directive {
1267 return None;
1268 }
1269 let link = self
1270 .by_spec_name_in_schema
1271 .get(element_name_in_schema.name.as_str())?;
1272 let name_in_spec = link.url.identity.name.clone().try_into().ok();
1273 Some((
1274 link,
1275 name_in_spec.map(|name| ElementName {
1276 name,
1277 is_directive: true,
1278 }),
1279 ))
1280 }
1281
1282 fn split_prefixed_name(name_in_schema: &Name) -> Option<(&str, &str)> {
1286 name_in_schema.split_once("__")
1287 }
1288}
1289
1290pub(crate) type ImportConflictsByIdentity = IndexMap<Identity, ImportConflict>;
1291
1292pub(crate) struct ImportConflict {
1293 self_: IndexSet<Arc<str>>,
1295 other: IndexSet<Arc<str>>,
1297}
1298
1299pub(crate) struct SpecConflictInfo<'a> {
1300 pub(crate) spec_name_in_schema: &'a Arc<str>,
1301 pub(crate) url: &'a Url,
1302 pub(crate) imports: Box<dyn Iterator<Item = Cow<'a, Import>> + 'a>,
1303}
1304
1305pub(crate) type UniqueSpecNameInSchema = Box<dyn FnMut(&str) -> Name>;
1306
1307struct ShadowingImport<'a> {
1309 link: &'a Arc<Link>,
1310 import_element_name_in_spec: ElementName,
1311 import_element_name_in_schema: ElementName,
1312}
1313
1314#[derive(Default)]
1315struct TrieNode {
1316 children: HashMap<char, TrieNode>,
1317}
1318
1319#[derive(Clone)]
1320struct TrieTraversal<'a> {
1321 node: &'a TrieNode,
1322 parent: usize,
1323 char_: char,
1324}
1325
1326static SPEC_NAME_IN_SCHEMA_REGEX: LazyLock<regex::Regex> =
1327 LazyLock::new(|| regex::Regex::new(r#"^[_A-Za-z][_0-9A-Za-z.\-]*$"#).unwrap());
1328
1329static GRAPHQL_NAME_REGEX: LazyLock<regex::Regex> =
1330 LazyLock::new(|| regex::Regex::new(r#"^[_A-Za-z][_0-9A-Za-z]*$"#).unwrap());
1331
1332static TRIE_SPEC_NAME_IN_SCHEMA_START: &str = concat!(
1333 "_",
1334 "abcdefghijklmnopqrstuvwxyz",
1335 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1336);
1337
1338static TRIE_SPEC_NAME_IN_SCHEMA_CONTINUE: &str = concat!(
1339 "abcdefghijklmnopqrstuvwxyz",
1340 "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
1341 "0123456789"
1342);
1343
1344#[cfg(test)]
1345mod tests {
1346 use std::collections::BTreeSet;
1347
1348 use apollo_compiler::name;
1349
1350 use super::*;
1351 use crate::link::Import;
1352 use crate::link::Purpose;
1353 use crate::link::spec::Version;
1354 use crate::subgraph::typestate::Subgraph;
1355
1356 fn errors(schema: &str) -> Vec<String> {
1357 let actual_errors: BTreeSet<_> =
1361 match Subgraph::parse("A", "", schema).and_then(|subgraph| subgraph.expand_links()) {
1362 Ok(_) => Default::default(),
1363 Err(error) => error
1364 .errors
1365 .into_iter()
1366 .map(|e| e.error.to_string())
1367 .collect(),
1368 };
1369 actual_errors.into_iter().collect()
1370 }
1371
1372 #[test]
1373 fn explicit_root_directive_import() -> Result<(), FederationError> {
1374 let schema = r#"
1375 extend schema
1376 @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
1377 @link(url: "https://specs.apollo.dev/inaccessible/v0.2", import: ["@inaccessible"])
1378
1379 type Query { x: Int }
1380
1381 enum link__Purpose {
1382 SECURITY
1383 EXECUTION
1384 }
1385
1386 scalar Import
1387
1388 directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
1389 "#;
1390
1391 let schema = Schema::parse(schema, "root_directive.graphqls").unwrap();
1392
1393 let meta = LinksMetadata::from_schema(&schema)?;
1394 let meta = meta.expect("should have metadata");
1395
1396 assert!(
1397 meta.source_link_of_directive(&name!("inaccessible"))
1398 .is_some()
1399 );
1400
1401 Ok(())
1402 }
1403
1404 #[test]
1405 fn renamed_link_directive() -> Result<(), FederationError> {
1406 let schema = r#"
1407 extend schema
1408 @lonk(url: "https://specs.apollo.dev/link/v1.0", as: "lonk")
1409 @lonk(url: "https://specs.apollo.dev/inaccessible/v0.2")
1410
1411 type Query { x: Int }
1412
1413 enum lonk__Purpose {
1414 SECURITY
1415 EXECUTION
1416 }
1417
1418 scalar lonk__Import
1419
1420 directive @lonk(url: String!, as: String, import: [lonk__Import], for: lonk__Purpose) repeatable on SCHEMA
1421 "#;
1422
1423 let schema = Schema::parse(schema, "lonk.graphqls").unwrap();
1424
1425 let meta = LinksMetadata::from_schema(&schema)?.expect("should have metadata");
1426 assert!(
1427 meta.source_link_of_directive(&name!("inaccessible"))
1428 .is_some()
1429 );
1430
1431 Ok(())
1432 }
1433
1434 #[test]
1435 fn renamed_core_directive() -> Result<(), FederationError> {
1436 let schema = r#"
1437 extend schema
1438 @care(feature: "https://specs.apollo.dev/core/v0.2", as: "care")
1439 @care(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
1440
1441 directive @care(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
1442 directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1443 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1444 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1445 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1446
1447 type Query { x: Int }
1448
1449 enum care__Purpose {
1450 SECURITY
1451 EXECUTION
1452 }
1453
1454 scalar care__Import
1455
1456 scalar join__FieldSet
1457
1458 enum join__Graph {
1459 USERS @join__graph(name: "users", url: "http://localhost:4001")
1460 }
1461 "#;
1462
1463 let schema = Schema::parse(schema, "care.graphqls").unwrap();
1464
1465 let meta = LinksMetadata::from_schema(&schema)?.expect("should have metadata");
1466 assert!(
1467 meta.source_link_of_directive(&name!("join__graph"))
1468 .is_some()
1469 );
1470
1471 Ok(())
1472 }
1473
1474 #[test]
1475 fn url_syntax() -> Result<(), FederationError> {
1476 let schema = r#"
1477 extend schema
1478 @link(url: "https://specs.apollo.dev/link/v1.0")
1479 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1480 @link(url: "https://example.com/my-directive/v1.0", import: ["@myDirective"])
1481
1482 type Query { x: Int }
1483
1484 directive @myDirective on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
1485
1486 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1487
1488 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1489
1490 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1491
1492 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1493
1494 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1495
1496 directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1497
1498 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1499 "#;
1500
1501 let schema = Schema::parse(schema, "url_dash.graphqls").unwrap();
1502
1503 let meta = LinksMetadata::from_schema(&schema)?;
1504 let meta = meta.expect("should have metadata");
1505
1506 assert!(
1507 meta.source_link_of_directive(&name!("myDirective"))
1508 .is_some()
1509 );
1510
1511 Ok(())
1512 }
1513
1514 #[test]
1515 fn computes_link_metadata() {
1516 let schema = r#"
1517 extend schema
1518 @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
1519 @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", { name: "@tag", as: "@myTag" }])
1520 @link(url: "https://custom.com/someSpec/v0.2", as: "mySpec")
1521 @link(url: "https://megacorp.com/auth/v1.0", for: SECURITY)
1522
1523 type Query {
1524 x: Int
1525 }
1526
1527 enum link__Purpose {
1528 SECURITY
1529 EXECUTION
1530 }
1531
1532 scalar Import
1533
1534 directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
1535 "#;
1536
1537 let schema = Schema::parse(schema, "testSchema").unwrap();
1538
1539 let meta = LinksMetadata::from_schema(&schema)
1540 .unwrap()
1542 .unwrap();
1543 let names_in_schema = meta
1544 .all_links()
1545 .map(|l| l.spec_name_in_schema())
1546 .collect::<Vec<_>>();
1547 assert_eq!(names_in_schema.len(), 4);
1548 assert_eq!(names_in_schema[0], "link");
1549 assert_eq!(names_in_schema[1], "federation");
1550 assert_eq!(names_in_schema[2], "mySpec");
1551 assert_eq!(names_in_schema[3], "auth");
1552
1553 let link_spec = meta.for_identity(&Identity::link_identity()).unwrap();
1554 assert_eq!(
1555 link_spec.imports.first().unwrap().as_ref(),
1556 &Import {
1557 element: name!("Import"),
1558 is_directive: false,
1559 alias: None
1560 }
1561 );
1562
1563 let fed_spec = meta.for_identity(&Identity::federation_identity()).unwrap();
1564 assert_eq!(fed_spec.url.version, Version { major: 2, minor: 3 });
1565 assert_eq!(fed_spec.purpose, None);
1566
1567 let imports = &fed_spec.imports;
1568 assert_eq!(imports.len(), 2);
1569 assert_eq!(
1570 imports.first().unwrap().as_ref(),
1571 &Import {
1572 element: name!("key"),
1573 is_directive: true,
1574 alias: None
1575 }
1576 );
1577 assert_eq!(
1578 imports.get(1).unwrap().as_ref(),
1579 &Import {
1580 element: name!("tag"),
1581 is_directive: true,
1582 alias: Some(name!("myTag"))
1583 }
1584 );
1585
1586 let auth_spec = meta
1587 .for_identity(&Identity {
1588 domain: "https://megacorp.com".to_string(),
1589 name: name!("auth").into(),
1590 })
1591 .unwrap();
1592 assert_eq!(auth_spec.purpose, Some(Purpose::SECURITY));
1593
1594 let import_source = meta.source_link_of_type(&name!("Import")).unwrap();
1595 assert_eq!(import_source.link.url.identity.name.as_ref(), "link");
1596 assert_eq!(import_source.name_in_spec, Some(name!("Import")));
1597
1598 assert!(meta.source_link_of_type(&name!("Purpose")).is_none());
1600
1601 let purpose_source = meta.source_link_of_type(&name!("link__Purpose")).unwrap();
1602 assert_eq!(purpose_source.link.url.identity.name.as_ref(), "link");
1603 assert_eq!(purpose_source.name_in_spec, Some(name!("Purpose")));
1604
1605 let key_source = meta.source_link_of_directive(&name!("key")).unwrap();
1606 assert_eq!(key_source.link.url.identity.name.as_ref(), "federation");
1607 assert_eq!(key_source.name_in_spec, Some(name!("key")));
1608
1609 assert!(meta.source_link_of_directive(&name!("tag")).is_none());
1611
1612 let tag_source = meta.source_link_of_directive(&name!("myTag")).unwrap();
1613 assert_eq!(tag_source.link.url.identity.name.as_ref(), "federation");
1614 assert_eq!(tag_source.name_in_spec, Some(name!("tag")));
1615 }
1616
1617 mod link_import {
1620 use super::*;
1621
1622 #[test]
1623 fn errors_on_malformed_values() {
1624 insta::assert_debug_snapshot!(errors(r#"
1625 extend schema @link(
1626 url: "https://specs.apollo.dev/federation/v2.0",
1627 import: [2]
1628 )
1629
1630 type Query {
1631 q: Int
1632 }
1633 "#), @r###"
1634 [
1635 "`2` in @link(import:) argument must either be a string `\"<importedElement>\"` or an object `{ name: \"<importedElement>\", as: \"<alias>\" }`",
1636 ]
1637 "###);
1638 insta::assert_debug_snapshot!(errors(r#"
1639 extend schema @link(
1640 url: "https://specs.apollo.dev/federation/v2.0",
1641 import: [{ foo: "bar" }]
1642 )
1643
1644 type Query {
1645 q: Int
1646 }
1647 "#), @r###"
1648 [
1649 "For `{foo: \"bar\"}` in @link(import:) argument, field \"foo\" is not a known field",
1650 ]
1651 "###);
1652 insta::assert_debug_snapshot!(errors(r#"
1653 extend schema @link(
1654 url: "https://specs.apollo.dev/federation/v2.0",
1655 import: [{ name: "@key", badName: "foo" }]
1656 )
1657
1658 type Query {
1659 q: Int
1660 }
1661 "#), @r###"
1662 [
1663 "For `{name: \"@key\", badName: \"foo\"}` in @link(import:) argument, field \"badName\" is not a known field",
1664 ]
1665 "###);
1666 insta::assert_debug_snapshot!(errors(r#"
1667 extend schema @link(
1668 url: "https://specs.apollo.dev/federation/v2.0",
1669 import: [{ name: 42 }]
1670 )
1671
1672 type Query {
1673 q: Int
1674 }
1675 "#), @r###"
1676 [
1677 "For `{name: 42}` in @link(import:) argument, value for field \"name\" must be a string",
1678 ]
1679 "###);
1680 insta::assert_debug_snapshot!(errors(r#"
1681 extend schema @link(
1682 url: "https://specs.apollo.dev/federation/v2.0",
1683 import: [{ name: "42" }]
1684 )
1685
1686 type Query {
1687 q: Int
1688 }
1689 "#), @r###"
1690 [
1691 "For `{name: \"42\"}` in @link(import:) argument, value for field \"name\" is not a valid GraphQL name",
1692 ]
1693 "###);
1694 insta::assert_debug_snapshot!(errors(r#"
1695 extend schema @link(
1696 url: "https://specs.apollo.dev/federation/v2.0",
1697 import: [{ name: "" }]
1698 )
1699
1700 type Query {
1701 q: Int
1702 }
1703 "#), @r###"
1704 [
1705 "For `{name: \"\"}` in @link(import:) argument, value for field \"name\" is not a valid GraphQL name",
1706 ]
1707 "###);
1708 insta::assert_debug_snapshot!(errors(r#"
1709 extend schema @link(
1710 url: "https://specs.apollo.dev/federation/v2.0",
1711 import: [{ name: "@bar", as: "@" }]
1712 )
1713
1714 type Query {
1715 q: Int
1716 }
1717 "#), @r###"
1718 [
1719 "For `{name: \"@bar\", as: \"@\"}` in @link(import:) argument, value for field \"as\" is not a valid GraphQL name",
1720 ]
1721 "###);
1722 insta::assert_debug_snapshot!(errors(r#"
1723 extend schema @link(
1724 url: "https://specs.apollo.dev/federation/v2.0",
1725 import: [{ as: "bar" }]
1726 )
1727
1728 type Query {
1729 q: Int
1730 }
1731 "#), @r###"
1732 [
1733 "For `{as: \"bar\"}` in @link(import:) argument, missing required field \"name\"",
1734 ]
1735 "###);
1736 }
1737
1738 #[test]
1739 fn errors_on_mismatch_between_name_and_alias() {
1740 insta::assert_debug_snapshot!(errors(r#"
1741 extend schema @link(
1742 url: "https://specs.apollo.dev/federation/v2.0",
1743 import: [{ name: "@key", as: "myKey" }]
1744 )
1745
1746 type Query {
1747 q: Int
1748 }
1749 "#), @r###"
1750 [
1751 "For `{name: \"@key\", as: \"myKey\"}` in @link(import:) argument, value for field \"as\" must start with \"@\" since value for field \"name\" does (\"@\" indicates a directive import)",
1752 ]
1753 "###);
1754 insta::assert_debug_snapshot!(errors(r#"
1755 extend schema @link(
1756 url: "https://specs.apollo.dev/federation/v2.0",
1757 import: [{ name: "FieldSet", as: "@fieldSet" }]
1758 )
1759
1760 type Query {
1761 q: Int
1762 }
1763 "#), @r###"
1764 [
1765 "For `{name: \"FieldSet\", as: \"@fieldSet\"}` in @link(import:) argument, value for field \"as\" must not start with \"@\" since value for field \"name\" does not (\"@\" indicates a directive import)",
1766 ]
1767 "###);
1768 }
1769
1770 #[test]
1771 fn errors_on_importing_unknown_elements_for_known_features() {
1772 insta::assert_debug_snapshot!(errors(r#"
1773 extend schema @link(
1774 url: "https://specs.apollo.dev/federation/v2.0",
1775 import: ["@foo"]
1776 )
1777
1778 type Query {
1779 q: Int
1780 }
1781 "#), @r###"
1782 [
1783 "Cannot import unknown element \"@foo\".",
1784 ]
1785 "###);
1786 insta::assert_debug_snapshot!(errors(r#"
1787 extend schema @link(
1788 url: "https://specs.apollo.dev/federation/v2.0",
1789 import: ["key"]
1790 )
1791
1792 type Query {
1793 q: Int
1794 }
1795 "#), @r###"
1796 [
1797 "Cannot import unknown element \"key\". Did you mean directive \"@key\"?",
1798 ]
1799 "###);
1800 insta::assert_debug_snapshot!(errors(r#"
1801 extend schema @link(
1802 url: "https://specs.apollo.dev/federation/v2.0",
1803 import: [{ name: "@sharable" }]
1804 )
1805
1806 type Query {
1807 q: Int
1808 }
1809 "#), @r###"
1810 [
1811 "Cannot import unknown element \"@sharable\". Did you mean \"@shareable\"?",
1812 ]
1813 "###);
1814 }
1815 }
1816
1817 mod link_alias_and_import_conflicts {
1818 use super::*;
1819
1820 #[test]
1821 fn errors_for_same_identity_imported_twice() {
1822 insta::assert_debug_snapshot!(errors(r#"
1823 extend schema
1824 @link(url: "https://specs.apollo.dev/federation/v2.0")
1825 @link(url: "https://specs.apollo.dev/federation/v2.0")
1826
1827 type Query {
1828 q: Int
1829 }
1830 "#), @r###"
1831 [
1832 "Cannot link feature \"https://specs.apollo.dev/federation\" since it has already been linked in the schema.",
1833 ]
1834 "###);
1835 }
1836
1837 #[test]
1838 fn errors_for_spec_name_in_schema_containing_double_underscore() {
1839 insta::assert_debug_snapshot!(errors(r#"
1840 extend schema
1841 @link(url: "https://specs.apollo.dev/federation/v2.0")
1842 @link(url: "https://custom.dev/f__oo/v1.0")
1843
1844 type Query {
1845 q: Int
1846 }
1847 "#), @r###"
1848 [
1849 "Cannot link feature \"https://custom.dev/f__oo\" as \"f__oo\" since it contains \"__\". Please rename to a compliant name via \"as\".",
1850 ]
1851 "###);
1852 }
1853
1854 #[test]
1855 fn succeeds_renaming_spec_name_containing_double_underscore() {
1856 insta::assert_debug_snapshot!(errors(r#"
1857 extend schema
1858 @link(url: "https://specs.apollo.dev/federation/v2.0")
1859 @link(url: "https://custom.dev/f__oo/v1.0", as: "foo")
1860
1861 type Query {
1862 q: Int
1863 }
1864 "#), @"[]");
1865 }
1866
1867 #[test]
1871 fn allows_exception_in_double_underscore_validation_for_federation_namespaced_tag_and_inaccessible()
1872 {
1873 insta::assert_debug_snapshot!(errors(r#"
1874 schema
1875 @link(url: "https://specs.apollo.dev/link/v1.0")
1876 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1877 @link(
1878 url: "https://specs.apollo.dev/inaccessible/v0.2"
1879 as: "federation__inaccessible"
1880 for: SECURITY
1881 )
1882 @link(url: "https://specs.apollo.dev/tag/v0.3", as: "federation__tag") {
1883 query: Query
1884 }
1885
1886 directive @federation__tag(
1887 name: String!
1888 ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
1889
1890 directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
1891
1892 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1893
1894 directive @join__field(
1895 graph: join__Graph
1896 requires: join__FieldSet
1897 provides: join__FieldSet
1898 type: String
1899 external: Boolean
1900 override: String
1901 usedOverridden: Boolean
1902 ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1903
1904 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1905
1906 directive @join__implements(
1907 graph: join__Graph!
1908 interface: String!
1909 ) repeatable on OBJECT | INTERFACE
1910
1911 directive @join__type(
1912 graph: join__Graph!
1913 key: join__FieldSet
1914 extension: Boolean! = false
1915 resolvable: Boolean! = true
1916 isInterfaceObject: Boolean! = false
1917 ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1918
1919 directive @join__unionMember(
1920 graph: join__Graph!
1921 member: String!
1922 ) repeatable on UNION
1923
1924 directive @link(
1925 url: String
1926 as: String
1927 for: link__Purpose
1928 import: [link__Import]
1929 ) repeatable on SCHEMA
1930
1931 scalar join__FieldSet
1932
1933 enum join__Graph {
1934 S @join__graph(name: "s", url: "")
1935 }
1936
1937 scalar link__Import
1938
1939 enum link__Purpose {
1940 SECURITY
1941 EXECUTION
1942 }
1943
1944 type Query @join__type(graph: S) {
1945 q: Int
1946 }
1947 "#), @"[]");
1948 }
1949
1950 #[test]
1951 fn errors_for_spec_name_in_schema_ending_in_underscore() {
1952 insta::assert_debug_snapshot!(errors(r#"
1953 extend schema
1954 @link(url: "https://specs.apollo.dev/federation/v2.0")
1955 @link(url: "https://custom.dev/foo_/v1.0")
1956
1957 type Query {
1958 q: Int
1959 }
1960 "#), @r###"
1961 [
1962 "Cannot link feature \"https://custom.dev/foo_\" as \"foo_\" since it ends in \"_\". Please rename to a compliant name via \"as\".",
1963 ]
1964 "###);
1965 }
1966
1967 #[test]
1968 fn succeeds_renaming_spec_name_ending_in_underscore() {
1969 insta::assert_debug_snapshot!(errors(r#"
1970 extend schema
1971 @link(url: "https://specs.apollo.dev/federation/v2.0")
1972 @link(url: "https://custom.dev/foo_/v1.0", as: "foo")
1973
1974 type Query {
1975 q: Int
1976 }
1977 "#), @"[]");
1978 }
1979
1980 #[test]
1981 fn errors_for_spec_name_in_schema_that_is_not_a_valid_graphql_name() {
1982 insta::assert_debug_snapshot!(errors(r#"
1983 extend schema
1984 @link(url: "https://specs.apollo.dev/federation/v2.0")
1985 @link(url: "https://custom.dev/0foo/v1.0")
1986
1987 type Query {
1988 q: Int
1989 }
1990 "#), @r###"
1991 [
1992 "Cannot link feature \"https://custom.dev/0foo\" as \"0foo\" since it is not a valid GraphQL name. Please rename to a compliant name via \"as\".",
1993 ]
1994 "###);
1995 }
1996
1997 #[test]
1998 fn succeeds_renaming_spec_name_that_is_not_a_valid_graphql_name() {
1999 insta::assert_debug_snapshot!(errors(r#"
2000 extend schema
2001 @link(url: "https://specs.apollo.dev/federation/v2.0")
2002 @link(url: "https://custom.dev/0foo/v1.0", as: "foo")
2003
2004 type Query {
2005 q: Int
2006 }
2007 "#), @"[]");
2008 }
2009
2010 #[test]
2013 fn allows_exception_in_graphql_name_validation_for_period_and_hyphen() {
2014 insta::assert_debug_snapshot!(errors(r#"
2015 extend schema
2016 @link(url: "https://specs.apollo.dev/federation/v2.0")
2017 @link(url: "https://custom.dev/f-o.o/v1.0")
2018
2019 type Query {
2020 q: Int
2021 }
2022 "#), @"[]");
2023 }
2024
2025 #[test]
2026 fn errors_for_spec_name_in_schema_that_conflicts_with_past_namespaced_directive() {
2027 insta::assert_debug_snapshot!(errors(r#"
2028 extend schema
2029 @link(
2030 url: "https://specs.apollo.dev/federation/v2.0"
2031 import: [{ name: "@key", as: "@foo__key" }]
2032 )
2033 @link(url: "https://custom.dev/foo/v1.0")
2034
2035 type Query {
2036 q: Int
2037 }
2038 "#), @r###"
2039 [
2040 "Cannot import \"@key\" as \"@foo__key\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with a namespaced name from another linked feature \"https://custom.dev/foo\". Please rename the import or feature to avoid conflicts via \"as\".",
2041 ]
2042 "###);
2043 }
2044
2045 #[test]
2046 fn succeeds_renaming_spec_name_that_conflicts_with_past_namespaced_directive() {
2047 insta::assert_debug_snapshot!(errors(r#"
2048 extend schema
2049 @link(
2050 url: "https://specs.apollo.dev/federation/v2.0"
2051 import: [{ name: "@key", as: "@foo__key" }]
2052 )
2053 @link(url: "https://custom.dev/foo/v1.0", as: "bar")
2054
2055 type Query {
2056 q: Int
2057 }
2058 "#), @"[]");
2059 }
2060
2061 #[test]
2062 fn errors_for_spec_name_in_schema_that_conflicts_with_future_namespaced_directive() {
2063 insta::assert_debug_snapshot!(errors(r#"
2064 extend schema
2065 @link(url: "https://specs.apollo.dev/federation/v2.0")
2066 @link(
2067 url: "https://custom.dev/foo/v1.0"
2068 import: [{ name: "Foo", as: "federation__Foo" }]
2069 )
2070
2071 type Query {
2072 q: Int
2073 }
2074 "#), @r###"
2075 [
2076 "Cannot import \"Foo\" as \"federation__Foo\" from feature \"https://custom.dev/foo\" since it can be confused with a namespaced name from another linked feature \"https://specs.apollo.dev/federation\". Please rename the import or feature to avoid conflicts via \"as\".",
2077 ]
2078 "###);
2079 }
2080
2081 #[test]
2082 fn succeeds_renaming_spec_name_that_conflicts_with_future_namespaced_directive() {
2083 insta::assert_debug_snapshot!(errors(r#"
2084 extend schema
2085 @link(url: "https://specs.apollo.dev/federation/v2.0", as: "bar")
2086 @link(
2087 url: "https://custom.dev/foo/v1.0"
2088 import: [{ name: "Foo", as: "federation__Foo" }]
2089 )
2090
2091 type Query {
2092 q: Int
2093 }
2094 "#), @"[]");
2095 }
2096
2097 #[test]
2098 fn errors_for_spec_name_in_schema_that_conflicts_with_past_default_directive() {
2099 insta::assert_debug_snapshot!(errors(r#"
2100 extend schema
2101 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2102 @link(url: "https://custom.dev/key/v1.0")
2103
2104 type Query {
2105 q: Int
2106 }
2107 "#), @r###"
2108 [
2109 "Cannot import \"@key\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with a namespaced name from another linked feature \"https://custom.dev/key\". Please rename the import or feature to avoid conflicts via \"as\".",
2110 ]
2111 "###);
2112 }
2113
2114 #[test]
2115 fn succeeds_renaming_spec_name_that_conflicts_with_past_default_directive() {
2116 insta::assert_debug_snapshot!(errors(r#"
2117 extend schema
2118 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2119 @link(url: "https://custom.dev/key/v1.0", as: "foo")
2120
2121 type Query {
2122 q: Int
2123 }
2124 "#), @"[]");
2125 }
2126
2127 #[test]
2128 fn errors_for_spec_name_in_schema_that_conflicts_with_future_default_directive() {
2129 insta::assert_debug_snapshot!(errors(r#"
2130 extend schema
2131 @link(url: "https://specs.apollo.dev/federation/v2.0")
2132 @link(
2133 url: "https://custom.dev/foo/v1.0"
2134 import: [{ name: "@foo", as: "@federation" }]
2135 )
2136
2137 type Query {
2138 q: Int
2139 }
2140 "#), @r###"
2141 [
2142 "Cannot import \"@foo\" as \"@federation\" from feature \"https://custom.dev/foo\" since it can be confused with a namespaced name from another linked feature \"https://specs.apollo.dev/federation\". Please rename the import or feature to avoid conflicts via \"as\".",
2143 ]
2144 "###);
2145 }
2146
2147 #[test]
2148 fn succeeds_renaming_spec_name_that_conflicts_with_future_default_directive() {
2149 insta::assert_debug_snapshot!(errors(r#"
2150 extend schema
2151 @link(url: "https://specs.apollo.dev/federation/v2.0", as: "bar")
2152 @link(
2153 url: "https://custom.dev/foo/v1.0"
2154 import: [{ name: "@foo", as: "@federation" }]
2155 )
2156
2157 type Query {
2158 q: Int
2159 }
2160 "#), @"[]");
2161 }
2162
2163 #[test]
2164 fn errors_for_spec_name_in_schema_that_conflicts_with_another_spec_name_in_schema() {
2165 insta::assert_debug_snapshot!(errors(r#"
2166 extend schema
2167 @link(url: "https://specs.apollo.dev/federation/v2.0")
2168 @link(url: "https://custom.dev/federation/v1.0")
2169
2170 type Query {
2171 q: Int
2172 }
2173 "#), @r###"
2174 [
2175 "Cannot link feature https://custom.dev/federation as \"federation\" since another feature \"https://specs.apollo.dev/federation\" already uses that alias. Please rename the feature to avoid conflicts via \"as\".",
2176 ]
2177 "###);
2178 }
2179
2180 #[test]
2181 fn succeeds_renaming_spec_name_that_conflicts_with_another_spec_name_in_schema() {
2182 insta::assert_debug_snapshot!(errors(r#"
2183 extend schema
2184 @link(url: "https://specs.apollo.dev/federation/v2.0")
2185 @link(url: "https://custom.dev/federation/v1.0", as: "foo")
2186
2187 type Query {
2188 q: Int
2189 }
2190 "#), @"[]");
2191 }
2192
2193 #[test]
2194 fn errors_for_namespaced_import_that_is_not_a_no_op_import() {
2195 insta::assert_debug_snapshot!(errors(r#"
2196 extend schema
2197 @link(
2198 url: "https://specs.apollo.dev/federation/v2.0"
2199 import: [{ name: "@key", as: "@federation__requires" }]
2200 )
2201
2202 type Query {
2203 q: Int
2204 }
2205 "#), @r###"
2206 [
2207 "Cannot import \"@key\" as \"@federation__requires\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with the namespaced name for \"@requires\". Please rename the import to avoid conflicts via \"as\".",
2208 ]
2209 "###);
2210 }
2211
2212 #[test]
2213 fn succeeds_for_namespaced_import_that_is_a_no_op_import() {
2214 insta::assert_debug_snapshot!(errors(r#"
2215 extend schema
2216 @link(
2217 url: "https://specs.apollo.dev/federation/v2.0"
2218 import: [{ name: "@key", as: "@federation__key" }]
2219 )
2220
2221 type Query {
2222 q: Int
2223 }
2224 "#), @"[]");
2225 }
2226
2227 #[test]
2228 fn errors_for_default_directive_import_that_is_not_a_no_op_import() {
2229 insta::assert_debug_snapshot!(errors(r#"
2230 extend schema
2231 @link(url: "https://specs.apollo.dev/federation/v2.0")
2232 @link(
2233 url: "https://custom.dev/foo/v1.0"
2234 as: "bar"
2235 import: [{ name: "@baz", as: "@bar" }]
2236 )
2237
2238 type Query {
2239 q: Int
2240 }
2241 "#), @r###"
2242 [
2243 "Cannot import \"@baz\" as \"@bar\" from feature \"https://custom.dev/foo\" since it can be confused with the namespaced name for \"@foo\". Please rename the import to avoid conflicts via \"as\".",
2244 ]
2245 "###);
2246 }
2247
2248 #[test]
2249 fn succeeds_for_default_directive_import_that_is_a_no_op_import() {
2250 insta::assert_debug_snapshot!(errors(r#"
2251 extend schema
2252 @link(url: "https://specs.apollo.dev/federation/v2.0")
2253 @link(
2254 url: "https://custom.dev/foo/v1.0"
2255 as: "bar"
2256 import: [{ name: "@foo", as: "@bar" }]
2257 )
2258
2259 type Query {
2260 q: Int
2261 }
2262 "#), @"[]");
2263 }
2264
2265 #[test]
2266 fn errors_for_imports_of_one_element_to_different_names() {
2267 insta::assert_debug_snapshot!(errors(r#"
2268 extend schema
2269 @link(
2270 url: "https://specs.apollo.dev/federation/v2.0"
2271 import: ["@key", { name: "@key", as: "@foo" }]
2272 )
2273
2274 type Query {
2275 q: Int
2276 }
2277 "#), @r###"
2278 [
2279 "Cannot import \"@key\" as \"@foo\" from feature \"https://specs.apollo.dev/federation\" since it was previously imported as \"@key\". Please remove one of these imports.",
2280 ]
2281 "###);
2282 }
2283
2284 #[test]
2285 fn succeeds_for_imports_of_one_element_to_same_name() {
2286 insta::assert_debug_snapshot!(errors(r#"
2287 extend schema
2288 @link(
2289 url: "https://specs.apollo.dev/federation/v2.0"
2290 import: [
2291 { name: "@key", as: "@foo" }
2292 "@requires"
2293 { name: "@key", as: "@foo" }
2294 ]
2295 )
2296
2297 type Query {
2298 q: Int
2299 }
2300 "#), @"[]");
2301 }
2302
2303 #[test]
2304 fn errors_for_import_name_in_schema_that_already_exists_in_different_spec() {
2305 insta::assert_debug_snapshot!(errors(r#"
2306 extend schema
2307 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2308 @link(
2309 url: "https://custom.dev/foo/v1.0"
2310 import: [{ name: "@foo", as: "@key" }]
2311 )
2312
2313 type Query {
2314 q: Int
2315 }
2316 "#), @r###"
2317 [
2318 "Cannot import \"@foo\" as \"@key\" from feature \"https://custom.dev/foo\" since it was previously imported from feature \"https://specs.apollo.dev/federation\". Please rename the import to avoid conflicts via \"as\".",
2319 ]
2320 "###);
2321 }
2322
2323 #[test]
2324 fn succeeds_renaming_import_name_that_already_exists_in_different_spec() {
2325 insta::assert_debug_snapshot!(errors(r#"
2326 extend schema
2327 @link(
2328 url: "https://specs.apollo.dev/federation/v2.0"
2329 import: [{ name: "@key", as: "@bar" }]
2330 )
2331 @link(
2332 url: "https://custom.dev/foo/v1.0"
2333 import: [{ name: "@foo", as: "@key" }]
2334 )
2335
2336 type Query {
2337 q: Int
2338 }
2339 "#), @"[]");
2340 }
2341
2342 #[test]
2343 fn errors_for_import_name_in_schema_that_already_exists_in_same_spec() {
2344 insta::assert_debug_snapshot!(errors(r#"
2345 extend schema
2346 @link(
2347 url: "https://specs.apollo.dev/federation/v2.0"
2348 import: ["@key", { name: "@requires", as: "@key" }]
2349 )
2350
2351 type Query {
2352 q: Int
2353 }
2354 "#), @r###"
2355 [
2356 "Cannot import \"@requires\" as \"@key\" from feature \"https://specs.apollo.dev/federation\" since it was previously imported for \"@key\". Please rename the import to avoid conflicts via \"as\".",
2357 ]
2358 "###);
2359 }
2360
2361 #[test]
2362 fn succeeds_renaming_import_name_that_already_exists_in_same_spec() {
2363 insta::assert_debug_snapshot!(errors(r#"
2364 extend schema
2365 @link(
2366 url: "https://specs.apollo.dev/federation/v2.0"
2367 import: [
2368 { name: "@key", as: "@requires" }
2369 { name: "@requires", as: "@key" }
2370 ]
2371 )
2372
2373 type Query {
2374 q: Int
2375 }
2376 "#), @"[]");
2377 }
2378
2379 #[test]
2380 fn errors_for_used_shadowed_directive_import() {
2381 insta::assert_debug_snapshot!(errors(r#"
2382 extend schema
2383 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2384
2385 directive @federation__key(
2386 fields: federation__FieldSet!
2387 resolvable: Boolean = true
2388 ) repeatable on OBJECT | INTERFACE
2389
2390 scalar federation__FieldSet
2391
2392 type Query {
2393 users: [User!]!
2394 }
2395
2396 type User @federation__key(fields: "id") {
2397 id: ID!
2398 name: String!
2399 }
2400 "#), @r###"
2401 [
2402 "Cannot import \"@key\" from feature \"https://specs.apollo.dev/federation\" since there's a used definition for the namespaced name \"@federation__key\". Please switch usages of the namespaced name to the import name and remove the definition.",
2403 ]
2404 "###);
2405 }
2406
2407 #[test]
2408 fn succeeds_for_unused_shadowed_directive_import() {
2409 insta::assert_debug_snapshot!(errors(r#"
2410 extend schema
2411 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2412
2413 directive @federation__key(
2414 fields: federation__FieldSet!
2415 resolvable: Boolean = true
2416 ) repeatable on OBJECT | INTERFACE
2417
2418 scalar federation__FieldSet
2419
2420 type Query {
2421 users: [User!]!
2422 }
2423
2424 type User @key(fields: "id") {
2425 id: ID!
2426 name: String!
2427 }
2428 "#), @"[]");
2429 }
2430
2431 #[test]
2432 fn errors_for_used_shadowed_type_import() {
2433 insta::assert_debug_snapshot!(errors(r#"
2434 extend schema
2435 @link(
2436 url: "https://specs.apollo.dev/federation/v2.0"
2437 import: ["FieldSet"]
2438 )
2439
2440 scalar federation__FieldSet
2441
2442 type Query {
2443 users: [User!]!
2444 }
2445
2446 type User {
2447 id: ID!
2448 fieldSet: federation__FieldSet!
2449 }
2450 "#), @r###"
2451 [
2452 "Cannot import \"FieldSet\" from feature \"https://specs.apollo.dev/federation\" since there's a used definition for the namespaced name \"federation__FieldSet\". Please switch usages of the namespaced name to the import name and remove the definition.",
2453 ]
2454 "###);
2455 }
2456
2457 #[test]
2458 fn succeeds_for_unused_shadowed_type_import() {
2459 insta::assert_debug_snapshot!(errors(r#"
2460 extend schema
2461 @link(
2462 url: "https://specs.apollo.dev/federation/v2.0"
2463 import: ["FieldSet"]
2464 )
2465
2466 scalar federation__FieldSet
2467
2468 type Query {
2469 users: [User!]!
2470 }
2471
2472 type User {
2473 id: ID!
2474 fieldSet: String!
2475 }
2476 "#), @"[]");
2477 }
2478
2479 #[test]
2480 fn succeeds_for_shadowed_type_import_used_in_shadowed_import() {
2481 insta::assert_debug_snapshot!(errors(r#"
2482 extend schema
2483 @link(
2484 url: "https://specs.apollo.dev/federation/v2.0"
2485 import: ["@key", "FieldSet"]
2486 )
2487
2488 directive @federation__key(
2489 fields: federation__FieldSet!
2490 resolvable: Boolean = true
2491 ) repeatable on OBJECT | INTERFACE
2492
2493 scalar federation__FieldSet
2494
2495 type Query {
2496 users: [User!]!
2497 }
2498
2499 type User {
2500 id: ID!
2501 fieldSet: String!
2502 }
2503 "#), @"[]");
2504 }
2505 }
2506
2507 #[test]
2508 fn allowed_link_directive_definitions() -> Result<(), FederationError> {
2509 let link_defs = [
2510 "directive @link(url: String!, as: String) repeatable on SCHEMA",
2511 "directive @link(url: String, as: String) repeatable on SCHEMA",
2512 "directive @link(url: String!) repeatable on SCHEMA",
2513 "directive @link(url: String) repeatable on SCHEMA",
2514 ];
2515 let schema_prefix = r#"
2516 extend schema @link(url: "https://specs.apollo.dev/link/v1.0")
2517 type Query { x: Int }
2518 "#;
2519 for link_def in link_defs {
2520 let schema_doc = format!("{schema_prefix}\n{link_def}");
2521 let schema = Schema::parse(&schema_doc, "test.graphql").unwrap();
2522 let meta = LinksMetadata::from_schema(&schema)?;
2523 assert!(meta.is_some(), "should have metadata for: {link_def}");
2524 }
2525 Ok(())
2526 }
2527}