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        })
277    }
278
279    pub fn namespaced_type_name(&self, name: &Name, is_directive: bool) -> Name {
280        if is_directive {
281            self.link.directive_name_in_schema(name)
282        } else {
283            self.link.type_name_in_schema(name)
284        }
285    }
286
287    pub fn directive_definition(
288        &self,
289        name: &Name,
290        alias: &Option<Name>,
291    ) -> Result<DirectiveDefinition, FederationSpecError> {
292        // TODO: `Name` has custom `PartialEq` and `Eq` impl so Clippy warns it should
293        // not be used in pattern matching (as some future Rust version will likely turn this into
294        // a hard error). We resort instead to indexing into a static IndexMap to get an enum, which
295        // can be used in a match.
296        let Some(enum_name) = FEDERATION_DIRECTIVE_NAMES_TO_ENUM.get(name) else {
297            return Err(UnsupportedFederationDirective(name.to_string()));
298        };
299        Ok(match enum_name {
300            FederationDirectiveName::Compose => self.compose_directive_definition(alias),
301            FederationDirectiveName::Context => self.context_directive_definition(alias),
302            FederationDirectiveName::Key => self.key_directive_definition(alias)?,
303            FederationDirectiveName::Extends => self.extends_directive_definition(alias),
304            FederationDirectiveName::External => self.external_directive_definition(alias),
305            FederationDirectiveName::FromContext => self.from_context_directive_definition(alias),
306            FederationDirectiveName::Inaccessible => self.inaccessible_directive_definition(alias),
307            FederationDirectiveName::IntfObject => {
308                self.interface_object_directive_definition(alias)
309            }
310            FederationDirectiveName::Override => self.override_directive_definition(alias),
311            FederationDirectiveName::Provides => self.provides_directive_definition(alias)?,
312            FederationDirectiveName::Requires => self.requires_directive_definition(alias)?,
313            FederationDirectiveName::Shareable => self.shareable_directive_definition(alias),
314            FederationDirectiveName::Tag => self.tag_directive_definition(alias),
315        })
316    }
317
318    /// scalar FieldSet
319    pub fn fieldset_scalar_definition(&self, name: Name) -> ScalarType {
320        ScalarType {
321            description: None,
322            name,
323            directives: Default::default(),
324        }
325    }
326
327    /// scalar ContextFieldValue
328    pub fn contextfieldvalue_scalar_definition(&self, alias: &Option<Name>) -> ScalarType {
329        ScalarType {
330            description: None,
331            name: alias.clone().unwrap_or(CONTEXTFIELDVALUE_SCALAR_NAME),
332            directives: Default::default(),
333        }
334    }
335
336    fn fields_argument_definition(&self) -> Result<InputValueDefinition, FederationSpecError> {
337        Ok(InputValueDefinition {
338            description: None,
339            name: name!("fields"),
340            ty: Type::Named(self.namespaced_type_name(&FIELDSET_SCALAR_NAME, false))
341                .non_null()
342                .into(),
343            default_value: None,
344            directives: Default::default(),
345        })
346    }
347
348    /// directive @composeDirective(name: String!) repeatable on SCHEMA
349    fn compose_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
350        DirectiveDefinition {
351            description: None,
352            name: alias.clone().unwrap_or(COMPOSE_DIRECTIVE_NAME),
353            arguments: vec![
354                InputValueDefinition {
355                    description: None,
356                    name: name!("name"),
357                    ty: ty!(String).into(),
358                    default_value: None,
359                    directives: Default::default(),
360                }
361                .into(),
362            ],
363            repeatable: true,
364            locations: vec![DirectiveLocation::Schema],
365        }
366    }
367
368    /// directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION
369    fn context_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
370        DirectiveDefinition {
371            description: None,
372            name: alias.clone().unwrap_or(CONTEXT_DIRECTIVE_NAME),
373            arguments: vec![
374                InputValueDefinition {
375                    description: None,
376                    name: name!("name"),
377                    ty: ty!(String!).into(),
378                    default_value: None,
379                    directives: Default::default(),
380                }
381                .into(),
382            ],
383            repeatable: true,
384            locations: vec![
385                DirectiveLocation::Interface,
386                DirectiveLocation::Object,
387                DirectiveLocation::Union,
388            ],
389        }
390    }
391
392    /// directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
393    fn key_directive_definition(
394        &self,
395        alias: &Option<Name>,
396    ) -> Result<DirectiveDefinition, FederationSpecError> {
397        Ok(DirectiveDefinition {
398            description: None,
399            name: alias.clone().unwrap_or(KEY_DIRECTIVE_NAME),
400            arguments: vec![
401                self.fields_argument_definition()?.into(),
402                InputValueDefinition {
403                    description: None,
404                    name: name!("resolvable"),
405                    ty: ty!(Boolean).into(),
406                    default_value: Some(true.into()),
407                    directives: Default::default(),
408                }
409                .into(),
410            ],
411            repeatable: true,
412            locations: vec![DirectiveLocation::Object, DirectiveLocation::Interface],
413        })
414    }
415
416    /// directive @extends on OBJECT | INTERFACE
417    fn extends_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
418        DirectiveDefinition {
419            description: None,
420            name: alias.clone().unwrap_or(EXTENDS_DIRECTIVE_NAME),
421            arguments: Vec::new(),
422            repeatable: false,
423            locations: vec![DirectiveLocation::Object, DirectiveLocation::Interface],
424        }
425    }
426
427    /// directive @external on OBJECT | FIELD_DEFINITION
428    fn external_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
429        DirectiveDefinition {
430            description: None,
431            name: alias.clone().unwrap_or(EXTERNAL_DIRECTIVE_NAME),
432            arguments: Vec::new(),
433            repeatable: false,
434            locations: vec![
435                DirectiveLocation::Object,
436                DirectiveLocation::FieldDefinition,
437            ],
438        }
439    }
440
441    // The directive is named `@fromContext`. This is confusing for clippy, as
442    // `from` is a conventional prefix used in conversion methods, which do not
443    // take `self` as an argument. This function does **not** perform
444    // conversion, but extracts `@fromContext` directive definition.
445    /// directive @fromContext(field: ContextFieldValue) on ARGUMENT_DEFINITION
446    #[allow(clippy::wrong_self_convention)]
447    fn from_context_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
448        DirectiveDefinition {
449            description: None,
450            name: alias.clone().unwrap_or(FROM_CONTEXT_DIRECTIVE_NAME),
451            arguments: vec![
452                InputValueDefinition {
453                    description: None,
454                    name: name!("field"),
455                    ty: Type::Named(
456                        self.namespaced_type_name(&CONTEXTFIELDVALUE_SCALAR_NAME, false),
457                    )
458                    .into(),
459                    default_value: None,
460                    directives: Default::default(),
461                }
462                .into(),
463            ],
464            repeatable: false,
465            locations: vec![DirectiveLocation::ArgumentDefinition],
466        }
467    }
468
469    /// directive @inaccessible on
470    ///   | ARGUMENT_DEFINITION
471    ///   | ENUM
472    ///   | ENUM_VALUE
473    ///   | FIELD_DEFINITION
474    ///   | INPUT_FIELD_DEFINITION
475    ///   | INPUT_OBJECT
476    ///   | INTERFACE
477    ///   | OBJECT
478    ///   | SCALAR
479    ///   | UNION
480    fn inaccessible_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
481        DirectiveDefinition {
482            description: None,
483            name: alias.clone().unwrap_or(INACCESSIBLE_DIRECTIVE_NAME),
484            arguments: Vec::new(),
485            repeatable: false,
486            locations: vec![
487                DirectiveLocation::ArgumentDefinition,
488                DirectiveLocation::Enum,
489                DirectiveLocation::EnumValue,
490                DirectiveLocation::FieldDefinition,
491                DirectiveLocation::InputFieldDefinition,
492                DirectiveLocation::InputObject,
493                DirectiveLocation::Interface,
494                DirectiveLocation::Object,
495                DirectiveLocation::Scalar,
496                DirectiveLocation::Union,
497            ],
498        }
499    }
500
501    /// directive @interfaceObject on OBJECT
502    fn interface_object_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
503        DirectiveDefinition {
504            description: None,
505            name: alias.clone().unwrap_or(INTF_OBJECT_DIRECTIVE_NAME),
506            arguments: Vec::new(),
507            repeatable: false,
508            locations: vec![DirectiveLocation::Object],
509        }
510    }
511
512    /// directive @override(from: String!) on FIELD_DEFINITION
513    fn override_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
514        DirectiveDefinition {
515            description: None,
516            name: alias.clone().unwrap_or(OVERRIDE_DIRECTIVE_NAME),
517            arguments: vec![
518                InputValueDefinition {
519                    description: None,
520                    name: name!("from"),
521                    ty: ty!(String!).into(),
522                    default_value: None,
523                    directives: Default::default(),
524                }
525                .into(),
526            ],
527            repeatable: false,
528            locations: vec![DirectiveLocation::FieldDefinition],
529        }
530    }
531
532    /// directive @provides(fields: FieldSet!) on FIELD_DEFINITION
533    fn provides_directive_definition(
534        &self,
535        alias: &Option<Name>,
536    ) -> Result<DirectiveDefinition, FederationSpecError> {
537        Ok(DirectiveDefinition {
538            description: None,
539            name: alias.clone().unwrap_or(PROVIDES_DIRECTIVE_NAME),
540            arguments: vec![self.fields_argument_definition()?.into()],
541            repeatable: false,
542            locations: vec![DirectiveLocation::FieldDefinition],
543        })
544    }
545
546    /// directive @requires(fields: FieldSet!) on FIELD_DEFINITION
547    fn requires_directive_definition(
548        &self,
549        alias: &Option<Name>,
550    ) -> Result<DirectiveDefinition, FederationSpecError> {
551        Ok(DirectiveDefinition {
552            description: None,
553            name: alias.clone().unwrap_or(REQUIRES_DIRECTIVE_NAME),
554            arguments: vec![self.fields_argument_definition()?.into()],
555            repeatable: false,
556            locations: vec![DirectiveLocation::FieldDefinition],
557        })
558    }
559
560    /// directive @shareable repeatable on FIELD_DEFINITION | OBJECT
561    fn shareable_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
562        DirectiveDefinition {
563            description: None,
564            name: alias.clone().unwrap_or(SHAREABLE_DIRECTIVE_NAME),
565            arguments: Vec::new(),
566            repeatable: true,
567            locations: vec![
568                DirectiveLocation::FieldDefinition,
569                DirectiveLocation::Object,
570            ],
571        }
572    }
573
574    /// directive @tag(name: String!) repeatable on
575    ///   | ARGUMENT_DEFINITION
576    ///   | ENUM
577    ///   | ENUM_VALUE
578    ///   | FIELD_DEFINITION
579    ///   | INPUT_FIELD_DEFINITION
580    ///   | INPUT_OBJECT
581    ///   | INTERFACE
582    ///   | OBJECT
583    ///   | SCALAR
584    ///   | UNION
585    fn tag_directive_definition(&self, alias: &Option<Name>) -> DirectiveDefinition {
586        DirectiveDefinition {
587            description: None,
588            name: alias.clone().unwrap_or(TAG_DIRECTIVE_NAME),
589            arguments: vec![
590                InputValueDefinition {
591                    description: None,
592                    name: name!("name"),
593                    ty: ty!(String!).into(),
594                    default_value: None,
595                    directives: Default::default(),
596                }
597                .into(),
598            ],
599            repeatable: true,
600            locations: vec![
601                DirectiveLocation::ArgumentDefinition,
602                DirectiveLocation::Enum,
603                DirectiveLocation::EnumValue,
604                DirectiveLocation::FieldDefinition,
605                DirectiveLocation::InputFieldDefinition,
606                DirectiveLocation::InputObject,
607                DirectiveLocation::Interface,
608                DirectiveLocation::Object,
609                DirectiveLocation::Scalar,
610                DirectiveLocation::Union,
611            ],
612        }
613    }
614
615    pub(crate) fn any_scalar_definition(&self) -> ExtendedType {
616        let any_scalar = ScalarType {
617            description: None,
618            name: ANY_SCALAR_NAME,
619            directives: Default::default(),
620        };
621        ExtendedType::Scalar(Node::new(any_scalar))
622    }
623
624    pub(crate) fn entity_union_definition(
625        &self,
626        entities: IndexSet<ComponentName>,
627    ) -> ExtendedType {
628        let service_type = UnionType {
629            description: None,
630            name: ENTITY_UNION_NAME,
631            directives: Default::default(),
632            members: entities,
633        };
634        ExtendedType::Union(Node::new(service_type))
635    }
636    pub(crate) fn service_object_type_definition(&self) -> ExtendedType {
637        let mut service_type = ObjectType {
638            description: None,
639            name: SERVICE_TYPE,
640            directives: Default::default(),
641            fields: IndexMap::default(),
642            implements_interfaces: IndexSet::default(),
643        };
644        service_type.fields.insert(
645            name!("_sdl"),
646            Component::new(FieldDefinition {
647                name: name!("_sdl"),
648                description: None,
649                directives: Default::default(),
650                arguments: Vec::new(),
651                ty: ty!(String),
652            }),
653        );
654        ExtendedType::Object(Node::new(service_type))
655    }
656
657    pub(crate) fn entities_query_field(&self) -> Component<FieldDefinition> {
658        Component::new(FieldDefinition {
659            name: ENTITIES_QUERY,
660            description: None,
661            directives: Default::default(),
662            arguments: vec![Node::new(InputValueDefinition {
663                name: name!("representations"),
664                description: None,
665                directives: Default::default(),
666                ty: Node::new(Type::NonNullList(Box::new(Type::NonNullNamed(
667                    ANY_SCALAR_NAME,
668                )))),
669                default_value: None,
670            })],
671            ty: Type::NonNullList(Box::new(Type::Named(ENTITY_UNION_NAME))),
672        })
673    }
674
675    pub(crate) fn service_sdl_query_field(&self) -> Component<FieldDefinition> {
676        Component::new(FieldDefinition {
677            name: SERVICE_SDL_QUERY,
678            description: None,
679            directives: Default::default(),
680            arguments: Vec::new(),
681            ty: Type::NonNullNamed(SERVICE_TYPE),
682        })
683    }
684}
685
686impl LinkSpecDefinitions {
687    pub fn new(link: Link) -> Self {
688        let import_scalar_name = link.type_name_in_schema(&DEFAULT_IMPORT_SCALAR_NAME);
689        let purpose_enum_name = link.type_name_in_schema(&DEFAULT_PURPOSE_ENUM_NAME);
690        Self {
691            link,
692            import_scalar_name,
693            purpose_enum_name,
694        }
695    }
696
697    ///   scalar Import
698    pub fn import_scalar_definition(&self, name: Name) -> ScalarType {
699        ScalarType {
700            description: None,
701            name,
702            directives: Default::default(),
703        }
704    }
705
706    ///   enum link__Purpose {
707    ///     SECURITY
708    ///     EXECUTION
709    ///   }
710    pub fn link_purpose_enum_definition(&self, name: Name) -> EnumType {
711        EnumType {
712            description: None,
713            name,
714            directives: Default::default(),
715            values: [
716                (
717                    name!("SECURITY"),
718                    EnumValueDefinition {
719                        description: None,
720                        value: name!("SECURITY"),
721                        directives: Default::default(),
722                    }
723                    .into(),
724                ),
725                (
726                    name!("EXECUTION"),
727                    EnumValueDefinition {
728                        description: None,
729                        value: name!("EXECUTION"),
730                        directives: Default::default(),
731                    }
732                    .into(),
733                ),
734            ]
735            .into_iter()
736            .collect(),
737        }
738    }
739
740    ///   directive @link(url: String!, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
741    pub fn link_directive_definition(&self) -> Result<DirectiveDefinition, FederationSpecError> {
742        Ok(DirectiveDefinition {
743            description: None,
744            name: DEFAULT_LINK_NAME,
745            arguments: vec![
746                InputValueDefinition {
747                    description: None,
748                    name: name!("url"),
749                    ty: ty!(String!).into(),
750                    default_value: None,
751                    directives: Default::default(),
752                }
753                .into(),
754                InputValueDefinition {
755                    description: None,
756                    name: name!("as"),
757                    ty: ty!(String).into(),
758                    default_value: None,
759                    directives: Default::default(),
760                }
761                .into(),
762                InputValueDefinition {
763                    description: None,
764                    name: name!("import"),
765                    ty: Type::Named(self.import_scalar_name.clone()).list().into(),
766                    default_value: None,
767                    directives: Default::default(),
768                }
769                .into(),
770                InputValueDefinition {
771                    description: None,
772                    name: name!("for"),
773                    ty: Type::Named(self.purpose_enum_name.clone()).into(),
774                    default_value: None,
775                    directives: Default::default(),
776                }
777                .into(),
778            ],
779            repeatable: true,
780            locations: vec![DirectiveLocation::Schema],
781        })
782    }
783}
784
785impl Default for LinkSpecDefinitions {
786    fn default() -> Self {
787        let link = Link {
788            url: Url {
789                identity: Identity::link_identity(),
790                version: Version { major: 1, minor: 0 },
791            },
792            imports: vec![Arc::new(Import {
793                element: name!("Import"),
794                is_directive: false,
795                alias: None,
796            })],
797            purpose: None,
798            spec_alias: None,
799        };
800        Self::new(link)
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use crate::link::spec::APOLLO_SPEC_DOMAIN;
808    use crate::link::spec::Identity;
809
810    // TODO: we should define this as part as some more generic "FederationSpec" definition, but need
811    // to define the ground work for that in `apollo-at-link` first.
812    fn federation_link_identity() -> Identity {
813        Identity {
814            domain: APOLLO_SPEC_DOMAIN.to_string(),
815            name: name!("federation"),
816        }
817    }
818
819    #[test]
820    fn handle_unsupported_federation_version() {
821        FederationSpecDefinitions::from_link(Link {
822            url: Url {
823                identity: federation_link_identity(),
824                version: Version {
825                    major: 99,
826                    minor: 99,
827                },
828            },
829            spec_alias: None,
830            imports: vec![],
831            purpose: None,
832        })
833        .expect_err("federation version 99 is not yet supported");
834    }
835}