Skip to main content

apollo_federation/subgraph/
spec.rs

1use std::sync::Arc;
2use std::sync::LazyLock;
3
4use apollo_compiler::InvalidNameError;
5use apollo_compiler::Name;
6use apollo_compiler::Node;
7use apollo_compiler::ast::Argument;
8use apollo_compiler::ast::Directive;
9use apollo_compiler::ast::DirectiveDefinition;
10use apollo_compiler::ast::DirectiveLocation;
11use apollo_compiler::ast::EnumValueDefinition;
12use apollo_compiler::ast::FieldDefinition;
13use apollo_compiler::ast::InputValueDefinition;
14use apollo_compiler::ast::Type;
15use apollo_compiler::ast::Value;
16use apollo_compiler::collections::IndexMap;
17use apollo_compiler::collections::IndexSet;
18use apollo_compiler::name;
19use apollo_compiler::schema::Component;
20use apollo_compiler::schema::ComponentName;
21use apollo_compiler::schema::EnumType;
22use apollo_compiler::schema::ExtendedType;
23use apollo_compiler::schema::ObjectType;
24use apollo_compiler::schema::ScalarType;
25use apollo_compiler::schema::UnionType;
26use apollo_compiler::ty;
27use thiserror::Error;
28
29use crate::link::DEFAULT_IMPORT_SCALAR_NAME;
30use crate::link::DEFAULT_LINK_NAME;
31use crate::link::DEFAULT_PURPOSE_ENUM_NAME;
32use crate::link::Import;
33use crate::link::Link;
34use crate::link::spec::Identity;
35use crate::link::spec::Url;
36use crate::link::spec::Version;
37use crate::subgraph::spec::FederationSpecError::UnsupportedFederationDirective;
38use crate::subgraph::spec::FederationSpecError::UnsupportedVersionError;
39
40pub const COMPOSE_DIRECTIVE_NAME: Name = name!("composeDirective");
41pub const CONTEXT_DIRECTIVE_NAME: Name = name!("context");
42pub const KEY_DIRECTIVE_NAME: Name = name!("key");
43pub const EXTENDS_DIRECTIVE_NAME: Name = name!("extends");
44pub const EXTERNAL_DIRECTIVE_NAME: Name = name!("external");
45pub const FROM_CONTEXT_DIRECTIVE_NAME: Name = name!("fromContext");
46pub const INACCESSIBLE_DIRECTIVE_NAME: Name = name!("inaccessible");
47pub const INTF_OBJECT_DIRECTIVE_NAME: Name = name!("interfaceObject");
48pub const OVERRIDE_DIRECTIVE_NAME: Name = name!("override");
49pub const PROVIDES_DIRECTIVE_NAME: Name = name!("provides");
50pub const REQUIRES_DIRECTIVE_NAME: Name = name!("requires");
51pub const SHAREABLE_DIRECTIVE_NAME: Name = name!("shareable");
52pub const TAG_DIRECTIVE_NAME: Name = name!("tag");
53pub const FIELDSET_SCALAR_NAME: Name = name!("FieldSet");
54pub const CONTEXTFIELDVALUE_SCALAR_NAME: Name = name!("ContextFieldValue");
55
56// federated types
57pub const ANY_SCALAR_NAME: Name = name!("_Any");
58pub const ENTITY_UNION_NAME: Name = name!("_Entity");
59pub const SERVICE_TYPE: Name = name!("_Service");
60
61pub const ENTITIES_QUERY: Name = name!("_entities");
62pub const SERVICE_SDL_QUERY: Name = name!("_service");
63
64pub const FEDERATION_V1_DIRECTIVE_NAMES: [Name; 5] = [
65    KEY_DIRECTIVE_NAME,
66    EXTENDS_DIRECTIVE_NAME,
67    EXTERNAL_DIRECTIVE_NAME,
68    PROVIDES_DIRECTIVE_NAME,
69    REQUIRES_DIRECTIVE_NAME,
70];
71
72pub const FEDERATION_V2_DIRECTIVE_NAMES: [Name; 13] = [
73    COMPOSE_DIRECTIVE_NAME,
74    CONTEXT_DIRECTIVE_NAME,
75    KEY_DIRECTIVE_NAME,
76    EXTENDS_DIRECTIVE_NAME,
77    EXTERNAL_DIRECTIVE_NAME,
78    FROM_CONTEXT_DIRECTIVE_NAME,
79    INACCESSIBLE_DIRECTIVE_NAME,
80    INTF_OBJECT_DIRECTIVE_NAME,
81    OVERRIDE_DIRECTIVE_NAME,
82    PROVIDES_DIRECTIVE_NAME,
83    REQUIRES_DIRECTIVE_NAME,
84    SHAREABLE_DIRECTIVE_NAME,
85    TAG_DIRECTIVE_NAME,
86];
87
88pub(crate) const FEDERATION_V2_ELEMENT_NAMES: [Name; 1] = [FIELDSET_SCALAR_NAME];
89
90// This type and the subsequent IndexMap exist purely so we can use match with Names; see comment
91// in FederationSpecDefinitions.directive_definition() for more information.
92enum FederationDirectiveName {
93    Compose,
94    Context,
95    Key,
96    Extends,
97    External,
98    FromContext,
99    Inaccessible,
100    IntfObject,
101    Override,
102    Provides,
103    Requires,
104    Shareable,
105    Tag,
106}
107
108static FEDERATION_DIRECTIVE_NAMES_TO_ENUM: LazyLock<IndexMap<Name, FederationDirectiveName>> =
109    LazyLock::new(|| {
110        IndexMap::from_iter([
111            (COMPOSE_DIRECTIVE_NAME, FederationDirectiveName::Compose),
112            (CONTEXT_DIRECTIVE_NAME, FederationDirectiveName::Context),
113            (KEY_DIRECTIVE_NAME, FederationDirectiveName::Key),
114            (EXTENDS_DIRECTIVE_NAME, FederationDirectiveName::Extends),
115            (EXTERNAL_DIRECTIVE_NAME, FederationDirectiveName::External),
116            (
117                FROM_CONTEXT_DIRECTIVE_NAME,
118                FederationDirectiveName::FromContext,
119            ),
120            (
121                INACCESSIBLE_DIRECTIVE_NAME,
122                FederationDirectiveName::Inaccessible,
123            ),
124            (
125                INTF_OBJECT_DIRECTIVE_NAME,
126                FederationDirectiveName::IntfObject,
127            ),
128            (OVERRIDE_DIRECTIVE_NAME, FederationDirectiveName::Override),
129            (PROVIDES_DIRECTIVE_NAME, FederationDirectiveName::Provides),
130            (REQUIRES_DIRECTIVE_NAME, FederationDirectiveName::Requires),
131            (SHAREABLE_DIRECTIVE_NAME, FederationDirectiveName::Shareable),
132            (TAG_DIRECTIVE_NAME, FederationDirectiveName::Tag),
133        ])
134    });
135
136const MIN_FEDERATION_VERSION: Version = Version { major: 2, minor: 0 };
137const MAX_FEDERATION_VERSION: Version = Version { major: 2, minor: 5 };
138
139#[derive(Error, Debug, PartialEq)]
140pub enum FederationSpecError {
141    #[error(
142        "Specified specification version {specified} is outside of supported range {min}-{max}"
143    )]
144    UnsupportedVersionError {
145        specified: String,
146        min: String,
147        max: String,
148    },
149    #[error("Unsupported federation directive import {0}")]
150    UnsupportedFederationDirective(String),
151    #[error(transparent)]
152    InvalidGraphQLName(InvalidNameError),
153}
154
155impl From<InvalidNameError> for FederationSpecError {
156    fn from(err: InvalidNameError) -> Self {
157        FederationSpecError::InvalidGraphQLName(err)
158    }
159}
160
161#[derive(Debug)]
162pub struct FederationSpecDefinitions {
163    link: Link,
164    pub fieldset_scalar_name: Name,
165}
166
167#[derive(Debug)]
168pub struct LinkSpecDefinitions {
169    link: Link,
170    pub import_scalar_name: Name,
171    pub purpose_enum_name: Name,
172}
173
174pub trait AppliedFederationLink {
175    fn applied_link_directive(&self) -> Directive;
176}
177
178macro_rules! applied_specification {
179    ($($t:ty),+) => {
180        $(impl AppliedFederationLink for $t {
181            /// ```graphql
182            /// @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
183            /// ```
184            fn applied_link_directive(&self) -> Directive {
185                let imports = self
186                    .link
187                    .imports
188                    .iter()
189                    .map(|i| {
190                        if i.alias.is_some() {
191                            Value::Object(vec![
192                                (name!("name"), i.element.as_str().into()),
193                                (name!("as"), i.imported_display_name().to_string().into()),
194                            ])
195                        } else {
196                            i.imported_display_name().to_string().into()
197                        }.into()
198                    })
199                    .collect::<Vec<Node<Value>>>();
200                let mut applied_link_directive = Directive {
201                    name: DEFAULT_LINK_NAME,
202                    arguments: vec![
203                        Argument {
204                            name: name!("url"),
205                            value: self.link.url.to_string().into(),
206                        }.into(),
207                        Argument {
208                            name: name!("import"),
209                            value: Value::List(imports).into(),
210                        }.into(),
211                    ]
212                };
213                if let Some(spec_alias) = &self.link.spec_alias {
214                    applied_link_directive.arguments.push(Argument {
215                        name: name!("as"),
216                        value: spec_alias.as_str().into(),
217                    }.into())
218                }
219                if let Some(purpose) = &self.link.purpose {
220                    applied_link_directive.arguments.push(Argument {
221                        name: name!("for"),
222                        value: Value::Enum(purpose.into()).into(),
223                    }.into())
224                }
225                applied_link_directive
226            }
227        })+
228    }
229}
230
231applied_specification!(FederationSpecDefinitions, LinkSpecDefinitions);
232
233impl FederationSpecDefinitions {
234    pub fn from_link(link: Link) -> Result<Self, FederationSpecError> {
235        if !link
236            .url
237            .version
238            .satisfies_range(&MIN_FEDERATION_VERSION, &MAX_FEDERATION_VERSION)
239        {
240            Err(UnsupportedVersionError {
241                specified: link.url.version.to_string(),
242                min: MIN_FEDERATION_VERSION.to_string(),
243                max: MAX_FEDERATION_VERSION.to_string(),
244            })
245        } else {
246            let fieldset_scalar_name = link.type_name_in_schema(&FIELDSET_SCALAR_NAME);
247            Ok(Self {
248                link,
249                fieldset_scalar_name,
250            })
251        }
252    }
253
254    // The Default trait doesn't allow for returning Results, so we ignore the clippy warning here.
255    #[allow(clippy::should_implement_trait)]
256    pub fn default() -> Result<Self, FederationSpecError> {
257        Self::from_link(Link {
258            url: Url {
259                identity: Identity::federation_identity(),
260                version: MAX_FEDERATION_VERSION,
261            },
262            imports: FEDERATION_V1_DIRECTIVE_NAMES
263                .iter()
264                .map(|i| {
265                    Arc::new(Import {
266                        element: i.clone(),
267                        alias: None,
268                        is_directive: true,
269                    })
270                })
271                .collect::<Vec<Arc<Import>>>(),
272            purpose: None,
273            spec_alias: None,
274        })
275    }
276
277    pub fn namespaced_type_name(&self, name: &Name, is_directive: bool) -> Name {
278        if is_directive {
279            self.link.directive_name_in_schema(name)
280        } else {
281            self.link.type_name_in_schema(name)
282        }
283    }
284
285    pub fn directive_definition(
286        &self,
287        name: &Name,
288        alias: &Option<Name>,
289    ) -> Result<DirectiveDefinition, FederationSpecError> {
290        // TODO: `Name` has custom `PartialEq` and `Eq` impl so Clippy warns it should
291        // not be used in pattern matching (as some future Rust version will likely turn this into
292        // a hard error). We resort instead to indexing into a static IndexMap to get an enum, which
293        // can be used in a match.
294        let Some(enum_name) = FEDERATION_DIRECTIVE_NAMES_TO_ENUM.get(name) else {
295            return Err(UnsupportedFederationDirective(name.to_string()));
296        };
297        Ok(match enum_name {
298            FederationDirectiveName::Compose => self.compose_directive_definition(alias),
299            FederationDirectiveName::Context => self.context_directive_definition(alias),
300            FederationDirectiveName::Key => self.key_directive_definition(alias)?,
301            FederationDirectiveName::Extends => self.extends_directive_definition(alias),
302            FederationDirectiveName::External => self.external_directive_definition(alias),
303            FederationDirectiveName::FromContext => self.from_context_directive_definition(alias),
304            FederationDirectiveName::Inaccessible => self.inaccessible_directive_definition(alias),
305            FederationDirectiveName::IntfObject => {
306                self.interface_object_directive_definition(alias)
307            }
308            FederationDirectiveName::Override => self.override_directive_definition(alias),
309            FederationDirectiveName::Provides => self.provides_directive_definition(alias)?,
310            FederationDirectiveName::Requires => self.requires_directive_definition(alias)?,
311            FederationDirectiveName::Shareable => self.shareable_directive_definition(alias),
312            FederationDirectiveName::Tag => self.tag_directive_definition(alias),
313        })
314    }
315
316    /// scalar FieldSet
317    pub fn fieldset_scalar_definition(&self, name: Name) -> ScalarType {
318        ScalarType {
319            description: None,
320            name,
321            directives: Default::default(),
322        }
323    }
324
325    /// scalar ContextFieldValue
326    pub fn contextfieldvalue_scalar_definition(&self, alias: &Option<Name>) -> ScalarType {
327        ScalarType {
328            description: None,
329            name: alias.clone().unwrap_or(CONTEXTFIELDVALUE_SCALAR_NAME),
330            directives: Default::default(),
331        }
332    }
333
334    fn fields_argument_definition(&self) -> Result<InputValueDefinition, FederationSpecError> {
335        Ok(InputValueDefinition {
336            description: None,
337            name: name!("fields"),
338            ty: Type::Named(self.namespaced_type_name(&FIELDSET_SCALAR_NAME, false))
339                .non_null()
340                .into(),
341            default_value: None,
342            directives: Default::default(),
343        })
344    }
345
346    /// directive @composeDirective(name: String!) repeatable on SCHEMA
347    fn compose_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
348        DirectiveDefinition {
349            description: None,
350            name: alias.clone().unwrap_or(COMPOSE_DIRECTIVE_NAME),
351            arguments: vec![
352                InputValueDefinition {
353                    description: None,
354                    name: name!("name"),
355                    ty: ty!(String!).into(),
356                    default_value: None,
357                    directives: Default::default(),
358                }
359                .into(),
360            ],
361            repeatable: true,
362            locations: vec![DirectiveLocation::Schema],
363        }
364    }
365
366    /// directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION
367    fn context_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
368        DirectiveDefinition {
369            description: None,
370            name: alias.clone().unwrap_or(CONTEXT_DIRECTIVE_NAME),
371            arguments: vec![
372                InputValueDefinition {
373                    description: None,
374                    name: name!("name"),
375                    ty: ty!(String!).into(),
376                    default_value: None,
377                    directives: Default::default(),
378                }
379                .into(),
380            ],
381            repeatable: true,
382            locations: vec![
383                DirectiveLocation::Interface,
384                DirectiveLocation::Object,
385                DirectiveLocation::Union,
386            ],
387        }
388    }
389
390    /// directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
391    fn key_directive_definition(
392        &self,
393        alias: &Option<Name>,
394    ) -> Result<DirectiveDefinition, FederationSpecError> {
395        Ok(DirectiveDefinition {
396            description: None,
397            name: alias.clone().unwrap_or(KEY_DIRECTIVE_NAME),
398            arguments: vec![
399                self.fields_argument_definition()?.into(),
400                InputValueDefinition {
401                    description: None,
402                    name: name!("resolvable"),
403                    ty: ty!(Boolean).into(),
404                    default_value: Some(true.into()),
405                    directives: Default::default(),
406                }
407                .into(),
408            ],
409            repeatable: true,
410            locations: vec![DirectiveLocation::Object, DirectiveLocation::Interface],
411        })
412    }
413
414    /// directive @extends on OBJECT | INTERFACE
415    fn extends_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
416        DirectiveDefinition {
417            description: None,
418            name: alias.clone().unwrap_or(EXTENDS_DIRECTIVE_NAME),
419            arguments: Vec::new(),
420            repeatable: false,
421            locations: vec![DirectiveLocation::Object, DirectiveLocation::Interface],
422        }
423    }
424
425    /// directive @external on OBJECT | FIELD_DEFINITION
426    fn external_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
427        DirectiveDefinition {
428            description: None,
429            name: alias.clone().unwrap_or(EXTERNAL_DIRECTIVE_NAME),
430            arguments: Vec::new(),
431            repeatable: false,
432            locations: vec![
433                DirectiveLocation::Object,
434                DirectiveLocation::FieldDefinition,
435            ],
436        }
437    }
438
439    // The directive is named `@fromContex`. This is confusing for clippy, as
440    // `from` is a conventional prefix used in conversion methods, which do not
441    // take `self` as an argument. This function does **not** perform
442    // conversion, but extracts `@fromContext` directive definition.
443    /// directive @fromContext(field: ContextFieldValue) on ARGUMENT_DEFINITION
444    #[allow(clippy::wrong_self_convention)]
445    fn from_context_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
446        DirectiveDefinition {
447            description: None,
448            name: alias.clone().unwrap_or(FROM_CONTEXT_DIRECTIVE_NAME),
449            arguments: vec![
450                InputValueDefinition {
451                    description: None,
452                    name: name!("field"),
453                    ty: Type::Named(
454                        self.namespaced_type_name(&CONTEXTFIELDVALUE_SCALAR_NAME, false),
455                    )
456                    .into(),
457                    default_value: None,
458                    directives: Default::default(),
459                }
460                .into(),
461            ],
462            repeatable: false,
463            locations: vec![DirectiveLocation::ArgumentDefinition],
464        }
465    }
466
467    /// directive @inaccessible on
468    ///   | ARGUMENT_DEFINITION
469    ///   | ENUM
470    ///   | ENUM_VALUE
471    ///   | FIELD_DEFINITION
472    ///   | INPUT_FIELD_DEFINITION
473    ///   | INPUT_OBJECT
474    ///   | INTERFACE
475    ///   | OBJECT
476    ///   | SCALAR
477    ///   | UNION
478    fn inaccessible_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
479        DirectiveDefinition {
480            description: None,
481            name: alias.clone().unwrap_or(INACCESSIBLE_DIRECTIVE_NAME),
482            arguments: Vec::new(),
483            repeatable: false,
484            locations: vec![
485                DirectiveLocation::ArgumentDefinition,
486                DirectiveLocation::Enum,
487                DirectiveLocation::EnumValue,
488                DirectiveLocation::FieldDefinition,
489                DirectiveLocation::InputFieldDefinition,
490                DirectiveLocation::InputObject,
491                DirectiveLocation::Interface,
492                DirectiveLocation::Object,
493                DirectiveLocation::Scalar,
494                DirectiveLocation::Union,
495            ],
496        }
497    }
498
499    /// directive @interfaceObject on OBJECT
500    fn interface_object_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
501        DirectiveDefinition {
502            description: None,
503            name: alias.clone().unwrap_or(INTF_OBJECT_DIRECTIVE_NAME),
504            arguments: Vec::new(),
505            repeatable: false,
506            locations: vec![DirectiveLocation::Object],
507        }
508    }
509
510    /// directive @override(from: String!) on FIELD_DEFINITION
511    fn override_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
512        DirectiveDefinition {
513            description: None,
514            name: alias.clone().unwrap_or(OVERRIDE_DIRECTIVE_NAME),
515            arguments: vec![
516                InputValueDefinition {
517                    description: None,
518                    name: name!("from"),
519                    ty: ty!(String!).into(),
520                    default_value: None,
521                    directives: Default::default(),
522                }
523                .into(),
524            ],
525            repeatable: false,
526            locations: vec![DirectiveLocation::FieldDefinition],
527        }
528    }
529
530    /// directive @provides(fields: FieldSet!) on FIELD_DEFINITION
531    fn provides_directive_definition(
532        &self,
533        alias: &Option<Name>,
534    ) -> Result<DirectiveDefinition, FederationSpecError> {
535        Ok(DirectiveDefinition {
536            description: None,
537            name: alias.clone().unwrap_or(PROVIDES_DIRECTIVE_NAME),
538            arguments: vec![self.fields_argument_definition()?.into()],
539            repeatable: false,
540            locations: vec![DirectiveLocation::FieldDefinition],
541        })
542    }
543
544    /// directive @requires(fields: FieldSet!) on FIELD_DEFINITION
545    fn requires_directive_definition(
546        &self,
547        alias: &Option<Name>,
548    ) -> Result<DirectiveDefinition, FederationSpecError> {
549        Ok(DirectiveDefinition {
550            description: None,
551            name: alias.clone().unwrap_or(REQUIRES_DIRECTIVE_NAME),
552            arguments: vec![self.fields_argument_definition()?.into()],
553            repeatable: false,
554            locations: vec![DirectiveLocation::FieldDefinition],
555        })
556    }
557
558    /// directive @shareable repeatable on FIELD_DEFINITION | OBJECT
559    fn shareable_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
560        DirectiveDefinition {
561            description: None,
562            name: alias.clone().unwrap_or(SHAREABLE_DIRECTIVE_NAME),
563            arguments: Vec::new(),
564            repeatable: true,
565            locations: vec![
566                DirectiveLocation::FieldDefinition,
567                DirectiveLocation::Object,
568            ],
569        }
570    }
571
572    /// directive @tag(name: String!) repeatable on
573    ///   | ARGUMENT_DEFINITION
574    ///   | ENUM
575    ///   | ENUM_VALUE
576    ///   | FIELD_DEFINITION
577    ///   | INPUT_FIELD_DEFINITION
578    ///   | INPUT_OBJECT
579    ///   | INTERFACE
580    ///   | OBJECT
581    ///   | SCALAR
582    ///   | UNION
583    fn tag_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
584        DirectiveDefinition {
585            description: None,
586            name: alias.clone().unwrap_or(TAG_DIRECTIVE_NAME),
587            arguments: vec![
588                InputValueDefinition {
589                    description: None,
590                    name: name!("name"),
591                    ty: ty!(String!).into(),
592                    default_value: None,
593                    directives: Default::default(),
594                }
595                .into(),
596            ],
597            repeatable: true,
598            locations: vec![
599                DirectiveLocation::ArgumentDefinition,
600                DirectiveLocation::Enum,
601                DirectiveLocation::EnumValue,
602                DirectiveLocation::FieldDefinition,
603                DirectiveLocation::InputFieldDefinition,
604                DirectiveLocation::InputObject,
605                DirectiveLocation::Interface,
606                DirectiveLocation::Object,
607                DirectiveLocation::Scalar,
608                DirectiveLocation::Union,
609            ],
610        }
611    }
612
613    pub(crate) fn any_scalar_definition(&self) -> ExtendedType {
614        let any_scalar = ScalarType {
615            description: None,
616            name: ANY_SCALAR_NAME,
617            directives: Default::default(),
618        };
619        ExtendedType::Scalar(Node::new(any_scalar))
620    }
621
622    pub(crate) fn entity_union_definition(
623        &self,
624        entities: IndexSet<ComponentName>,
625    ) -> ExtendedType {
626        let service_type = UnionType {
627            description: None,
628            name: ENTITY_UNION_NAME,
629            directives: Default::default(),
630            members: entities,
631        };
632        ExtendedType::Union(Node::new(service_type))
633    }
634    pub(crate) fn service_object_type_definition(&self) -> ExtendedType {
635        let mut service_type = ObjectType {
636            description: None,
637            name: SERVICE_TYPE,
638            directives: Default::default(),
639            fields: IndexMap::default(),
640            implements_interfaces: IndexSet::default(),
641        };
642        service_type.fields.insert(
643            name!("_sdl"),
644            Component::new(FieldDefinition {
645                name: name!("_sdl"),
646                description: None,
647                directives: Default::default(),
648                arguments: Vec::new(),
649                ty: ty!(String),
650            }),
651        );
652        ExtendedType::Object(Node::new(service_type))
653    }
654
655    pub(crate) fn entities_query_field(&self) -> Component<FieldDefinition> {
656        Component::new(FieldDefinition {
657            name: ENTITIES_QUERY,
658            description: None,
659            directives: Default::default(),
660            arguments: vec![Node::new(InputValueDefinition {
661                name: name!("representations"),
662                description: None,
663                directives: Default::default(),
664                ty: Node::new(Type::NonNullList(Box::new(Type::NonNullNamed(
665                    ANY_SCALAR_NAME,
666                )))),
667                default_value: None,
668            })],
669            ty: Type::NonNullList(Box::new(Type::Named(ENTITY_UNION_NAME))),
670        })
671    }
672
673    pub(crate) fn service_sdl_query_field(&self) -> Component<FieldDefinition> {
674        Component::new(FieldDefinition {
675            name: SERVICE_SDL_QUERY,
676            description: None,
677            directives: Default::default(),
678            arguments: Vec::new(),
679            ty: Type::NonNullNamed(SERVICE_TYPE),
680        })
681    }
682}
683
684impl LinkSpecDefinitions {
685    pub fn new(link: Link) -> Self {
686        let import_scalar_name = link.type_name_in_schema(&DEFAULT_IMPORT_SCALAR_NAME);
687        let purpose_enum_name = link.type_name_in_schema(&DEFAULT_PURPOSE_ENUM_NAME);
688        Self {
689            link,
690            import_scalar_name,
691            purpose_enum_name,
692        }
693    }
694
695    ///   scalar Import
696    pub fn import_scalar_definition(&self, name: Name) -> ScalarType {
697        ScalarType {
698            description: None,
699            name,
700            directives: Default::default(),
701        }
702    }
703
704    ///   enum link__Purpose {
705    ///     SECURITY
706    ///     EXECUTION
707    ///   }
708    pub fn link_purpose_enum_definition(&self, name: Name) -> EnumType {
709        EnumType {
710            description: None,
711            name,
712            directives: Default::default(),
713            values: [
714                (
715                    name!("SECURITY"),
716                    EnumValueDefinition {
717                        description: None,
718                        value: name!("SECURITY"),
719                        directives: Default::default(),
720                    }
721                    .into(),
722                ),
723                (
724                    name!("EXECUTION"),
725                    EnumValueDefinition {
726                        description: None,
727                        value: name!("EXECUTION"),
728                        directives: Default::default(),
729                    }
730                    .into(),
731                ),
732            ]
733            .into_iter()
734            .collect(),
735        }
736    }
737
738    ///   directive @link(url: String!, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
739    pub fn link_directive_definition(&self) -> Result<DirectiveDefinition, FederationSpecError> {
740        Ok(DirectiveDefinition {
741            description: None,
742            name: DEFAULT_LINK_NAME,
743            arguments: vec![
744                InputValueDefinition {
745                    description: None,
746                    name: name!("url"),
747                    ty: ty!(String!).into(),
748                    default_value: None,
749                    directives: Default::default(),
750                }
751                .into(),
752                InputValueDefinition {
753                    description: None,
754                    name: name!("as"),
755                    ty: ty!(String).into(),
756                    default_value: None,
757                    directives: Default::default(),
758                }
759                .into(),
760                InputValueDefinition {
761                    description: None,
762                    name: name!("import"),
763                    ty: Type::Named(self.import_scalar_name.clone()).list().into(),
764                    default_value: None,
765                    directives: Default::default(),
766                }
767                .into(),
768                InputValueDefinition {
769                    description: None,
770                    name: name!("for"),
771                    ty: Type::Named(self.purpose_enum_name.clone()).into(),
772                    default_value: None,
773                    directives: Default::default(),
774                }
775                .into(),
776            ],
777            repeatable: true,
778            locations: vec![DirectiveLocation::Schema],
779        })
780    }
781}
782
783impl Default for LinkSpecDefinitions {
784    fn default() -> Self {
785        let link = Link {
786            url: Url {
787                identity: Identity::link_identity(),
788                version: Version { major: 1, minor: 0 },
789            },
790            imports: vec![Arc::new(Import {
791                element: name!("Import"),
792                is_directive: false,
793                alias: None,
794            })],
795            purpose: None,
796            spec_alias: None,
797        };
798        Self::new(link)
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use crate::link::spec::APOLLO_SPEC_DOMAIN;
806    use crate::link::spec::Identity;
807
808    // TODO: we should define this as part as some more generic "FederationSpec" definition, but need
809    // to define the ground work for that in `apollo-at-link` first.
810    fn federation_link_identity() -> Identity {
811        Identity {
812            domain: APOLLO_SPEC_DOMAIN.to_string(),
813            name: name!("federation"),
814        }
815    }
816
817    #[test]
818    fn handle_unsupported_federation_version() {
819        FederationSpecDefinitions::from_link(Link {
820            url: Url {
821                identity: federation_link_identity(),
822                version: Version {
823                    major: 99,
824                    minor: 99,
825                },
826            },
827            spec_alias: None,
828            imports: vec![],
829            purpose: None,
830        })
831        .expect_err("federation version 99 is not yet supported");
832    }
833}