Skip to main content

apollo_federation/link/
mod.rs

1use std::fmt;
2use std::ops::Range;
3use std::sync::Arc;
4
5use apollo_compiler::Name;
6use apollo_compiler::Node;
7use apollo_compiler::Schema;
8use apollo_compiler::ast::Directive;
9use apollo_compiler::ast::Value;
10use apollo_compiler::parser::LineColumn;
11use apollo_compiler::schema::Component;
12
13use crate::error::FederationError;
14use crate::link::link_spec_definition::CORE_VERSIONS;
15use crate::link::link_spec_definition::LINK_VERSIONS;
16use crate::link::spec::Identity;
17use crate::link::spec::Url;
18
19pub(crate) mod argument;
20pub(crate) mod authenticated_spec_definition;
21pub(crate) mod cache_tag_spec_definition;
22pub(crate) mod context_spec_definition;
23pub mod cost_spec_definition;
24pub(crate) mod federation_spec_definition;
25pub(crate) mod graphql_definition;
26pub(crate) mod inaccessible_spec_definition;
27pub(crate) mod join_spec_definition;
28pub(crate) mod link_spec_definition;
29pub mod metadata;
30pub(crate) mod policy_spec_definition;
31pub(crate) mod requires_scopes_spec_definition;
32pub mod spec;
33pub(crate) mod spec_definition;
34pub(crate) mod spec_registry;
35pub(crate) mod tag_spec_definition;
36
37#[derive(Clone, Copy, Eq, PartialEq, Debug)]
38pub enum Purpose {
39    SECURITY,
40    EXECUTION,
41}
42
43impl fmt::Display for Purpose {
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        Value::from(self).fmt(f)
46    }
47}
48
49#[derive(Debug, Clone, Eq, PartialEq)]
50pub struct Import {
51    /// The name of the element that is being imported.
52    ///
53    /// Note that this will never start with '@': whether or not this is the name of a directive is
54    /// entirely reflected by the value of `is_directive`.
55    pub element: Name,
56
57    /// Whether the imported element is a directive (if it is not, then it is an imported type).
58    pub is_directive: bool,
59
60    /// The optional alias under which the element is imported.
61    pub alias: Option<Name>,
62}
63
64impl Import {
65    pub fn name_in_schema(&self) -> &Name {
66        self.alias.as_ref().unwrap_or(&self.element)
67    }
68
69    pub fn element_name_in_spec(&self) -> ElementName {
70        ElementName {
71            name: self.element.clone(),
72            is_directive: self.is_directive,
73        }
74    }
75
76    pub fn element_name_in_schema(&self) -> ElementName {
77        ElementName {
78            name: self.name_in_schema().clone(),
79            is_directive: self.is_directive,
80        }
81    }
82}
83
84/// The name of a type or directive, regardless of whether it's a name-in-spec or a name-in-schema.
85/// Note that this is cheap to clone since [Name] is cheap to clone.
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
87pub struct ElementName {
88    pub name: Name,
89    pub is_directive: bool,
90}
91
92impl fmt::Display for ElementName {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        if self.is_directive {
95            f.write_str("@")?;
96        }
97        f.write_str(&self.name)
98    }
99}
100
101impl fmt::Display for Import {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        Value::from(self).fmt(f)
104    }
105}
106
107/// Metadata about a single application of @link in a schema.
108// PORT_NOTE: Named `CoreFeature` in the JS codebase, but "core" is outdated terminology.
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct Link {
111    pub url: Url,
112    pub spec_alias: Option<Name>,
113    pub imports: Vec<Arc<Import>>,
114    pub purpose: Option<Purpose>,
115    pub line_column_range: Option<Range<LineColumn>>,
116}
117
118impl Link {
119    /// Use [super::LinkSpecDefinition::link_from_directive] instead of this method where possible,
120    /// since this method guesses the version of the link/core spec.
121    pub(crate) fn from_directive_application_when_link_spec_unknown(
122        directive: &Node<Directive>,
123        schema: &Schema,
124    ) -> Result<Link, FederationError> {
125        LINK_VERSIONS
126            .latest()
127            .link_from_directive(directive, schema)
128            .or_else(|error| {
129                // If the directive couldn't be parsed as a @link application, try parsing it as @core
130                // one, though if that also errors then prefer the original @link-parsing errors.
131                CORE_VERSIONS
132                    .latest()
133                    .link_from_directive(directive, schema)
134                    .or(Err(error))
135            })
136    }
137
138    pub fn spec_name_in_schema(&self) -> Name {
139        if let Some(spec_alias) = &self.spec_alias {
140            return spec_alias.clone();
141        }
142        let name = &self.url.identity.name;
143        name.clone().try_into().unwrap_or_else(|_| {
144            // TODO: While @link does allow for specs with invalid names as long as they have valid
145            // aliases, for backwards compatibility, we need to support @link applications that have
146            // an invalid name and no alias. These have historically been fine because such schemas
147            // do not use the default names for types/directives from the spec being linked. So
148            // ideally, we would return a `Result::Err` in `directive_name_in_schema()` and
149            // `type_name_in_schema()` when someone tries to compute an element name-in-schema that
150            // would be an invalid name. However, that would cause many upstream callers to also
151            // have to return `Result`. For now, we're leaving that step to the future. (We wouldn't
152            // do that in this method because it's used in `LinksMetadata::add_link()`.)
153            Name::new_unchecked(name)
154        })
155    }
156
157    pub fn directive_name_in_schema(&self, name: &Name) -> Name {
158        // If the directive is imported, then it's name in schema is whatever name it is
159        // imported under. Otherwise, it is usually fully qualified by the spec name (so,
160        // something like 'federation__key'), but there is a special case for directives
161        // whose name match the one of the spec: those don't get qualified.
162        if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
163            import.alias.clone().unwrap_or_else(|| name.clone())
164        } else if name.as_str() == self.url.identity.name.as_ref() {
165            self.spec_name_in_schema().clone()
166        } else {
167            // Both sides are `Name`s and we just add valid characters in between.
168            Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
169        }
170    }
171
172    pub(crate) fn directive_name_in_schema_for_core_arguments(
173        spec_url: &Url,
174        spec_name_in_schema: &Name,
175        imports: &[Import],
176        directive_name_in_spec: &Name,
177    ) -> Name {
178        if let Some(element_import) = imports
179            .iter()
180            .find(|i| i.element == *directive_name_in_spec)
181        {
182            element_import.name_in_schema().clone()
183        } else if spec_url.identity.name.as_ref() == directive_name_in_spec.as_str() {
184            spec_name_in_schema.clone()
185        } else {
186            Name::new_unchecked(format!("{spec_name_in_schema}__{directive_name_in_spec}").as_str())
187        }
188    }
189
190    pub fn type_name_in_schema(&self, name: &Name) -> Name {
191        // Similar to directives, but the special case of a directive name matching the spec
192        // name does not apply to types.
193        if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
194            import.alias.clone().unwrap_or_else(|| name.clone())
195        } else {
196            // Both sides are `Name`s and we just add valid characters in between.
197            Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
198        }
199    }
200
201    pub(crate) fn for_identity<'schema>(
202        schema: &'schema Schema,
203        identity: &Identity,
204    ) -> Option<(Self, &'schema Component<Directive>)> {
205        schema
206            .schema_definition
207            .directives
208            .iter()
209            .find_map(|directive| {
210                let link =
211                    Link::from_directive_application_when_link_spec_unknown(directive, schema)
212                        .ok()?;
213                if link.url.identity == *identity {
214                    Some((link, directive))
215                } else {
216                    None
217                }
218            })
219    }
220}
221
222impl fmt::Display for Link {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        LINK_VERSIONS.latest().directive_from_link(self).fmt(f)
225    }
226}