1use std::collections::HashMap;
2use std::fmt;
3use std::str;
4use std::sync::Arc;
5
6use apollo_compiler::InvalidNameError;
7use apollo_compiler::Name;
8use apollo_compiler::Node;
9use apollo_compiler::Schema;
10use apollo_compiler::ast::Directive;
11use apollo_compiler::ast::Value;
12use apollo_compiler::collections::IndexMap;
13use apollo_compiler::name;
14use apollo_compiler::schema::Component;
15use thiserror::Error;
16
17use crate::error::FederationError;
18use crate::error::SingleFederationError;
19use crate::link::link_spec_definition::CORE_VERSIONS;
20use crate::link::link_spec_definition::LINK_VERSIONS;
21use crate::link::link_spec_definition::LinkSpecDefinition;
22use crate::link::spec::Identity;
23use crate::link::spec::Url;
24
25pub(crate) mod argument;
26pub(crate) mod authenticated_spec_definition;
27pub(crate) mod cache_tag_spec_definition;
28pub(crate) mod context_spec_definition;
29pub mod cost_spec_definition;
30pub mod database;
31pub(crate) mod federation_spec_definition;
32pub(crate) mod graphql_definition;
33pub(crate) mod inaccessible_spec_definition;
34pub(crate) mod join_spec_definition;
35pub(crate) mod link_spec_definition;
36pub(crate) mod policy_spec_definition;
37pub(crate) mod requires_scopes_spec_definition;
38pub mod spec;
39pub(crate) mod spec_definition;
40pub(crate) mod tag_spec_definition;
41
42pub const DEFAULT_LINK_NAME: Name = name!("link");
43pub const DEFAULT_IMPORT_SCALAR_NAME: Name = name!("Import");
44pub const DEFAULT_PURPOSE_ENUM_NAME: Name = name!("Purpose");
45pub(crate) const IMPORT_AS_ARGUMENT: Name = name!("as");
46pub(crate) const IMPORT_NAME_ARGUMENT: Name = name!("name");
47
48#[derive(Error, Debug, PartialEq)]
51pub enum LinkError {
52 #[error(transparent)]
53 InvalidName(#[from] InvalidNameError),
54 #[error("Invalid use of @link in schema: {0}")]
55 BootstrapError(String),
56 #[error("Unknown import: {0}")]
57 InvalidImport(String),
58}
59
60impl From<LinkError> for FederationError {
62 fn from(value: LinkError) -> Self {
63 SingleFederationError::InvalidLinkDirectiveUsage {
64 message: value.to_string(),
65 }
66 .into()
67 }
68}
69
70#[derive(Clone, Copy, Eq, PartialEq, Debug)]
71pub enum Purpose {
72 SECURITY,
73 EXECUTION,
74}
75
76impl Purpose {
77 pub fn from_value(value: &Value) -> Result<Purpose, LinkError> {
78 value
79 .as_enum()
80 .ok_or_else(|| {
81 LinkError::BootstrapError("invalid `purpose` value, should be an enum".to_string())
82 })
83 .and_then(|value| value.parse())
84 }
85}
86
87impl str::FromStr for Purpose {
88 type Err = LinkError;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 match s {
92 "SECURITY" => Ok(Purpose::SECURITY),
93 "EXECUTION" => Ok(Purpose::EXECUTION),
94 _ => Err(LinkError::BootstrapError(format!(
95 "invalid/unrecognized `purpose` value '{s}'"
96 ))),
97 }
98 }
99}
100
101impl fmt::Display for Purpose {
102 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103 match self {
104 Purpose::SECURITY => f.write_str("SECURITY"),
105 Purpose::EXECUTION => f.write_str("EXECUTION"),
106 }
107 }
108}
109
110impl From<&Purpose> for Name {
111 fn from(value: &Purpose) -> Self {
112 match value {
113 Purpose::SECURITY => name!("SECURITY"),
114 Purpose::EXECUTION => name!("EXECUTION"),
115 }
116 }
117}
118
119#[derive(Eq, PartialEq, Debug)]
120pub struct Import {
121 pub element: Name,
126
127 pub is_directive: bool,
129
130 pub alias: Option<Name>,
132}
133
134impl Import {
135 pub fn from_value(value: &Value) -> Result<Import, LinkError> {
136 match value {
140 Value::String(str) => {
141 if let Some(directive_name) = str.strip_prefix('@') {
142 Ok(Import {
143 element: Name::new(directive_name)?,
144 is_directive: true,
145 alias: None,
146 })
147 } else {
148 Ok(Import {
149 element: Name::new(str)?,
150 is_directive: false,
151 alias: None,
152 })
153 }
154 }
155 Value::Object(fields) => {
156 let mut name: Option<&str> = None;
157 let mut alias: Option<&str> = None;
158 for (k, v) in fields {
159 match k.as_str() {
160 "name" => {
161 name = Some(v.as_str().ok_or_else(|| {
162 LinkError::BootstrapError(format!(r#"in "{}", invalid value for `name` field in @link(import:) argument: must be a string"#, value.serialize().no_indent()))
163 })?)
164 },
165 "as" => {
166 alias = Some(v.as_str().ok_or_else(|| {
167 LinkError::BootstrapError(format!(r#"in "{}", invalid value for `as` field in @link(import:) argument: must be a string"#, value.serialize().no_indent()))
168 })?)
169 },
170 _ => Err(LinkError::BootstrapError(format!(r#"in "{}", unknown field `{k}` in @link(import:) argument"#, value.serialize().no_indent())))?
171 }
172 }
173 let Some(element) = name else {
174 return Err(LinkError::BootstrapError(format!(
175 r#"in "{}", invalid entry in @link(import:) argument, missing mandatory `name` field"#,
176 value.serialize().no_indent()
177 )));
178 };
179 if let Some(directive_name) = element.strip_prefix('@') {
180 if let Some(alias_str) = alias.as_ref() {
181 let Some(alias_str) = alias_str.strip_prefix('@') else {
182 return Err(LinkError::BootstrapError(format!(
183 r#"in "{}", invalid alias '{alias_str}' for import name '{element}': should start with '@' since the imported name does"#,
184 value.serialize().no_indent()
185 )));
186 };
187 alias = Some(alias_str);
188 }
189 Ok(Import {
190 element: Name::new(directive_name)?,
191 is_directive: true,
192 alias: alias.map(Name::new).transpose()?,
193 })
194 } else {
195 if let Some(alias) = &alias
196 && alias.starts_with('@')
197 {
198 return Err(LinkError::BootstrapError(format!(
199 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 '@')"#,
200 value.serialize().no_indent()
201 )));
202 }
203 Ok(Import {
204 element: Name::new(element)?,
205 is_directive: false,
206 alias: alias.map(Name::new).transpose()?,
207 })
208 }
209 }
210 _ => Err(LinkError::BootstrapError(format!(
211 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>" }}."#,
212 value.serialize().no_indent()
213 ))),
214 }
215 }
216
217 pub fn element_display_name(&self) -> impl fmt::Display {
218 DisplayName {
219 name: &self.element,
220 is_directive: self.is_directive,
221 }
222 }
223
224 pub fn imported_name(&self) -> &Name {
225 self.alias.as_ref().unwrap_or(&self.element)
226 }
227
228 pub fn imported_display_name(&self) -> impl fmt::Display {
229 DisplayName {
230 name: self.imported_name(),
231 is_directive: self.is_directive,
232 }
233 }
234}
235
236struct DisplayName<'s> {
238 name: &'s str,
239 is_directive: bool,
240}
241
242impl fmt::Display for DisplayName<'_> {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 if self.is_directive {
245 f.write_str("@")?;
246 }
247 f.write_str(self.name)
248 }
249}
250
251impl fmt::Display for Import {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 if self.alias.is_some() {
254 write!(
255 f,
256 r#"{{ name: "{}", as: "{}" }}"#,
257 self.element_display_name(),
258 self.imported_display_name()
259 )
260 } else {
261 write!(f, r#""{}""#, self.imported_display_name())
262 }
263 }
264}
265
266#[allow(clippy::from_over_into)]
267impl Into<Value> for Import {
268 fn into(self) -> Value {
269 if let Some(alias) = self.alias {
270 Value::Object(vec![
271 (
272 IMPORT_NAME_ARGUMENT,
273 Node::new(Value::String(self.element.to_string())),
274 ),
275 (
276 IMPORT_AS_ARGUMENT,
277 Node::new(Value::String(alias.to_string())),
278 ),
279 ])
280 } else {
281 Value::String(self.element.to_string())
282 }
283 }
284}
285
286#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct Link {
288 pub url: Url,
289 pub spec_alias: Option<Name>,
290 pub imports: Vec<Arc<Import>>,
291 pub purpose: Option<Purpose>,
292}
293
294impl Link {
295 pub fn spec_name_in_schema(&self) -> &Name {
296 self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
297 }
298
299 pub fn directive_name_in_schema(&self, name: &Name) -> Name {
300 if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
305 import.alias.clone().unwrap_or_else(|| name.clone())
306 } else if name == self.url.identity.name.as_str() {
307 self.spec_name_in_schema().clone()
308 } else {
309 Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
311 }
312 }
313
314 pub(crate) fn directive_name_in_schema_for_core_arguments(
315 spec_url: &Url,
316 spec_name_in_schema: &Name,
317 imports: &[Import],
318 directive_name_in_spec: &Name,
319 ) -> Name {
320 if let Some(element_import) = imports
321 .iter()
322 .find(|i| i.element == *directive_name_in_spec)
323 {
324 element_import.imported_name().clone()
325 } else if spec_url.identity.name == *directive_name_in_spec {
326 spec_name_in_schema.clone()
327 } else {
328 Name::new_unchecked(format!("{spec_name_in_schema}__{directive_name_in_spec}").as_str())
329 }
330 }
331
332 pub fn type_name_in_schema(&self, name: &Name) -> Name {
333 if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
336 import.alias.clone().unwrap_or_else(|| name.clone())
337 } else {
338 Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
340 }
341 }
342
343 pub fn from_directive_application(directive: &Node<Directive>) -> Result<Link, LinkError> {
344 let (url, is_link) = if let Some(value) = directive.specified_argument_by_name("url") {
345 (value, true)
346 } else if let Some(value) = directive.specified_argument_by_name("feature") {
347 (value, false)
350 } else {
351 return Err(LinkError::BootstrapError(
352 "the `url` argument for @link is mandatory".to_string(),
353 ));
354 };
355
356 let (directive_name, arg_name) = if is_link {
357 ("link", "url")
358 } else {
359 ("core", "feature")
360 };
361
362 let url = url.as_str().ok_or_else(|| {
363 LinkError::BootstrapError(format!(
364 "the `{arg_name}` argument for @{directive_name} must be a String"
365 ))
366 })?;
367 let url: Url = url.parse::<Url>().map_err(|e| {
368 LinkError::BootstrapError(format!("invalid `{arg_name}` argument (reason: {e})"))
369 })?;
370
371 let spec_alias = directive
372 .specified_argument_by_name("as")
373 .and_then(|arg| arg.as_str())
374 .map(Name::new)
375 .transpose()?;
376 let purpose = if let Some(value) = directive.specified_argument_by_name("for") {
377 Some(Purpose::from_value(value)?)
378 } else {
379 None
380 };
381
382 let imports = if is_link {
383 directive
384 .specified_argument_by_name("import")
385 .and_then(|arg| arg.as_list())
386 .unwrap_or(&[])
387 .iter()
388 .map(|value| Ok(Arc::new(Import::from_value(value)?)))
389 .collect::<Result<Vec<Arc<Import>>, LinkError>>()?
390 } else {
391 Default::default()
392 };
393
394 Ok(Link {
395 url,
396 spec_alias,
397 imports,
398 purpose,
399 })
400 }
401
402 pub fn for_identity<'schema>(
403 schema: &'schema Schema,
404 identity: &Identity,
405 ) -> Option<(Self, &'schema Component<Directive>)> {
406 schema
407 .schema_definition
408 .directives
409 .iter()
410 .find_map(|directive| {
411 let link = Link::from_directive_application(directive).ok()?;
412 if link.url.identity == *identity {
413 Some((link, directive))
414 } else {
415 None
416 }
417 })
418 }
419}
420
421impl fmt::Display for Link {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 let imported_types: Vec<String> = self
424 .imports
425 .iter()
426 .map(|import| import.to_string())
427 .collect::<Vec<String>>();
428 let imports = if imported_types.is_empty() {
429 "".to_string()
430 } else {
431 format!(r#", import: [{}]"#, imported_types.join(", "))
432 };
433 let alias = self
434 .spec_alias
435 .as_ref()
436 .map(|a| format!(r#", as: "{a}""#))
437 .unwrap_or("".to_string());
438 let purpose = self
439 .purpose
440 .as_ref()
441 .map(|p| format!(r#", for: {p}"#))
442 .unwrap_or("".to_string());
443 write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
444 }
445}
446
447#[derive(Clone, Debug)]
448pub struct LinkedElement {
449 pub link: Arc<Link>,
450 pub import: Option<Arc<Import>>,
451}
452
453#[derive(Clone, Default, Eq, PartialEq, Debug)]
454pub struct LinksMetadata {
455 pub(crate) links: Vec<Arc<Link>>,
456 pub(crate) by_identity: IndexMap<Identity, Arc<Link>>,
457 pub(crate) by_name_in_schema: IndexMap<Name, Arc<Link>>,
458 pub(crate) types_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
459 pub(crate) directives_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
460 pub(crate) directives_by_original_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
461}
462
463impl LinksMetadata {
464 pub(crate) fn link_spec_definition(
466 &self,
467 ) -> Result<&'static LinkSpecDefinition, FederationError> {
468 if let Some(link_link) = self.for_identity(&Identity::link_identity()) {
469 LINK_VERSIONS.find(&link_link.url.version).ok_or_else(|| {
470 SingleFederationError::Internal {
471 message: format!("Unexpected link spec version {}", link_link.url.version),
472 }
473 .into()
474 })
475 } else if let Some(core_link) = self.for_identity(&Identity::core_identity()) {
476 CORE_VERSIONS.find(&core_link.url.version).ok_or_else(|| {
477 SingleFederationError::Internal {
478 message: format!("Unexpected core spec version {}", core_link.url.version),
479 }
480 .into()
481 })
482 } else {
483 Err(SingleFederationError::Internal {
484 message: "Unexpectedly could not find core/link spec".to_owned(),
485 }
486 .into())
487 }
488 }
489
490 pub fn all_links(&self) -> &[Arc<Link>] {
491 self.links.as_ref()
492 }
493
494 pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
495 self.by_identity.get(identity).cloned()
496 }
497
498 pub fn source_link_of_type(&self, type_name: &Name) -> Option<LinkedElement> {
499 if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
502 Some(LinkedElement {
503 link: Arc::clone(link),
504 import: Some(Arc::clone(import)),
505 })
506 } else {
507 type_name.split_once("__").and_then(|(spec_name, _)| {
508 self.by_name_in_schema
509 .get(spec_name)
510 .map(|link| LinkedElement {
511 link: Arc::clone(link),
512 import: None,
513 })
514 })
515 }
516 }
517
518 pub fn source_link_of_directive(&self, directive_name: &Name) -> Option<LinkedElement> {
519 if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
525 return Some(LinkedElement {
526 link: Arc::clone(link),
527 import: Some(Arc::clone(import)),
528 });
529 }
530
531 if let Some(link) = self.by_name_in_schema.get(directive_name) {
532 return Some(LinkedElement {
533 link: Arc::clone(link),
534 import: None,
535 });
536 }
537
538 directive_name.split_once("__").and_then(|(spec_name, _)| {
539 self.by_name_in_schema
540 .get(spec_name)
541 .map(|link| LinkedElement {
542 link: Arc::clone(link),
543 import: None,
544 })
545 })
546 }
547
548 pub(crate) fn import_to_feature_url_map(&self) -> HashMap<String, Url> {
549 let directive_entries = self
550 .directives_by_imported_name
551 .iter()
552 .map(|(name, (link, _))| (name.to_string(), link.url.clone()));
553 let type_entries = self
554 .types_by_imported_name
555 .iter()
556 .map(|(name, (link, _))| (name.to_string(), link.url.clone()));
557
558 directive_entries.chain(type_entries).collect()
559 }
560}