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#[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
52impl 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 pub element: Name,
118
119 pub is_directive: bool,
121
122 pub alias: Option<Name>,
124}
125
126impl Import {
127 pub fn from_value(value: &Value) -> Result<Import, LinkError> {
128 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
228struct 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 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 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 if let Some(import) = self.imports.iter().find(|i| i.element == *name) {
290 import.alias.clone().unwrap_or_else(|| name.clone())
291 } else {
292 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 (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 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 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}