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