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 let element_string = if self.is_directive {
270 format!("@{}", self.element)
271 } else {
272 self.element.to_string()
273 };
274
275 if let Some(alias) = self.alias {
276 let alias_string = if self.is_directive {
277 format!("@{}", alias)
278 } else {
279 alias.to_string()
280 };
281 Value::Object(vec![
282 (
283 IMPORT_NAME_ARGUMENT,
284 Node::new(Value::String(element_string)),
285 ),
286 (IMPORT_AS_ARGUMENT, Node::new(Value::String(alias_string))),
287 ])
288 } else {
289 Value::String(element_string)
290 }
291 }
292}
293
294#[derive(Clone, Debug, Eq, PartialEq)]
295pub struct Link {
296 pub url: Url,
297 pub spec_alias: Option<Name>,
298 pub imports: Vec<Arc<Import>>,
299 pub purpose: Option<Purpose>,
300}
301
302impl Link {
303 pub fn spec_name_in_schema(&self) -> &Name {
304 self.spec_alias.as_ref().unwrap_or(&self.url.identity.name)
305 }
306
307 pub fn directive_name_in_schema(&self, name: &Name) -> Name {
308 if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
313 import.alias.clone().unwrap_or_else(|| name.clone())
314 } else if name == self.url.identity.name.as_str() {
315 self.spec_name_in_schema().clone()
316 } else {
317 Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
319 }
320 }
321
322 pub(crate) fn directive_name_in_schema_for_core_arguments(
323 spec_url: &Url,
324 spec_name_in_schema: &Name,
325 imports: &[Import],
326 directive_name_in_spec: &Name,
327 ) -> Name {
328 if let Some(element_import) = imports
329 .iter()
330 .find(|i| i.element == *directive_name_in_spec)
331 {
332 element_import.imported_name().clone()
333 } else if spec_url.identity.name == *directive_name_in_spec {
334 spec_name_in_schema.clone()
335 } else {
336 Name::new_unchecked(format!("{spec_name_in_schema}__{directive_name_in_spec}").as_str())
337 }
338 }
339
340 pub fn type_name_in_schema(&self, name: &Name) -> Name {
341 if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
344 import.alias.clone().unwrap_or_else(|| name.clone())
345 } else {
346 Name::new_unchecked(&format!("{}__{}", self.spec_name_in_schema(), name))
348 }
349 }
350
351 pub fn from_directive_application(directive: &Node<Directive>) -> Result<Link, LinkError> {
352 let (url, is_link) = if let Some(value) = directive.specified_argument_by_name("url") {
353 (value, true)
354 } else if let Some(value) = directive.specified_argument_by_name("feature") {
355 (value, false)
358 } else {
359 return Err(LinkError::BootstrapError(
360 "the `url` argument for @link is mandatory".to_string(),
361 ));
362 };
363
364 let (directive_name, arg_name) = if is_link {
365 ("link", "url")
366 } else {
367 ("core", "feature")
368 };
369
370 let url = url.as_str().ok_or_else(|| {
371 LinkError::BootstrapError(format!(
372 "the `{arg_name}` argument for @{directive_name} must be a String"
373 ))
374 })?;
375 let url: Url = url.parse::<Url>().map_err(|e| {
376 LinkError::BootstrapError(format!("invalid `{arg_name}` argument (reason: {e})"))
377 })?;
378
379 let spec_alias = directive
380 .specified_argument_by_name("as")
381 .and_then(|arg| arg.as_str())
382 .map(Name::new)
383 .transpose()?;
384 let purpose = if let Some(value) = directive.specified_argument_by_name("for") {
385 Some(Purpose::from_value(value)?)
386 } else {
387 None
388 };
389
390 let imports = if is_link {
391 directive
392 .specified_argument_by_name("import")
393 .and_then(|arg| arg.as_list())
394 .unwrap_or(&[])
395 .iter()
396 .map(|value| Ok(Arc::new(Import::from_value(value)?)))
397 .collect::<Result<Vec<Arc<Import>>, LinkError>>()?
398 } else {
399 Default::default()
400 };
401
402 Ok(Link {
403 url,
404 spec_alias,
405 imports,
406 purpose,
407 })
408 }
409
410 pub fn for_identity<'schema>(
411 schema: &'schema Schema,
412 identity: &Identity,
413 ) -> Option<(Self, &'schema Component<Directive>)> {
414 schema
415 .schema_definition
416 .directives
417 .iter()
418 .find_map(|directive| {
419 let link = Link::from_directive_application(directive).ok()?;
420 if link.url.identity == *identity {
421 Some((link, directive))
422 } else {
423 None
424 }
425 })
426 }
427
428 pub(crate) fn renames(&self, element: &Name) -> bool {
430 self.imports
431 .iter()
432 .find(|import| &import.element == element)
433 .is_some_and(|import| *import.imported_name() != *element)
434 }
435}
436
437impl fmt::Display for Link {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 let imported_types: Vec<String> = self
440 .imports
441 .iter()
442 .map(|import| import.to_string())
443 .collect::<Vec<String>>();
444 let imports = if imported_types.is_empty() {
445 "".to_string()
446 } else {
447 format!(r#", import: [{}]"#, imported_types.join(", "))
448 };
449 let alias = self
450 .spec_alias
451 .as_ref()
452 .map(|a| format!(r#", as: "{a}""#))
453 .unwrap_or("".to_string());
454 let purpose = self
455 .purpose
456 .as_ref()
457 .map(|p| format!(r#", for: {p}"#))
458 .unwrap_or("".to_string());
459 write!(f, r#"@link(url: "{}"{alias}{imports}{purpose})"#, self.url)
460 }
461}
462
463#[derive(Clone, Debug)]
464pub struct LinkedElement {
465 pub link: Arc<Link>,
466 pub import: Option<Arc<Import>>,
467}
468
469#[derive(Clone, Default, Eq, PartialEq, Debug)]
470pub struct LinksMetadata {
471 pub(crate) links: Vec<Arc<Link>>,
472 pub(crate) by_identity: IndexMap<Identity, Arc<Link>>,
473 pub(crate) by_name_in_schema: IndexMap<Name, Arc<Link>>,
474 pub(crate) types_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
475 pub(crate) directives_by_imported_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
476 pub(crate) directives_by_original_name: IndexMap<Name, (Arc<Link>, Arc<Import>)>,
477}
478
479impl LinksMetadata {
480 pub(crate) fn link_spec_definition(
482 &self,
483 ) -> Result<&'static LinkSpecDefinition, FederationError> {
484 if let Some(link_link) = self.for_identity(&Identity::link_identity()) {
485 LINK_VERSIONS.find(&link_link.url.version).ok_or_else(|| {
486 SingleFederationError::Internal {
487 message: format!("Unexpected link spec version {}", link_link.url.version),
488 }
489 .into()
490 })
491 } else if let Some(core_link) = self.for_identity(&Identity::core_identity()) {
492 CORE_VERSIONS.find(&core_link.url.version).ok_or_else(|| {
493 SingleFederationError::Internal {
494 message: format!("Unexpected core spec version {}", core_link.url.version),
495 }
496 .into()
497 })
498 } else {
499 Err(SingleFederationError::Internal {
500 message: "Unexpectedly could not find core/link spec".to_owned(),
501 }
502 .into())
503 }
504 }
505
506 pub fn all_links(&self) -> &[Arc<Link>] {
507 self.links.as_ref()
508 }
509
510 pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
511 self.by_identity.get(identity).cloned()
512 }
513
514 pub fn source_link_of_type(&self, type_name: &Name) -> Option<LinkedElement> {
515 if let Some((link, import)) = self.types_by_imported_name.get(type_name) {
518 Some(LinkedElement {
519 link: Arc::clone(link),
520 import: Some(Arc::clone(import)),
521 })
522 } else {
523 type_name.split_once("__").and_then(|(spec_name, _)| {
524 self.by_name_in_schema
525 .get(spec_name)
526 .map(|link| LinkedElement {
527 link: Arc::clone(link),
528 import: None,
529 })
530 })
531 }
532 }
533
534 pub fn source_link_of_directive(&self, directive_name: &Name) -> Option<LinkedElement> {
535 if let Some((link, import)) = self.directives_by_imported_name.get(directive_name) {
541 return Some(LinkedElement {
542 link: Arc::clone(link),
543 import: Some(Arc::clone(import)),
544 });
545 }
546
547 if let Some(link) = self.by_name_in_schema.get(directive_name) {
548 return Some(LinkedElement {
549 link: Arc::clone(link),
550 import: None,
551 });
552 }
553
554 directive_name.split_once("__").and_then(|(spec_name, _)| {
555 self.by_name_in_schema
556 .get(spec_name)
557 .map(|link| LinkedElement {
558 link: Arc::clone(link),
559 import: None,
560 })
561 })
562 }
563
564 pub(crate) fn import_to_feature_url_map(&self) -> HashMap<String, Url> {
565 let directive_entries = self
566 .directives_by_imported_name
567 .iter()
568 .map(|(name, (link, _))| (name.to_string(), link.url.clone()));
569 let type_entries = self
570 .types_by_imported_name
571 .iter()
572 .map(|(name, (link, _))| (name.to_string(), link.url.clone()));
573
574 directive_entries.chain(type_entries).collect()
575 }
576}