apollo_at_link/
link.rs

1use crate::spec::Identity;
2use crate::spec::Url;
3use apollo_compiler::ast::{Directive, Value};
4use std::fmt;
5use std::str;
6use std::{collections::HashMap, sync::Arc};
7use thiserror::Error;
8
9pub const DEFAULT_LINK_NAME: &str = "link";
10pub const DEFAULT_IMPORT_SCALAR_NAME: &str = "Import";
11pub const DEFAULT_PURPOSE_ENUM_NAME: &str = "Purpose";
12
13// TODO: we should provide proper "diagnostic" here, linking to ast, accumulating more than one
14// error and whatnot.
15#[derive(Error, Debug, PartialEq)]
16pub enum LinkError {
17    #[error("Invalid use of @link in schema: {0}")]
18    BootstrapError(String),
19}
20
21#[derive(Eq, PartialEq, Debug)]
22pub enum Purpose {
23    SECURITY,
24    EXECUTION,
25}
26
27impl Purpose {
28    pub fn from_ast_value(value: &Value) -> Result<Purpose, LinkError> {
29        if let Value::Enum(value) = value {
30            Ok(value.parse::<Purpose>()?)
31        } else {
32            Err(LinkError::BootstrapError(
33                "invalid `purpose` value, should be an enum".to_string(),
34            ))
35        }
36    }
37}
38
39impl str::FromStr for Purpose {
40    type Err = LinkError;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        match s {
44            "SECURITY" => Ok(Purpose::SECURITY),
45            "EXECUTION" => Ok(Purpose::EXECUTION),
46            _ => Err(LinkError::BootstrapError(format!(
47                "invalid/unrecognized `purpose` value '{}'",
48                s
49            ))),
50        }
51    }
52}
53
54impl fmt::Display for Purpose {
55    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56        let str = match self {
57            Purpose::SECURITY => "SECURITY",
58            Purpose::EXECUTION => "EXECUTION",
59        };
60        write!(f, "{}", str)
61    }
62}
63
64#[derive(Eq, PartialEq, Debug)]
65pub struct Import {
66    /// The name of the element that is being imported.
67    ///
68    /// Note that this will never start with '@': whether or not this is the name of a directive is
69    /// entirely reflected by the value of `is_directive`.
70    pub element: String,
71
72    /// Whether the imported element is a directive (if it is not, then it is an imported type).
73    pub is_directive: bool,
74
75    /// The optional alias under which the element is imported.
76    pub alias: Option<String>,
77}
78
79impl Import {
80    pub fn from_hir_value(value: &Value) -> Result<Import, LinkError> {
81        // TODO: it could be nice to include the broken value in the error messages of this method
82        // (especially since @link(import:) is a list), but `Value` does not implement `Display`
83        // currently, so a bit annoying.
84        match value {
85            Value::String(str) => {
86                let is_directive = str.starts_with('@');
87                let element = if is_directive {
88                    str.strip_prefix('@').unwrap().to_string()
89                } else {
90                    str.to_string()
91                };
92                Ok(Import { element, is_directive, alias: None })
93            },
94            Value::Object(fields) => {
95                let mut name: Option<String> = None;
96                let mut alias: Option<String> = None;
97                for (k, v) in fields {
98                    match k.as_str() {
99                        "name" => {
100                            name = Some(v.as_str().ok_or_else(|| {
101                                LinkError::BootstrapError("invalid value for `name` field in @link(import:) argument: must be a string".to_string())
102                            })?.to_owned())
103                        },
104                        "as" => {
105                            alias = Some(v.as_str().ok_or_else(|| {
106                                LinkError::BootstrapError("invalid value for `as` field in @link(import:) argument: must be a string".to_string())
107                            })?.to_owned())
108                        },
109                        _ => Err(LinkError::BootstrapError(format!("unknown field `{k}` in @link(import:) argument")))?
110                    }
111                }
112                if let Some(element) = name {
113                    let is_directive = element.starts_with('@');
114                    if is_directive {
115                        let element = element.strip_prefix('@').unwrap().to_string();
116                        if let Some(alias_str) = alias {
117                            if !alias_str.starts_with('@') {
118                                Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should start with '@' since the imported name does", alias_str, element)))?
119                            }
120                            alias = Some(alias_str.strip_prefix('@').unwrap().to_string());
121                        }
122                        Ok(Import { element, is_directive, alias })
123                    } else {
124                        if let Some(alias) = &alias {
125                            if alias.starts_with('@') {
126                                Err(LinkError::BootstrapError(format!("invalid alias '{}' for import name '{}': should not start with '@' (or, if {} is a directive, then the name should start with '@')", alias, element, element)))?
127                            }
128                        }
129                        Ok(Import { element, is_directive, alias })
130                    }
131                } else {
132                    Err(LinkError::BootstrapError("invalid entry in @link(import:) argument, missing mandatory `name` field".to_string()))
133                }
134            },
135            _ => Err(LinkError::BootstrapError("invalid sub-value for @link(import:) argument: values should be either strings or input object values of the form { name: \"<importedElement>\", as: \"<alias>\" }.".to_string()))
136        }
137    }
138
139    pub fn imported_name(&self) -> &String {
140        return self.alias.as_ref().unwrap_or(&self.element);
141    }
142
143    pub fn imported_display_name(&self) -> String {
144        if self.is_directive {
145            format!("@{}", self.imported_name())
146        } else {
147            self.imported_name().clone()
148        }
149    }
150}
151
152impl fmt::Display for Import {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        if self.alias.is_some() {
155            write!(
156                f,
157                r#"{{ name: "{}", as: "{}" }}"#,
158                if self.is_directive {
159                    format!("@{}", self.element)
160                } else {
161                    self.element.clone()
162                },
163                self.imported_display_name()
164            )
165        } else {
166            write!(f, r#""{}""#, self.imported_display_name())
167        }
168    }
169}
170
171#[derive(Debug, Eq, PartialEq)]
172pub struct Link {
173    pub url: Url,
174    pub spec_alias: Option<String>,
175    pub imports: Vec<Arc<Import>>,
176    pub purpose: Option<Purpose>,
177}
178
179impl Link {
180    pub fn spec_name_in_schema(&self) -> &String {
181        self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
182    }
183
184    pub fn directive_name_in_schema(&self, name: &str) -> String {
185        // If the directive is imported, then it's name in schema is whatever name it is
186        // imported under. Otherwise, it is usually fully qualified by the spec name (so,
187        // something like 'federation__key'), but there is a special case for directives
188        // whose name match the one of the spec: those don't get qualified.
189        if let Some(import) = self.imports.iter().find(|i| i.element == name) {
190            import.alias.clone().unwrap_or(name.to_string())
191        } else if name == self.url.identity.name {
192            self.spec_name_in_schema().clone()
193        } else {
194            format!("{}__{}", self.spec_name_in_schema(), name)
195        }
196    }
197
198    pub fn type_name_in_schema(&self, name: &str) -> String {
199        // Similar to directives, but the special case of a directive name matching the spec
200        // name does not apply to types.
201        if let Some(import) = self.imports.iter().find(|i| i.element == name) {
202            import.alias.clone().unwrap_or(name.to_string())
203        } else {
204            format!("{}__{}", self.spec_name_in_schema(), name)
205        }
206    }
207
208    pub fn from_directive_application(directive: &Directive) -> Result<Link, LinkError> {
209        let url = directive
210            .argument_by_name("url")
211            .and_then(|arg| arg.as_str())
212            .ok_or(LinkError::BootstrapError(
213                "the `url` argument for @link is mandatory".to_string(),
214            ))?;
215        let url: Url = url.parse::<Url>().map_err(|e| {
216            LinkError::BootstrapError(format!("invalid `url` argument (reason: {})", e))
217        })?;
218        let spec_alias = directive
219            .argument_by_name("as")
220            .and_then(|arg| arg.as_str())
221            .map(|s| s.to_owned());
222        let purpose = if let Some(value) = directive.argument_by_name("for") {
223            Some(Purpose::from_ast_value(value)?)
224        } else {
225            None
226        };
227        let mut imports = Vec::new();
228        if let Some(values) = directive
229            .argument_by_name("import")
230            .and_then(|arg| arg.as_list())
231        {
232            for v in values {
233                imports.push(Arc::new(Import::from_hir_value(v)?));
234            }
235        };
236        Ok(Link {
237            url,
238            spec_alias,
239            imports,
240            purpose,
241        })
242    }
243}
244
245impl fmt::Display for Link {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        let imported_types: Vec<String> = self
248            .imports
249            .iter()
250            .map(|import| import.to_string())
251            .collect::<Vec<String>>();
252        let imports = if imported_types.is_empty() {
253            "".to_string()
254        } else {
255            format!(r#", import: [{}]"#, imported_types.join(", "))
256        };
257        let alias = self
258            .spec_alias
259            .as_ref()
260            .map(|a| format!(r#", as: "{}""#, a))
261            .unwrap_or("".to_string());
262        let purpose = self
263            .purpose
264            .as_ref()
265            .map(|p| format!(r#", for: {}"#, p))
266            .unwrap_or("".to_string());
267        write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
268    }
269}
270
271#[derive(Debug)]
272pub struct LinkedElement {
273    pub link: Arc<Link>,
274    pub import: Option<Arc<Import>>,
275}
276
277#[derive(Default, Eq, PartialEq, Debug)]
278pub struct LinksMetadata {
279    pub(crate) links: Vec<Arc<Link>>,
280    pub(crate) by_identity: HashMap<Identity, Arc<Link>>,
281    pub(crate) by_name_in_schema: HashMap<String, Arc<Link>>,
282    pub(crate) types_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
283    pub(crate) directives_by_imported_name: HashMap<String, (Arc<Link>, Arc<Import>)>,
284}
285
286impl LinksMetadata {
287    pub fn all_links(&self) -> &[Arc<Link>] {
288        return self.links.as_ref();
289    }
290
291    pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
292        return self.by_identity.get(identity).cloned();
293    }
294
295    pub fn source_link_of_type(&self, type_name: &str) -> Option<LinkedElement> {
296        // For types, it's either an imported name or it must be fully qualified
297
298        if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
299            Some(LinkedElement {
300                link: Arc::clone(link),
301                import: Some(Arc::clone(import)),
302            })
303        } else {
304            type_name.split_once("__").and_then(|(spec_name, _)| {
305                self.by_name_in_schema
306                    .get(spec_name)
307                    .map(|link| LinkedElement {
308                        link: Arc::clone(link),
309                        import: None,
310                    })
311            })
312        }
313    }
314
315    pub fn source_link_of_directive(&self, directive_name: &str) -> Option<LinkedElement> {
316        // For directives, it can be either:
317        //   1. be an imported name,
318        //   2. be the "imported" name of a linked spec (special case of a directive named like the
319        //      spec),
320        //   3. or it must be fully qualified.
321        if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
322            return Some(LinkedElement {
323                link: Arc::clone(link),
324                import: Some(Arc::clone(import)),
325            });
326        }
327
328        if let Some(link) = self.by_name_in_schema.get(directive_name) {
329            return Some(LinkedElement {
330                link: Arc::clone(link),
331                import: None,
332            });
333        }
334
335        directive_name.split_once("__").and_then(|(spec_name, _)| {
336            self.by_name_in_schema
337                .get(spec_name)
338                .map(|link| LinkedElement {
339                    link: Arc::clone(link),
340                    import: None,
341                })
342        })
343    }
344}