Skip to main content

apollo_federation/link/
link_spec_definition.rs

1use std::sync::Arc;
2use std::sync::LazyLock;
3
4use apollo_compiler::Name;
5use apollo_compiler::Node;
6use apollo_compiler::Schema;
7use apollo_compiler::ast::Argument;
8use apollo_compiler::ast::Directive;
9use apollo_compiler::ast::DirectiveDefinition;
10use apollo_compiler::ast::DirectiveLocation;
11use apollo_compiler::ast::Type;
12use apollo_compiler::ast::Value;
13use apollo_compiler::name;
14use apollo_compiler::schema::Component;
15use apollo_compiler::ty;
16use itertools::Itertools;
17
18use crate::bail;
19use crate::error::FederationError;
20use crate::error::MultiTry;
21use crate::error::MultiTryAll;
22use crate::error::SingleFederationError;
23use crate::link::Import;
24use crate::link::Link;
25use crate::link::Purpose;
26use crate::link::argument::directive_optional_list_argument;
27use crate::link::argument::directive_optional_string_argument;
28use crate::link::spec::Identity;
29use crate::link::spec::Url;
30use crate::link::spec::Version;
31use crate::link::spec_definition::SpecDefinition;
32use crate::link::spec_definition::SpecDefinitions;
33use crate::schema::FederationSchema;
34use crate::schema::SchemaElement;
35use crate::schema::position::SchemaDefinitionPosition;
36use crate::schema::type_and_directive_specification::ArgumentSpecification;
37use crate::schema::type_and_directive_specification::DirectiveArgumentSpecification;
38use crate::schema::type_and_directive_specification::DirectiveSpecification;
39use crate::schema::type_and_directive_specification::EnumTypeSpecification;
40use crate::schema::type_and_directive_specification::EnumValueSpecification;
41use crate::schema::type_and_directive_specification::ScalarTypeSpecification;
42use crate::schema::type_and_directive_specification::TypeAndDirectiveSpecification;
43
44pub(crate) const LINK_DIRECTIVE_NAME_IN_SPEC: Name = name!("link");
45pub(crate) const LINK_DIRECTIVE_AS_ARGUMENT_NAME: Name = name!("as");
46pub(crate) const LINK_DIRECTIVE_URL_ARGUMENT_NAME: Name = name!("url");
47pub(crate) const LINK_DIRECTIVE_FOR_ARGUMENT_NAME: Name = name!("for");
48pub(crate) const LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME: Name = name!("import");
49pub(crate) const LINK_DIRECTIVE_FEATURE_ARGUMENT_NAME: Name = name!("feature"); // Fed 1's `url` argument
50
51pub(crate) const IMPORT_TYPE_NAME_IN_SPEC: Name = name!("Import");
52pub(crate) const IMPORT_TYPE_NAME_FIELD_NAME: Name = name!("name");
53pub(crate) const IMPORT_TYPE_AS_FIELD_NAME: Name = name!("as");
54
55pub(crate) const PURPOSE_TYPE_NAME_IN_SPEC: Name = name!("Purpose");
56
57impl TryFrom<&Value> for Purpose {
58    type Error = FederationError;
59
60    fn try_from(value: &Value) -> Result<Self, Self::Error> {
61        let Some(purpose) = value.as_enum() else {
62            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
63                message: format!(
64                    r#"@link(for:) argument `{}` must be an enum value"#,
65                    value.serialize().no_indent()
66                ),
67            }
68            .into());
69        };
70        match purpose.as_str() {
71            "SECURITY" => Ok(Purpose::SECURITY),
72            "EXECUTION" => Ok(Purpose::EXECUTION),
73            _ => Err(SingleFederationError::InvalidLinkDirectiveUsage {
74                message: format!(
75                    r#"@link(for:) argument `{}` is not a known enum value"#,
76                    value.serialize().no_indent()
77                ),
78            }
79            .into()),
80        }
81    }
82}
83
84impl From<&Purpose> for Value {
85    fn from(value: &Purpose) -> Self {
86        match value {
87            Purpose::SECURITY => Value::Enum(name!("SECURITY")),
88            Purpose::EXECUTION => Value::Enum(name!("EXECUTION")),
89        }
90    }
91}
92
93impl TryFrom<&Value> for Import {
94    type Error = FederationError;
95
96    fn try_from(value: &Value) -> Result<Self, Self::Error> {
97        match value {
98            Value::String(str) => {
99                if let Some(directive_name) = str.strip_prefix('@') {
100                    Ok(Import {
101                        element: Name::new(directive_name).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
102                            message: format!(r#"`{}` in @link(import:) argument is not a valid GraphQL name"#, value.serialize().no_indent()),
103                        })?,
104                        is_directive: true,
105                        alias: None,
106                    })
107                } else {
108                    Ok(Import {
109                        element: Name::new(str).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
110                            message: format!(r#"`{}` in @link(import:) argument is not a valid GraphQL name"#, value.serialize().no_indent()),
111                        })?,
112                        is_directive: false,
113                        alias: None,
114                    })
115                }
116            }
117            Value::Object(fields) => {
118                let mut name: Option<&str> = None;
119                let mut alias: Option<&str> = None;
120                for (k, v) in fields {
121                    match k.as_str() {
122                        "name" => {
123                            name = Some(v.as_str().ok_or_else(|| SingleFederationError::InvalidLinkDirectiveUsage {
124                                message: format!(r#"For `{}` in @link(import:) argument, value for field "name" must be a string"#, value.serialize().no_indent()),
125                            })?)
126                        },
127                        "as" => {
128                            alias = Some(v.as_str().ok_or_else(|| SingleFederationError::InvalidLinkDirectiveUsage {
129                                message: format!(r#"For `{}` in @link(import:) argument, value for field "as" must be a string"#, value.serialize().no_indent()),
130                            })?)
131                        },
132                        _ => {
133                            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
134                                message: format!(r#"For `{}` in @link(import:) argument, field "{k}" is not a known field"#, value.serialize().no_indent()),
135                            }.into());
136                        }
137                    }
138                }
139                let Some(element) = name else {
140                    return Err(SingleFederationError::InvalidLinkDirectiveUsage {
141                        message: format!(r#"For `{}` in @link(import:) argument, missing required field "name""#, value.serialize().no_indent()),
142                    }.into());
143                };
144                if let Some(directive_name) = element.strip_prefix('@') {
145                    if let Some(alias_str) = alias.as_ref() {
146                        let Some(alias_str) = alias_str.strip_prefix('@') else {
147                            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
148                                message: format!(r#"For `{}` in @link(import:) argument, value for field "as" must start with "@" since value for field "name" does ("@" indicates a directive import)"#, value.serialize().no_indent()),
149                            }.into());
150                        };
151                        alias = Some(alias_str);
152                    }
153                    Ok(Import {
154                        element: Name::new(directive_name).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
155                            message: format!(r#"For `{}` in @link(import:) argument, value for field "name" is not a valid GraphQL name"#, value.serialize().no_indent()),
156                        })?,
157                        is_directive: true,
158                        alias: alias.map(|alias| Name::new(alias).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
159                            message: format!(r#"For `{}` in @link(import:) argument, value for field "as" is not a valid GraphQL name"#, value.serialize().no_indent()),
160                        })).transpose()?,
161                    })
162                } else {
163                    if let Some(alias) = &alias
164                        && alias.starts_with('@')
165                    {
166                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
167                            message: format!(r#"For `{}` in @link(import:) argument, value for field "as" must not start with "@" since value for field "name" does not ("@" indicates a directive import)"#, value.serialize().no_indent()),
168                        }.into());
169                    }
170                    Ok(Import {
171                        element: Name::new(element).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
172                            message: format!(r#"For `{}` in @link(import:) argument, value for field "name" is not a valid GraphQL name"#, value.serialize().no_indent()),
173                        })?,
174                        is_directive: false,
175                        alias: alias.map(|alias| Name::new(alias).map_err(|_| SingleFederationError::InvalidLinkDirectiveUsage {
176                            message: format!(r#"For `{}` in @link(import:) argument, value for field "as" is not a valid GraphQL name"#, value.serialize().no_indent()),
177                        })).transpose()?,
178                    })
179                }
180            }
181            _ => Err(SingleFederationError::InvalidLinkDirectiveUsage {
182                message: format!(r#"`{}` in @link(import:) argument must either be a string `"<importedElement>"` or an object `{{ name: "<importedElement>", as: "<alias>" }}`"#, value.serialize().no_indent()),
183            }.into()),
184        }
185    }
186}
187
188impl From<&Import> for Value {
189    fn from(value: &Import) -> Self {
190        let element_string = value.element_name_in_spec().to_string();
191
192        if value.alias.is_some() {
193            let alias_string = value.element_name_in_schema().to_string();
194            Value::Object(vec![
195                (
196                    IMPORT_TYPE_NAME_FIELD_NAME,
197                    Node::new(Value::String(element_string)),
198                ),
199                (
200                    IMPORT_TYPE_AS_FIELD_NAME,
201                    Node::new(Value::String(alias_string)),
202                ),
203            ])
204        } else {
205            Value::String(element_string)
206        }
207    }
208}
209
210#[derive(Debug)]
211pub(crate) struct LinkSpecDefinition {
212    url: Url,
213    name: Name,
214    minimum_federation_version: Version,
215}
216
217impl LinkSpecDefinition {
218    pub(crate) fn new(
219        version: Version,
220        minimum_federation_version: Version,
221        is_link: bool,
222    ) -> Self {
223        Self {
224            url: Url {
225                identity: if is_link {
226                    Identity::link_identity()
227                } else {
228                    Identity::core_identity()
229                },
230                version,
231            },
232            name: if is_link {
233                Identity::LINK_NAME
234            } else {
235                Identity::CORE_NAME
236            },
237            minimum_federation_version,
238        }
239    }
240
241    pub(crate) fn name(&self) -> &Name {
242        &self.name
243    }
244
245    fn create_definition_argument_specifications(&self) -> Vec<DirectiveArgumentSpecification> {
246        let mut specs = vec![
247            DirectiveArgumentSpecification {
248                base_spec: ArgumentSpecification {
249                    name: self.url_arg_name(),
250                    get_type: |_, _| Ok(ty!(String)),
251                    default_value: None,
252                },
253                composition_strategy: None,
254            },
255            DirectiveArgumentSpecification {
256                base_spec: ArgumentSpecification {
257                    name: LINK_DIRECTIVE_AS_ARGUMENT_NAME,
258                    get_type: |_, _| Ok(ty!(String)),
259                    default_value: None,
260                },
261                composition_strategy: None,
262            },
263        ];
264        if self.supports_purpose() {
265            specs.push(DirectiveArgumentSpecification {
266                base_spec: ArgumentSpecification {
267                    name: LINK_DIRECTIVE_FOR_ARGUMENT_NAME,
268                    get_type: |_schema, link| {
269                        let Some(link) = link else {
270                            bail!(
271                                "Type {PURPOSE_TYPE_NAME_IN_SPEC} shouldn't be added without being attached to a @link spec"
272                            )
273                        };
274                        Ok(Type::Named(link.type_name_in_schema(&PURPOSE_TYPE_NAME_IN_SPEC)))
275                    },
276                    default_value: None,
277                },
278                composition_strategy: None,
279            });
280        }
281        if self.supports_import() {
282            specs.push(DirectiveArgumentSpecification {
283                base_spec: ArgumentSpecification {
284                    name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME,
285                    get_type: |_, link| {
286                        let Some(link) = link else {
287                            bail!(
288                                "Type {IMPORT_TYPE_NAME_IN_SPEC} shouldn't be added without being attached to a @link spec"
289                            )
290                        };
291                        Ok(Type::List(Box::new(Type::Named(
292                            link.type_name_in_schema(&IMPORT_TYPE_NAME_IN_SPEC),
293                        ))))
294                    },
295                    default_value: None,
296                },
297                composition_strategy: None,
298            });
299        }
300        specs
301    }
302
303    fn supports_purpose(&self) -> bool {
304        self.version().gt(&Version { major: 0, minor: 1 })
305    }
306
307    fn supports_import(&self) -> bool {
308        self.version().satisfies(&Version { major: 1, minor: 0 })
309    }
310
311    pub(crate) fn url_arg_name(&self) -> Name {
312        if self.name == Identity::CORE_NAME {
313            LINK_DIRECTIVE_FEATURE_ARGUMENT_NAME
314        } else {
315            LINK_DIRECTIVE_URL_ARGUMENT_NAME
316        }
317    }
318
319    pub(crate) fn link_from_directive(
320        &self,
321        directive: &Node<Directive>,
322        schema: &Schema,
323    ) -> Result<Link, FederationError> {
324        let url = if let Some(value) = directive.specified_argument_by_name(&self.url_arg_name()) {
325            value
326        } else {
327            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
328                message: format!(
329                    r#"`{}` missing required argument "{}""#,
330                    directive.serialize().no_indent(),
331                    self.url_arg_name(),
332                ),
333            }
334            .into());
335        };
336
337        let url = url
338            .as_str()
339            .ok_or_else(|| SingleFederationError::InvalidLinkDirectiveUsage {
340                message: format!(
341                    r#"@{}({}:) argument `{}` must be a string"#,
342                    self.name,
343                    self.url_arg_name(),
344                    url.serialize().no_indent()
345                ),
346            })?;
347        let url: Url = url.parse::<Url>()?;
348
349        let spec_alias = directive
350            .specified_argument_by_name("as")
351            .take_if(|arg| !arg.is_null())
352            .map(|arg| {
353                arg.as_str()
354                    .ok_or_else(|| SingleFederationError::InvalidLinkDirectiveUsage {
355                        message: format!(
356                            r#"@{}(as:) argument `{}` must be a string or null"#,
357                            self.name,
358                            arg.serialize().no_indent()
359                        ),
360                    })
361            })
362            .transpose()?
363            .map(Name::new)
364            .transpose()?;
365
366        let purpose = if self.supports_purpose() {
367            directive
368                .specified_argument_by_name("for")
369                .take_if(|arg| !arg.is_null())
370                .map(|arg| Purpose::try_from(arg.as_ref()))
371                .transpose()?
372        } else {
373            None
374        };
375
376        let imports = if self.supports_import() {
377            directive
378                .specified_argument_by_name("import")
379                .take_if(|arg| !arg.is_null())
380                .map(|arg| {
381                    // Note that list input coercion rules mandate that when the value is not
382                    // a list and not null then it is wrapped into a list of size one.
383                    if let Value::List(value) = arg.as_ref() {
384                        value
385                    } else {
386                        std::slice::from_ref(arg)
387                    }
388                })
389                .unwrap_or(&[])
390                .iter()
391                .map(|value| Ok(Arc::new(Import::try_from(value.as_ref())?)))
392                .collect::<Result<Vec<Arc<Import>>, FederationError>>()?
393        } else {
394            Default::default()
395        };
396
397        Ok(Link {
398            url,
399            spec_alias,
400            imports,
401            purpose,
402            line_column_range: directive.line_column_range(&schema.sources),
403        })
404    }
405
406    pub(crate) fn directive_from_link(&self, link: &Link) -> Directive {
407        let mut arguments = Vec::new();
408        arguments.push(Node::new(Argument {
409            name: self.url_arg_name().clone(),
410            value: Node::new(Value::String(link.url.to_string())),
411        }));
412        if let Some(spec_alias) = &link.spec_alias {
413            arguments.push(Node::new(Argument {
414                name: LINK_DIRECTIVE_AS_ARGUMENT_NAME.clone(),
415                value: Node::new(Value::String(spec_alias.to_string())),
416            }));
417        }
418        if self.supports_import() && !link.imports.is_empty() {
419            arguments.push(Node::new(Argument {
420                name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME.clone(),
421                value: Node::new(Value::List(
422                    link.imports
423                        .iter()
424                        .map(|import| Node::new(Value::from(import.as_ref())))
425                        .collect(),
426                )),
427            }));
428        }
429        if self.supports_purpose()
430            && let Some(purpose) = &link.purpose
431        {
432            arguments.push(Node::new(Argument {
433                name: LINK_DIRECTIVE_FOR_ARGUMENT_NAME.clone(),
434                value: Node::new(Value::from(purpose)),
435            }));
436        }
437        Directive {
438            name: self.name.clone(),
439            arguments,
440        }
441    }
442
443    /// Returns whether a given directive is the @link or @core directive that imports the @link or
444    /// @core spec.
445    pub(super) fn is_bootstrap_directive(schema: &Schema, directive: &Directive) -> bool {
446        let Some(definition) = schema.directive_definitions.get(&directive.name) else {
447            return false;
448        };
449        if Self::is_link_directive_definition(definition) {
450            if let Some(url) = directive
451                .specified_argument_by_name("url")
452                .and_then(|value| value.as_str())
453            {
454                let url = url.parse::<Url>();
455                let default_link_name = LINK_DIRECTIVE_NAME_IN_SPEC;
456                let expected_name = directive
457                    .specified_argument_by_name("as")
458                    .and_then(|value| value.as_str())
459                    .unwrap_or(default_link_name.as_str());
460                return url.is_ok_and(|url| {
461                    url.identity == Identity::link_identity() && directive.name == expected_name
462                });
463            }
464        } else if Self::is_core_directive_definition(definition) {
465            // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be
466            // removed when those are updated.
467            if let Some(url) = directive
468                .specified_argument_by_name("feature")
469                .and_then(|value| value.as_str())
470            {
471                let url = url.parse::<Url>();
472                let expected_name = directive
473                    .specified_argument_by_name("as")
474                    .and_then(|value| value.as_str())
475                    .unwrap_or("core");
476                return url.is_ok_and(|url| {
477                    url.identity == Identity::core_identity() && directive.name == expected_name
478                });
479            }
480        };
481        false
482    }
483
484    /// Returns true if the given definition matches the @link definition.
485    ///
486    /// Either of these definitions are accepted:
487    /// ```graphql
488    /// directive @_ANY_NAME_(url: String!, as: String) repeatable on SCHEMA
489    /// directive @_ANY_NAME_(url: String, as: String) repeatable on SCHEMA
490    /// directive @_ANY_NAME_(url: String!) repeatable on SCHEMA
491    /// directive @_ANY_NAME_(url: String) repeatable on SCHEMA
492    /// ```
493    fn is_link_directive_definition(definition: &DirectiveDefinition) -> bool {
494        definition.repeatable
495            && definition.locations == [DirectiveLocation::Schema]
496            && definition.argument_by_name("url").is_some_and(|argument| {
497                // The "true" type of `url` in the @link spec is actually `String` (nullable), and this
498                // for future-proofing reasons (the idea was that we may introduce later other
499                // ways to identify specs that are not urls). But we allow the definition to
500                // have a non-nullable type both for convenience and because some early
501                // federation previews actually generated that.
502                *argument.ty == ty!(String!) || *argument.ty == ty!(String)
503            })
504            && definition
505                .argument_by_name("as")
506                .is_none_or(|argument| *argument.ty == ty!(String))
507    }
508
509    /// Returns true if the given definition matches the @core definition.
510    ///
511    /// Either of these definitions are accepted:
512    /// ```graphql
513    /// directive @_ANY_NAME_(feature: String!, as: String) repeatable on SCHEMA
514    /// directive @_ANY_NAME_(feature: String, as: String) repeatable on SCHEMA
515    /// directive @_ANY_NAME_(feature: String!) repeatable on SCHEMA
516    /// directive @_ANY_NAME_(feature: String) repeatable on SCHEMA
517    /// ```
518    fn is_core_directive_definition(definition: &DirectiveDefinition) -> bool {
519        // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be
520        // removed when those are updated.
521        definition.repeatable
522            && definition.locations == [DirectiveLocation::Schema]
523            && definition
524                .argument_by_name("feature")
525                .is_some_and(|argument| {
526                    // The "true" type of `url` in the @core spec is actually `String` (nullable), and this
527                    // for future-proofing reasons (the idea was that we may introduce later other
528                    // ways to identify specs that are not urls). But we allow the definition to
529                    // have a non-nullable type both for convenience and because some early
530                    // federation previews actually generated that.
531                    *argument.ty == ty!(String!) || *argument.ty == ty!(String)
532                })
533            && definition
534                .argument_by_name("as")
535                .is_none_or(|argument| *argument.ty == ty!(String))
536    }
537
538    /// Add `self` (the @link spec definition) and a directive application of it to the schema.
539    // Note: we may want to allow some `import` as argument to this method. When we do, we need to
540    // watch for imports of `Purpose` and `Import` and add the types under their imported name.
541    pub(crate) fn add_to_schema(
542        &self,
543        schema: &mut FederationSchema,
544        alias: Option<Name>,
545    ) -> Result<(), FederationError> {
546        self.add_definitions_to_schema(schema, alias.clone(), vec![])?;
547
548        // This adds `@link(url: "https://specs.apollo.dev/link/v1.0")` to the "schema" definition.
549        // And we have a choice to add it either the main definition, or to an `extend schema`.
550        //
551        // In theory, always adding it to the main definition should be safe since even if some
552        // root operations can be defined in extensions, you shouldn't have an extension without a
553        // definition, and so we should never be in a case where _all_ root operations are defined
554        // in extensions (which would be a problem for printing the definition itself since it's
555        // syntactically invalid to have a schema definition with no operations).
556        //
557        // In practice however, graphQL-js has historically accepted extensions without definition
558        // for schema, and we even abuse this a bit with federation out of convenience, so we could
559        // end up in the situation where if we put the directive on the definition, it cannot be
560        // printed properly due to the user having defined all its root operations in an extension.
561        //
562        // We could always add the directive to an extension, and that could kind of work but:
563        // 1. the core/link spec says that the link-to-link application should be the first `@link`
564        //   of the schema, but if user put some `@link` on their schema definition but we always
565        //   put the link-to-link on an extension, then we're kind of not respecting our own spec
566        //   (in practice, our own code can actually handle this as it does not strongly rely on
567        //   that "it should be the first" rule, but that would set a bad example).
568        // 2. earlier versions (pre-#1875) were always putting that directive on the definition,
569        //   and we wanted to avoid surprising users by changing that for not reason.
570        //
571        // So instead, we put the directive on the schema definition unless some extensions exists
572        // but no definition does (that is, no non-extension elements are populated).
573        //
574        // Side-note: this test must be done _before_ we call `insert_directive`, otherwise it
575        // would take it into account.
576
577        let name = alias.as_ref().unwrap_or(&self.name).clone();
578        let mut arguments = vec![Node::new(Argument {
579            name: self.url_arg_name(),
580            value: self.url.to_string().into(),
581        })];
582        if let Some(alias) = alias {
583            arguments.push(Node::new(Argument {
584                name: LINK_DIRECTIVE_AS_ARGUMENT_NAME,
585                value: alias.to_string().into(),
586            }));
587        }
588
589        let schema_definition = SchemaDefinitionPosition.get(schema.schema());
590        SchemaDefinitionPosition.insert_directive_at(
591            schema,
592            Component {
593                origin: schema_definition.origin_to_use(),
594                node: Node::new(Directive { name, arguments }),
595            },
596            0, // @link to link spec should be first
597        )?;
598        Ok(())
599    }
600
601    pub(crate) fn extract_alias_and_imports_on_missing_link_directive_definition(
602        application: &Component<Directive>,
603    ) -> Result<(Option<Name>, Vec<Arc<Import>>), FederationError> {
604        // PORT_NOTE: This is really logic encapsulated from onMissingDirectiveDefinition() in the
605        // JS codebase's FederationBlueprint, but moved here since it's all link-specific. The logic
606        // itself has a lot of problems, but we're porting it as-is for now, and we'll address the
607        // problems with it in a later version bump.
608        let url =
609            directive_optional_string_argument(application, &LINK_DIRECTIVE_URL_ARGUMENT_NAME)?;
610        if let Some(url) = url
611            && url.starts_with(&LinkSpecDefinition::latest().url.identity.to_string())
612        {
613            let alias =
614                directive_optional_string_argument(application, &LINK_DIRECTIVE_AS_ARGUMENT_NAME)?
615                    .map(Name::new)
616                    .transpose()?;
617            let imports = directive_optional_list_argument(
618                application,
619                &LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME,
620            )?
621            .into_iter()
622            .flatten()
623            .map(|value| Ok::<_, FederationError>(Arc::new(Import::try_from(value.as_ref())?)))
624            .process_results(|r| r.collect::<Vec<_>>())?;
625            return Ok((alias, imports));
626        }
627        Ok((None, vec![]))
628    }
629
630    pub(crate) fn add_definitions_to_schema(
631        &self,
632        schema: &mut FederationSchema,
633        alias: Option<Name>,
634        imports: Vec<Arc<Import>>,
635    ) -> Result<(), FederationError> {
636        if let Some(metadata) = schema.metadata() {
637            let link_spec_def = metadata.link_spec_definition();
638            if link_spec_def.url.identity == *self.identity() {
639                // Already exists with the same version, let it be.
640                return Ok(());
641            }
642            let self_fmt = format!("{}/{}", self.identity(), self.version());
643            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
644                message: format!(
645                    "Cannot add link spec {self_fmt} to the schema, it already has {existing_def}",
646                    existing_def = link_spec_def.url
647                ),
648            }
649            .into());
650        }
651
652        // The @link spec is special in that it is the one that bootstrap everything, and by the
653        // time this method is called, the `schema` may not yet have any `schema.metadata()` set up
654        // yet. To have `check_or_add` calls below still work, we pass a mock link object with the
655        // proper information.
656        let mock_link = Arc::new(Link {
657            url: self.url.clone(),
658            spec_alias: alias,
659            imports,
660            purpose: None,
661            line_column_range: None,
662        });
663        Ok(())
664            .and_try(
665                self.type_specs()
666                    .into_iter()
667                    .try_for_all(|spec| spec.check_or_add(schema, Some(&mock_link))),
668            )
669            .and_try(
670                self.directive_specs()
671                    .into_iter()
672                    .try_for_all(|spec| spec.check_or_add(schema, Some(&mock_link))),
673            )
674    }
675
676    pub(crate) fn apply_feature_to_schema(
677        &self,
678        schema: &mut FederationSchema,
679        feature: &dyn SpecDefinition,
680        alias: Option<Name>,
681        purpose: Option<Purpose>,
682        imports: Option<Vec<Import>>,
683        mut on_apply_error: impl FnMut(FederationError) -> Result<(), FederationError>,
684    ) -> Result<(), FederationError> {
685        let Some(metadata) = schema.metadata() else {
686            bail!("Schema unexpectedly not a link schema (add @link first)");
687        };
688        if metadata.link_itself().url != self.url {
689            bail!(
690                "Cannot use this version of @link ({}), the schema uses version {}",
691                self.url,
692                metadata.link_itself().url,
693            );
694        }
695        let mut directive = Directive::new(metadata.link_itself().spec_name_in_schema());
696        directive.arguments.push(Node::new(Argument {
697            name: self.url_arg_name(),
698            value: Node::new(feature.to_string().into()),
699        }));
700        if let Some(alias) = alias {
701            directive.arguments.push(Node::new(Argument {
702                name: LINK_DIRECTIVE_AS_ARGUMENT_NAME,
703                value: Node::new(alias.to_string().into()),
704            }));
705        }
706        if let Some(purpose) = &purpose {
707            if self.supports_purpose() {
708                directive.arguments.push(Node::new(Argument {
709                    name: LINK_DIRECTIVE_FOR_ARGUMENT_NAME,
710                    value: Node::new(purpose.into()),
711                }));
712            } else {
713                return Err(SingleFederationError::InvalidLinkDirectiveUsage {
714                    message: format!(
715                        "Cannot apply feature {} with purpose since the schema's @core/@link version does not support it.", feature.to_string()
716                    ),
717                }.into());
718            }
719        }
720        if let Some(imports) = imports
721            && !imports.is_empty()
722        {
723            if self.supports_import() {
724                directive.arguments.push(Node::new(Argument {
725                    name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME,
726                    value: Node::new(Value::List(
727                        imports
728                            .into_iter()
729                            .map(|i| Node::new((&i).into()))
730                            .collect(),
731                    )),
732                }))
733            } else {
734                return Err(SingleFederationError::InvalidLinkDirectiveUsage {
735                        message: format!(
736                            "Cannot apply feature {} with imports since the schema's @core/@link version does not support it.",
737                            feature.to_string()
738                        ),
739                    }.into());
740            }
741        }
742
743        if let Err(error) =
744            SchemaDefinitionPosition.insert_directive(schema, Component::new(directive))
745        {
746            on_apply_error(error)?;
747        };
748        feature.add_elements_to_schema(schema)?;
749
750        Ok(())
751    }
752
753    pub(crate) fn fed1_latest() -> &'static Self {
754        // Note: The `unwrap()` calls won't panic, since `CORE_VERSIONS` will always have at
755        // least one version.
756        let latest_version = CORE_VERSIONS.versions().last().unwrap();
757        CORE_VERSIONS.find(latest_version).unwrap()
758    }
759
760    /// PORT_NOTE: This is a port of the `linkSpec`, which is defined as `LINK_VERSIONS.latest()`.
761    pub(crate) fn latest() -> &'static Self {
762        // Note: The `unwrap()` calls won't panic, since `LINK_VERSIONS` will always have at
763        // least one version.
764        let latest_version = LINK_VERSIONS.versions().last().unwrap();
765        LINK_VERSIONS.find(latest_version).unwrap()
766    }
767}
768
769impl SpecDefinition for LinkSpecDefinition {
770    fn url(&self) -> &Url {
771        &self.url
772    }
773
774    fn directive_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
775        vec![Box::new(DirectiveSpecification::new(
776            self.name().clone(),
777            &self.create_definition_argument_specifications(),
778            true,
779            &[DirectiveLocation::Schema],
780            None,
781        ))]
782    }
783
784    fn type_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
785        let mut specs: Vec<Box<dyn TypeAndDirectiveSpecification>> = Vec::with_capacity(2);
786        if self.supports_purpose() {
787            specs.push(Box::new(create_link_purpose_type_spec()))
788        }
789        if self.supports_import() {
790            specs.push(Box::new(create_link_import_type_spec()))
791        }
792        specs
793    }
794
795    fn minimum_federation_version(&self) -> &Version {
796        &self.minimum_federation_version
797    }
798
799    fn add_elements_to_schema(
800        &self,
801        _schema: &mut FederationSchema,
802    ) -> Result<(), FederationError> {
803        // Link is special and the @link directive is added in `add_to_schema` above
804        Ok(())
805    }
806
807    fn purpose(&self) -> Option<Purpose> {
808        None
809    }
810}
811
812fn create_link_purpose_type_spec() -> EnumTypeSpecification {
813    EnumTypeSpecification {
814        name: PURPOSE_TYPE_NAME_IN_SPEC,
815        values: vec![
816            EnumValueSpecification {
817                name: name!("SECURITY"),
818                description: Some(
819                    "`SECURITY` features provide metadata necessary to securely resolve fields."
820                        .to_string(),
821                ),
822            },
823            EnumValueSpecification {
824                name: name!("EXECUTION"),
825                description: Some(
826                    "`EXECUTION` features provide metadata necessary for operation execution."
827                        .to_string(),
828                ),
829            },
830        ],
831    }
832}
833
834fn create_link_import_type_spec() -> ScalarTypeSpecification {
835    ScalarTypeSpecification {
836        name: IMPORT_TYPE_NAME_IN_SPEC,
837    }
838}
839
840pub(crate) static CORE_VERSIONS: LazyLock<SpecDefinitions<LinkSpecDefinition>> =
841    LazyLock::new(|| {
842        let mut definitions = SpecDefinitions::new(Identity::core_identity());
843        definitions.add(LinkSpecDefinition::new(
844            Version { major: 0, minor: 1 },
845            Version { major: 1, minor: 0 },
846            false,
847        ));
848        definitions.add(LinkSpecDefinition::new(
849            Version { major: 0, minor: 2 },
850            Version { major: 2, minor: 0 },
851            false,
852        ));
853        definitions
854    });
855pub(crate) static LINK_VERSIONS: LazyLock<SpecDefinitions<LinkSpecDefinition>> =
856    LazyLock::new(|| {
857        let mut definitions = SpecDefinitions::new(Identity::link_identity());
858        definitions.add(LinkSpecDefinition::new(
859            Version { major: 1, minor: 0 },
860            Version { major: 2, minor: 0 },
861            true,
862        ));
863        definitions
864    });