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