Skip to main content

apollo_federation/link/
mod.rs

1use std::fmt;
2use std::str;
3use std::sync::Arc;
4
5use apollo_compiler::InvalidNameError;
6use apollo_compiler::Name;
7use apollo_compiler::Node;
8use apollo_compiler::Schema;
9use apollo_compiler::ast::Directive;
10use apollo_compiler::ast::Value;
11use apollo_compiler::collections::IndexMap;
12use apollo_compiler::name;
13use apollo_compiler::schema::Component;
14use thiserror::Error;
15
16use crate::error::FederationError;
17use crate::error::SingleFederationError;
18use crate::link::link_spec_definition::CORE_VERSIONS;
19use crate::link::link_spec_definition::LINK_VERSIONS;
20use crate::link::link_spec_definition::LinkSpecDefinition;
21use crate::link::spec::Identity;
22use crate::link::spec::Url;
23
24pub(crate) mod argument;
25pub(crate) mod context_spec_definition;
26pub mod cost_spec_definition;
27pub mod database;
28pub(crate) mod federation_spec_definition;
29pub(crate) mod graphql_definition;
30pub(crate) mod inaccessible_spec_definition;
31pub(crate) mod join_spec_definition;
32pub(crate) mod link_spec_definition;
33pub mod spec;
34pub(crate) mod spec_definition;
35
36pub const DEFAULT_LINK_NAME: Name = name!("link");
37pub const DEFAULT_IMPORT_SCALAR_NAME: Name = name!("Import");
38pub const DEFAULT_PURPOSE_ENUM_NAME: Name = name!("Purpose");
39
40// TODO: we should provide proper "diagnostic" here, linking to ast, accumulating more than one
41// error and whatnot.
42#[derive(Error, Debug, PartialEq)]
43pub enum LinkError {
44    #[error(transparent)]
45    InvalidName(#[from] InvalidNameError),
46    #[error("Invalid use of @link in schema: {0}")]
47    BootstrapError(String),
48    #[error("Unknown import: {0}")]
49    InvalidImport(String),
50}
51
52// TODO: Replace LinkError usages with FederationError.
53impl From<LinkError> for FederationError {
54    fn from(value: LinkError) -> Self {
55        SingleFederationError::InvalidLinkDirectiveUsage {
56            message: value.to_string(),
57        }
58        .into()
59    }
60}
61
62#[derive(Eq, PartialEq, Debug)]
63pub enum Purpose {
64    SECURITY,
65    EXECUTION,
66}
67
68impl Purpose {
69    pub fn from_value(value: &Value) -> Result<Purpose, LinkError> {
70        value
71            .as_enum()
72            .ok_or_else(|| {
73                LinkError::BootstrapError("invalid `purpose` value, should be an enum".to_string())
74            })
75            .and_then(|value| value.parse())
76    }
77}
78
79impl str::FromStr for Purpose {
80    type Err = LinkError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        match s {
84            "SECURITY" => Ok(Purpose::SECURITY),
85            "EXECUTION" => Ok(Purpose::EXECUTION),
86            _ => Err(LinkError::BootstrapError(format!(
87                "invalid/unrecognized `purpose` value '{s}'"
88            ))),
89        }
90    }
91}
92
93impl fmt::Display for Purpose {
94    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95        match self {
96            Purpose::SECURITY => f.write_str("SECURITY"),
97            Purpose::EXECUTION => f.write_str("EXECUTION"),
98        }
99    }
100}
101
102impl From<&Purpose> for Name {
103    fn from(value: &Purpose) -> Self {
104        match value {
105            Purpose::SECURITY => name!("SECURITY"),
106            Purpose::EXECUTION => name!("EXECUTION"),
107        }
108    }
109}
110
111#[derive(Eq, PartialEq, Debug)]
112pub struct Import {
113    /// The name of the element that is being imported.
114    ///
115    /// Note that this will never start with '@': whether or not this is the name of a directive is
116    /// entirely reflected by the value of `is_directive`.
117    pub element: Name,
118
119    /// Whether the imported element is a directive (if it is not, then it is an imported type).
120    pub is_directive: bool,
121
122    /// The optional alias under which the element is imported.
123    pub alias: Option<Name>,
124}
125
126impl Import {
127    pub fn from_value(value: &Value) -> Result<Import, LinkError> {
128        // TODO: it could be nice to include the broken value in the error messages of this method
129        // (especially since @link(import:) is a list), but `Value` does not implement `Display`
130        // currently, so a bit annoying.
131        match value {
132            Value::String(str) => {
133                if let Some(directive_name) = str.strip_prefix('@') {
134                    Ok(Import {
135                        element: Name::new(directive_name)?,
136                        is_directive: true,
137                        alias: None,
138                    })
139                } else {
140                    Ok(Import {
141                        element: Name::new(str)?,
142                        is_directive: false,
143                        alias: None,
144                    })
145                }
146            }
147            Value::Object(fields) => {
148                let mut name: Option<&str> = None;
149                let mut alias: Option<&str> = None;
150                for (k, v) in fields {
151                    match k.as_str() {
152                        "name" => {
153                            name = Some(v.as_str().ok_or_else(|| {
154                                LinkError::BootstrapError(format!(r#"in "{}", invalid value for `name` field in @link(import:) argument: must be a string"#, value.serialize().no_indent()))
155                            })?)
156                        },
157                        "as" => {
158                            alias = Some(v.as_str().ok_or_else(|| {
159                                LinkError::BootstrapError(format!(r#"in "{}", invalid value for `as` field in @link(import:) argument: must be a string"#, value.serialize().no_indent()))
160                            })?)
161                        },
162                        _ => Err(LinkError::BootstrapError(format!(r#"in "{}", unknown field `{k}` in @link(import:) argument"#, value.serialize().no_indent())))?
163                    }
164                }
165                let Some(element) = name else {
166                    return Err(LinkError::BootstrapError(format!(
167                        r#"in "{}", invalid entry in @link(import:) argument, missing mandatory `name` field"#,
168                        value.serialize().no_indent()
169                    )));
170                };
171                if let Some(directive_name) = element.strip_prefix('@') {
172                    if let Some(alias_str) = alias.as_ref() {
173                        let Some(alias_str) = alias_str.strip_prefix('@') else {
174                            return Err(LinkError::BootstrapError(format!(
175                                r#"in "{}", invalid alias '{alias_str}' for import name '{element}': should start with '@' since the imported name does"#,
176                                value.serialize().no_indent()
177                            )));
178                        };
179                        alias = Some(alias_str);
180                    }
181                    Ok(Import {
182                        element: Name::new(directive_name)?,
183                        is_directive: true,
184                        alias: alias.map(Name::new).transpose()?,
185                    })
186                } else {
187                    if let Some(alias) = &alias {
188                        if alias.starts_with('@') {
189                            return Err(LinkError::BootstrapError(format!(
190                                r#"in "{}", invalid alias '{alias}' for import name '{element}': should not start with '@' (or, if {element} is a directive, then the name should start with '@')"#,
191                                value.serialize().no_indent()
192                            )));
193                        }
194                    }
195                    Ok(Import {
196                        element: Name::new(element)?,
197                        is_directive: false,
198                        alias: alias.map(Name::new).transpose()?,
199                    })
200                }
201            }
202            _ => Err(LinkError::BootstrapError(format!(
203                r#"in "{}", invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form {{ name: "<importedElement>", as: "<alias>" }}."#,
204                value.serialize().no_indent()
205            ))),
206        }
207    }
208
209    pub fn element_display_name(&self) -> impl fmt::Display + '_ {
210        DisplayName {
211            name: &self.element,
212            is_directive: self.is_directive,
213        }
214    }
215
216    pub fn imported_name(&self) -> &Name {
217        self.alias.as_ref().unwrap_or(&self.element)
218    }
219
220    pub fn imported_display_name(&self) -> impl fmt::Display + '_ {
221        DisplayName {
222            name: self.imported_name(),
223            is_directive: self.is_directive,
224        }
225    }
226}
227
228/// A [`fmt::Display`]able wrapper for name strings that adds an `@` in front for directive names.
229struct DisplayName<'s> {
230    name: &'s str,
231    is_directive: bool,
232}
233
234impl fmt::Display for DisplayName<'_> {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        if self.is_directive {
237            f.write_str("@")?;
238        }
239        f.write_str(self.name)
240    }
241}
242
243impl fmt::Display for Import {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        if self.alias.is_some() {
246            write!(
247                f,
248                r#"{{ name: "{}", as: "{}" }}"#,
249                self.element_display_name(),
250                self.imported_display_name()
251            )
252        } else {
253            write!(f, r#""{}""#, self.imported_display_name())
254        }
255    }
256}
257
258#[derive(Debug, Eq, PartialEq)]
259pub struct Link {
260    pub url: Url,
261    pub spec_alias: Option<Name>,
262    pub imports: Vec<Arc<Import>>,
263    pub purpose: Option<Purpose>,
264}
265
266impl Link {
267    pub fn spec_name_in_schema(&self) -> &Name {
268        self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
269    }
270
271    pub fn directive_name_in_schema(&self, name: &Name) -> Name {
272        // If the directive is imported, then it's name in schema is whatever name it is
273        // imported under. Otherwise, it is usually fully qualified by the spec name (so,
274        // something like 'federation__key'), but there is a special case for directives
275        // whose name match the one of the spec: those don't get qualified.
276        if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
277            import.alias.clone().unwrap_or_else(|| name.clone())
278        } else if name == self.url.identity.name.as_str() {
279            self.spec_name_in_schema().clone()
280        } else {
281            // Both sides are `Name`s and we just add valid characters in between.
282            Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
283        }
284    }
285
286    pub fn type_name_in_schema(&self, name: &Name) -> Name {
287        // Similar to directives, but the special case of a directive name matching the spec
288        // name does not apply to types.
289        if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
290            import.alias.clone().unwrap_or_else(|| name.clone())
291        } else {
292            // Both sides are `Name`s and we just add valid characters in between.
293            Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
294        }
295    }
296
297    pub fn from_directive_application(directive: &Node<Directive>) -> Result<Link, LinkError> {
298        let (url, is_link) = if let Some(value) = directive.specified_argument_by_name("url") {
299            (value, true)
300        } else if let Some(value) = directive.specified_argument_by_name("feature") {
301            // XXX(@goto-bus-stop): @core compatibility is primarily to support old tests--should be
302            // removed when those are updated.
303            (value, false)
304        } else {
305            return Err(LinkError::BootstrapError(
306                "the `url` argument for @link is mandatory".to_string(),
307            ));
308        };
309
310        let (directive_name, arg_name) = if is_link {
311            ("link", "url")
312        } else {
313            ("core", "feature")
314        };
315
316        let url = url.as_str().ok_or_else(|| {
317            LinkError::BootstrapError(format!(
318                "the `{arg_name}` argument for @{directive_name} must be a String"
319            ))
320        })?;
321        let url: Url = url.parse::<Url>().map_err(|e| {
322            LinkError::BootstrapError(format!("invalid `{arg_name}` argument (reason: {e})"))
323        })?;
324
325        let spec_alias = directive
326            .specified_argument_by_name("as")
327            .and_then(|arg| arg.as_str())
328            .map(Name::new)
329            .transpose()?;
330        let purpose = if let Some(value) = directive.specified_argument_by_name("for") {
331            Some(Purpose::from_value(value)?)
332        } else {
333            None
334        };
335
336        let imports = if is_link {
337            directive
338                .specified_argument_by_name("import")
339                .and_then(|arg| arg.as_list())
340                .unwrap_or(&[])
341                .iter()
342                .map(|value| Ok(Arc::new(Import::from_value(value)?)))
343                .collect::<Result<Vec<Arc<Import>>, LinkError>>()?
344        } else {
345            Default::default()
346        };
347
348        Ok(Link {
349            url,
350            spec_alias,
351            imports,
352            purpose,
353        })
354    }
355
356    pub fn for_identity<'schema>(
357        schema: &'schema Schema,
358        identity: &Identity,
359    ) -> Option<(Self, &'schema Component<Directive>)> {
360        schema
361            .schema_definition
362            .directives
363            .iter()
364            .find_map(|directive| {
365                let link = Link::from_directive_application(directive).ok()?;
366                if link.url.identity == *identity {
367                    Some((link, directive))
368                } else {
369                    None
370                }
371            })
372    }
373}
374
375impl fmt::Display for Link {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        let imported_types: Vec<String> = self
378            .imports
379            .iter()
380            .map(|import| import.to_string())
381            .collect::<Vec<String>>();
382        let imports = if imported_types.is_empty() {
383            "".to_string()
384        } else {
385            format!(r#", import: [{}]"#, imported_types.join(", "))
386        };
387        let alias = self
388            .spec_alias
389            .as_ref()
390            .map(|a| format!(r#", as: "{}""#, a))
391            .unwrap_or("".to_string());
392        let purpose = self
393            .purpose
394            .as_ref()
395            .map(|p| format!(r#", for: {}"#, p))
396            .unwrap_or("".to_string());
397        write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
398    }
399}
400
401#[derive(Debug)]
402pub struct LinkedElement {
403    pub link: Arc<Link>,
404    pub import: Option<Arc<Import>>,
405}
406
407#[derive(Default, Eq, PartialEq, Debug)]
408pub struct LinksMetadata {
409    pub(crate) links: Vec<Arc<Link>>,
410    pub(crate) by_identity: IndexMap<Identity, Arc<Link>>,
411    pub(crate) by_name_in_schema: IndexMap<Name, Arc<Link>>,
412    pub(crate) types_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
413    pub(crate) directives_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
414}
415
416impl LinksMetadata {
417    pub(crate) fn link_spec_definition(
418        &self,
419    ) -> Result<&'static LinkSpecDefinition, FederationError> {
420        if let Some(link_link) = self.for_identity(&Identity::link_identity()) {
421            LINK_VERSIONS.find(&link_link.url.version).ok_or_else(|| {
422                SingleFederationError::Internal {
423                    message: format!("Unexpected link spec version {}", link_link.url.version),
424                }
425                .into()
426            })
427        } else if let Some(core_link) = self.for_identity(&Identity::core_identity()) {
428            CORE_VERSIONS.find(&core_link.url.version).ok_or_else(|| {
429                SingleFederationError::Internal {
430                    message: format!("Unexpected core spec version {}", core_link.url.version),
431                }
432                .into()
433            })
434        } else {
435            Err(SingleFederationError::Internal {
436                message: "Unexpectedly could not find core/link spec".to_owned(),
437            }
438            .into())
439        }
440    }
441
442    pub fn all_links(&self) -> &[Arc<Link>] {
443        self.links.as_ref()
444    }
445
446    pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
447        self.by_identity.get(identity).cloned()
448    }
449
450    pub fn source_link_of_type(&self, type_name: &Name) -> Option<LinkedElement> {
451        // For types, it's either an imported name or it must be fully qualified
452
453        if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
454            Some(LinkedElement {
455                link: Arc::clone(link),
456                import: Some(Arc::clone(import)),
457            })
458        } else {
459            type_name.split_once("__").and_then(|(spec_name, _)| {
460                self.by_name_in_schema
461                    .get(spec_name)
462                    .map(|link| LinkedElement {
463                        link: Arc::clone(link),
464                        import: None,
465                    })
466            })
467        }
468    }
469
470    pub fn source_link_of_directive(&self, directive_name: &Name) -> Option<LinkedElement> {
471        // For directives, it can be either:
472        //   1. be an imported name,
473        //   2. be the "imported" name of a linked spec (special case of a directive named like the
474        //      spec),
475        //   3. or it must be fully qualified.
476        if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
477            return Some(LinkedElement {
478                link: Arc::clone(link),
479                import: Some(Arc::clone(import)),
480            });
481        }
482
483        if let Some(link) = self.by_name_in_schema.get(directive_name) {
484            return Some(LinkedElement {
485                link: Arc::clone(link),
486                import: None,
487            });
488        }
489
490        directive_name.split_once("__").and_then(|(spec_name, _)| {
491            self.by_name_in_schema
492                .get(spec_name)
493                .map(|link| LinkedElement {
494                    link: Arc::clone(link),
495                    import: None,
496                })
497        })
498    }
499}