use std::borrow::Cow;
use std::sync::Arc;
use apollo_compiler::Name;
use apollo_compiler::ast::DirectiveDefinition;
use indexmap::IndexMap;
use indexmap::IndexSet;
use crate::bail;
use crate::error::CompositionError;
use crate::error::FederationError;
use crate::error::HasLocations;
use crate::error::Locations;
use crate::error::SingleFederationError;
use crate::error::SubgraphLocation;
use crate::error::suggestion::did_you_mean;
use crate::error::suggestion::suggestion_list;
use crate::link::Link;
use crate::link::LinkedElement;
use crate::link::authenticated_spec_definition::AUTHENTICATED_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_TAG_DIRECTIVE_NAME_IN_SPEC;
use crate::link::inaccessible_spec_definition::INACCESSIBLE_DIRECTIVE_NAME_IN_SPEC;
use crate::link::policy_spec_definition::POLICY_DIRECTIVE_NAME_IN_SPEC;
use crate::link::requires_scopes_spec_definition::REQUIRES_SCOPES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::spec::APOLLO_SPEC_DOMAIN;
use crate::link::spec::Identity;
use crate::merger::error_reporter::ErrorReporter;
use crate::merger::hints::HintCode;
use crate::schema::position::DirectiveDefinitionPosition;
use crate::subgraph::typestate::HasMetadata;
use crate::subgraph::typestate::Subgraph;
use crate::supergraph::CompositionHint;
use crate::utils::MultiIndexMap as MultiMap;
use crate::utils::human_readable::human_readable_subgraph_names;
const DEFAULT_COMPOSED_DIRECTIVES: [Name; 6] = [
FEDERATION_TAG_DIRECTIVE_NAME_IN_SPEC,
INACCESSIBLE_DIRECTIVE_NAME_IN_SPEC,
AUTHENTICATED_DIRECTIVE_NAME_IN_SPEC,
REQUIRES_SCOPES_DIRECTIVE_NAME_IN_SPEC,
POLICY_DIRECTIVE_NAME_IN_SPEC,
FEDERATION_CONTEXT_DIRECTIVE_NAME_IN_SPEC,
];
pub(crate) struct ComposeDirectiveManager {
merge_directive_map: IndexMap<String, IndexSet<Name>>,
latest_feature_map: IndexMap<Identity, (Arc<Link>, IndexSet<String>)>,
directive_identity_map: IndexMap<Name, (Identity, Name)>,
identity_directive_map: IndexMap<Identity, IndexMap<Name, Name>>,
}
#[derive(Clone)]
pub(crate) struct MergeDirectiveItem {
subgraph_name: String,
pub(crate) definition: DirectiveDefinition,
link: LinkedElement,
}
impl MergeDirectiveItem {
fn new(subgraph_name: String, definition: DirectiveDefinition, link: LinkedElement) -> Self {
Self {
subgraph_name,
definition,
link,
}
}
fn identity(&self) -> &Identity {
&self.link.link.url.identity
}
fn directive_name_in_spec(&self) -> &Name {
&self.link.name_in_spec
}
fn directive_name(&self) -> &Name {
&self.link.name
}
fn directive_has_different_name_in_subgraph<T: HasMetadata>(
&self,
subgraph: &Subgraph<T>,
) -> bool {
let Some(metadata) = subgraph.schema().metadata() else {
return false;
};
let Some(link) = metadata.for_identity(&self.link.link.url.identity) else {
return false;
};
let Some(imp) = link
.imports
.iter()
.find(|i| i.is_directive && &i.element == self.directive_name_in_spec())
else {
return false;
};
let name_in_subgraph = imp.alias.as_ref().unwrap_or(&imp.element);
name_in_subgraph != self.directive_name()
}
}
impl std::fmt::Display for MergeDirectiveItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.directive_name())
}
}
type DirectiveImportSpecNamesByAlias<'a> = Cow<'a, IndexMap<Name, Name>>;
#[allow(dead_code)]
impl ComposeDirectiveManager {
pub(crate) fn new() -> Self {
Self {
merge_directive_map: Default::default(),
latest_feature_map: Default::default(),
directive_identity_map: Default::default(),
identity_directive_map: Default::default(),
}
}
pub(crate) fn all_composed_core_features(
&self,
) -> Vec<(Arc<Link>, DirectiveImportSpecNamesByAlias<'_>)> {
self.latest_feature_map
.iter()
.filter_map(|(identity, (link, _))| {
if identity.domain == APOLLO_SPEC_DOMAIN {
None
} else {
Some((
link.clone(),
self.identity_directive_map
.get(identity)
.map(Cow::Borrowed)
.unwrap_or_default(),
))
}
})
.collect()
}
pub(crate) fn directive_exists_in_supergraph(&self, directive_name: &Name) -> bool {
self.directive_identity_map.contains_key(directive_name)
}
pub(crate) fn get_latest_directive_definition<T: HasMetadata>(
&self,
directive_name: &Name,
subgraphs: &[Subgraph<T>],
error_reporter: &mut ErrorReporter,
) -> Result<Option<(String, DirectiveDefinitionPosition)>, FederationError> {
let Some((identity, name_in_spec)) = self.directive_identity_map.get(directive_name) else {
return Ok(None);
};
let Some((link, subgraph_names)) = self.latest_feature_map.get(identity) else {
bail!("link feature identity must exist in map");
};
let mut latest_subgraphs: Vec<Option<&Subgraph<T>>> = std::iter::repeat_with(|| None)
.take(subgraph_names.len())
.collect();
for subgraph in subgraphs {
let Some(index) = subgraph_names.get_index_of(&subgraph.name) else {
continue;
};
let Some(entry) = latest_subgraphs.get_mut(index) else {
bail!("subgraph index unexpectedly not in same-size vector");
};
*entry = Some(subgraph);
}
for subgraph in latest_subgraphs {
let Some(subgraph) = subgraph else {
bail!("subgraph name unexpectedly not in list of subgraphs");
};
let Some(metadata) = subgraph.schema().metadata() else {
continue;
};
let Some(link) = metadata.for_identity(identity) else {
continue;
};
let name_in_schema = link.directive_name_in_schema(name_in_spec);
let Some(def) = subgraph.schema().get_directive_definition(&name_in_schema) else {
continue;
};
return Ok(Some((subgraph.name.clone(), def)));
}
let plural = subgraph_names.len() != 1;
error_reporter.add_error(CompositionError::DirectiveCompositionError {
message: format!(
"Core feature \"{}/v{}\" in {} {} not have a directive definition for \"@{}\"",
identity,
link.url.version,
human_readable_subgraph_names(subgraph_names.iter()),
if plural { "do" } else { "does" },
directive_name,
),
});
Ok(None)
}
pub(crate) fn should_compose_directive(
&self,
subgraph_name: &str,
directive_name: &Name,
) -> bool {
self.merge_directive_map
.get(subgraph_name)
.is_some_and(|set| set.contains(directive_name))
}
pub(crate) fn validate<T: HasMetadata>(
&mut self,
subgraphs: &[Subgraph<T>],
error_reporter: &mut ErrorReporter,
) -> Result<(), FederationError> {
let mut wont_merge_features: IndexSet<_> = Default::default();
let mut wont_merge_directive_names: IndexSet<_> = Default::default();
let mut items_by_subgraph: MultiMap<String, MergeDirectiveItem> = MultiMap::new();
let mut items_by_directive_name: MultiMap<Name, MergeDirectiveItem> = MultiMap::new();
let mut items_by_directive_name_in_spec: MultiMap<Name, MergeDirectiveItem> =
MultiMap::new();
let tag_names_in_subgraphs: MultiMap<Name, String> = subgraphs
.iter()
.filter_map(|s| s.tag_directive_name().zip(Some(s.name.clone())))
.collect();
let inaccessible_names_in_subgraphs: MultiMap<Name, String> = subgraphs
.iter()
.filter_map(|s| s.inaccessible_directive_name().zip(Some(s.name.clone())))
.collect();
for subgraph in subgraphs {
let Ok(compose_directive_applications) =
subgraph.schema().compose_directive_applications()
else {
continue;
};
for application in compose_directive_applications {
match application {
Ok(compose_directive) => {
if compose_directive.arguments.name.is_empty() {
error_reporter
.add_compose_directive_error_for_empty_name(subgraph.name.as_str());
continue;
}
if !compose_directive.arguments.name.starts_with("@") {
error_reporter.add_compose_directive_error_for_missing_start_symbol(
compose_directive.arguments.name,
subgraph.name.as_str(),
);
continue;
}
let name = compose_directive.arguments.name.split_at(1).1;
let Some(directive) =
subgraph.schema().schema().directive_definitions.get(name)
else {
error_reporter.add_compose_directive_error_for_undefined_directive(
compose_directive.arguments.name,
subgraph,
);
continue;
};
let Some(feature) = subgraph
.schema()
.metadata()
.and_then(|links| links.source_link_of_directive(&directive.name))
else {
error_reporter.add_compose_directive_error_for_unrecognized_feature(
compose_directive.arguments.name,
subgraph.name.as_str(),
);
continue;
};
if feature.link.url.identity.domain == APOLLO_SPEC_DOMAIN
&& DEFAULT_COMPOSED_DIRECTIVES.contains(&feature.name_in_spec)
{
error_reporter
.add_compose_directive_hint_for_default_composed_directive(
compose_directive.arguments.name,
directive.locations(subgraph),
);
} else if feature.link.url.identity.domain == APOLLO_SPEC_DOMAIN {
error_reporter.add_compose_directive_error_for_unsupported_directive(
compose_directive.arguments.name,
subgraph.name.as_str(),
);
} else if let Some(subgraphs_with_conflict) =
tag_names_in_subgraphs.get_vec(&directive.name)
{
error_reporter.add_compose_directive_error_for_tag_conflict(
compose_directive.arguments.name,
subgraph.name.as_str(),
subgraphs_with_conflict,
);
} else if let Some(subgraphs_with_conflict) =
inaccessible_names_in_subgraphs.get_vec(&directive.name)
{
error_reporter.add_compose_directive_error_for_inaccessible_conflict(
compose_directive.arguments.name,
subgraph.name.as_str(),
subgraphs_with_conflict,
);
} else {
let item = MergeDirectiveItem::new(
subgraph.name.clone(),
directive.as_ref().clone(),
feature,
);
items_by_subgraph.insert(subgraph.name.clone(), item.clone());
items_by_directive_name.insert(directive.name.clone(), item.clone());
items_by_directive_name_in_spec
.insert(item.directive_name_in_spec().to_owned(), item);
}
}
Err(FederationError::SingleFederationError(
SingleFederationError::Internal { message },
)) if message.as_str()
== "Required argument \"name\" of directive \"@composeDirective\" was not present." =>
{
error_reporter
.add_compose_directive_error_for_empty_name(subgraph.name.as_str());
}
Err(e) => e.into_errors().into_iter().for_each(|err| {
error_reporter.add_error(CompositionError::InternalError {
message: err.to_string(),
});
}),
}
}
}
let all_identities: IndexSet<&Identity> = subgraphs
.iter()
.filter_map(|subgraph| {
Some(
subgraph
.schema()
.metadata()?
.links
.iter()
.map(|link| &link.url.identity),
)
})
.flatten()
.collect();
for identity in all_identities {
let subgraphs_used = subgraphs
.iter()
.filter_map(|subgraph| {
let items = items_by_subgraph.get(&subgraph.name)?;
if items.iter().any(|item| item.identity() == identity) {
Some(subgraph.name.clone())
} else {
None
}
})
.collect();
if let Some(latest) =
Self::get_latest_if_compatible(identity, subgraphs, &subgraphs_used, error_reporter)
{
self.latest_feature_map.insert(identity.clone(), latest);
} else {
wont_merge_features.insert(identity);
}
}
for (name, items) in items_by_directive_name.iter_all() {
if !Self::all_elements_equal(items, |item| item.directive_name_in_spec()) {
wont_merge_directive_names.insert(name.clone());
error_reporter.add_error(CompositionError::DirectiveCompositionError {
message: format!("Composed directive \"@{name}\" does not refer to the same directive in every subgraph"),
});
}
if !Self::all_elements_equal(items, |item| item.identity()) {
wont_merge_directive_names.insert(name.clone());
error_reporter.add_error(CompositionError::DirectiveCompositionError {
message: format!("Composed directive \"@{name}\" is not linked by the same core feature in every subgraph"),
});
}
}
for (name, items) in items_by_directive_name_in_spec.iter_all() {
if !Self::all_elements_equal(items, |item| item.directive_name()) {
for item in items {
wont_merge_directive_names.insert(item.directive_name().clone());
}
error_reporter
.add_compose_directive_error_for_inconsistent_imports(items, subgraphs);
}
let Some(item) = items.first() else {
continue;
};
let subgraphs_exporting_this_directive = items
.iter()
.map(|i| i.subgraph_name.clone())
.collect::<IndexSet<_>>();
let subgraphs_with_different_naming: Vec<_> = subgraphs
.iter()
.filter(|subgraph| {
if subgraphs_exporting_this_directive.contains(&subgraph.name) {
return false;
}
item.directive_has_different_name_in_subgraph(subgraph)
})
.collect();
if !subgraphs_with_different_naming.is_empty() {
error_reporter.add_hint(CompositionHint {
definition: HintCode::DirectiveCompositionWarn.definition(),
message: format!("Composed directive \"@{name}\" is named differently in a subgraph that doesn't export it. Consistent naming will be required to export it."),
locations: Self::locations_for_identity(item.identity(), subgraphs_with_different_naming),
});
}
}
for (subgraph, items) in items_by_subgraph.iter_all() {
let mut directives_for_subgraph = IndexSet::with_capacity(items.len());
for item in items {
if !wont_merge_features.contains(item.identity())
&& !wont_merge_directive_names.contains(item.directive_name())
{
directives_for_subgraph.insert(item.directive_name().clone());
}
self.directive_identity_map.insert(
item.directive_name().clone(),
(
item.identity().clone(),
item.directive_name_in_spec().to_owned(),
),
);
self.identity_directive_map
.entry(item.identity().clone())
.or_default()
.entry(item.directive_name().clone())
.or_insert(item.directive_name_in_spec().clone());
}
self.merge_directive_map
.insert(subgraph.clone(), directives_for_subgraph);
}
Ok(())
}
fn get_latest_if_compatible<T: HasMetadata>(
identity: &Identity,
subgraphs: &[Subgraph<T>],
subgraphs_used: &IndexSet<String>,
error_reporter: &mut ErrorReporter,
) -> Option<(Arc<Link>, IndexSet<String>)> {
let mut links_and_subgraphs: Vec<(Arc<Link>, &String)> = Vec::new();
let mut latest_link: Option<Arc<Link>> = None;
let mut any_composed = false;
let mut major_mismatch_hint_raised = false;
for subgraph in subgraphs {
let Some(link) = subgraph.schema().metadata()?.for_identity(identity) else {
continue;
};
links_and_subgraphs.push((link.clone(), &subgraph.name));
let composed = subgraphs_used.contains(&subgraph.name);
let Some(previous_latest_link) = &mut latest_link else {
latest_link = Some(link);
any_composed = composed;
continue;
};
if previous_latest_link.url.version.major != link.url.version.major {
if any_composed && composed {
latest_link = None;
any_composed = false;
error_reporter.add_error(CompositionError::DirectiveCompositionError {
message: format!("Core feature \"{identity}\" requested to be merged has major version mismatch across subgraphs")
});
break;
}
if !major_mismatch_hint_raised {
error_reporter.add_hint(CompositionHint {
definition: HintCode::DirectiveCompositionInfo.definition(),
message: format!("Non-composed core feature \"{identity}\" has major version mismatch across subgraphs"),
locations: Self::locations_for_identity(identity, subgraphs),
});
major_mismatch_hint_raised = true;
}
if !any_composed {
*previous_latest_link = link;
any_composed = composed;
}
continue;
}
if any_composed && !composed {
continue;
}
if !any_composed && composed {
*previous_latest_link = link;
any_composed = true;
continue;
}
if previous_latest_link.url.version.minor <= link.url.version.minor {
*previous_latest_link = link;
}
}
let latest_link = latest_link?;
if !any_composed {
return None;
}
let subgraph_names_with_latest_link = links_and_subgraphs
.into_iter()
.filter(|(link, _)| link.url.version == latest_link.url.version)
.map(|(_, subgraph_name)| subgraph_name.clone())
.rev()
.collect();
Some((latest_link, subgraph_names_with_latest_link))
}
fn all_elements_equal<'a, T: PartialEq>(
items: &'a [MergeDirectiveItem],
select: impl Fn(&'a MergeDirectiveItem) -> T,
) -> bool {
let mut value = None;
for item in items {
let item_value = select(item);
if let Some(value) = &value {
if *value != item_value {
return false;
}
} else {
value = Some(item_value);
}
}
true
}
fn locations_for_identity<'a, T: HasMetadata + 'a>(
identity: &Identity,
subgraphs: impl IntoIterator<Item = &'a Subgraph<T>>,
) -> Locations {
subgraphs
.into_iter()
.flat_map(|subgraph| {
let Some(metadata) = subgraph.schema().metadata() else {
return Locations::new();
};
let Some(link) = metadata.for_identity(identity) else {
return Locations::new();
};
link.locations(subgraph)
})
.collect()
}
}
impl ErrorReporter {
fn add_compose_directive_error_for_empty_name(&mut self, subgraph: &str) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!("Argument to @composeDirective in subgraph \"{subgraph}\" cannot be NULL or an empty String")
});
}
fn add_compose_directive_error_for_missing_start_symbol(
&mut self,
invalid_name: &str,
subgraph: &str,
) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!("Argument to @composeDirective \"{invalid_name}\" in subgraph \"{subgraph}\" must have a leading \"@\""),
});
}
fn add_compose_directive_error_for_unrecognized_feature(&mut self, name: &str, subgraph: &str) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!("Directive \"{name}\" in subgraph \"{subgraph}\" cannot be composed because it is not a member of a core feature")
});
}
fn add_compose_directive_hint_for_default_composed_directive(
&mut self,
name: &str,
locations: Vec<SubgraphLocation>,
) {
self.add_hint(CompositionHint {
definition: HintCode::DirectiveCompositionInfo.definition(),
message: format!("Directive \"{name}\" should not be explicitly manually composed since it is a federation directive composed by default"),
locations,
});
}
fn add_compose_directive_error_for_unsupported_directive(
&mut self,
name: &str,
subgraph: &str,
) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!("Composing federation directive \"{name}\" in subgraph \"{subgraph}\" is not supported")
});
}
fn add_compose_directive_error_for_tag_conflict(
&mut self,
name: &str,
subgraph: &str,
subgraphs_with_conflict: &[String],
) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!(
"Directive \"{name}\" in subgraph \"{subgraph}\" cannot be composed because it conflicts with automatically composed federation directive \"@tag\". Conflict exists in subgraph(s): ({})",
subgraphs_with_conflict.join(",")
)
});
}
fn add_compose_directive_error_for_inaccessible_conflict(
&mut self,
name: &str,
subgraph: &str,
subgraphs_with_conflict: &[String],
) {
self.add_error(CompositionError::DirectiveCompositionError {
message: format!(
"Directive \"{name}\" in subgraph \"{subgraph}\" cannot be composed because it conflicts with automatically composed federation directive \"@inaccessible\". Conflict exists in subgraph(s): ({})",
subgraphs_with_conflict.join(",")
),
});
}
fn add_compose_directive_error_for_undefined_directive<T: HasMetadata>(
&mut self,
name: &str,
subgraph: &Subgraph<T>,
) {
let words = suggestion_list(
name,
subgraph
.schema()
.schema()
.directive_definitions
.keys()
.map(|d| format!("@{d}")),
);
self.add_error(CompositionError::DirectiveCompositionError {
message: format!(
"Could not find matching directive definition for argument to @composeDirective \"{name}\" in subgraph \"{}\". {}",
subgraph.name,
did_you_mean(words),
),
});
}
fn add_compose_directive_error_for_inconsistent_imports<T: HasMetadata>(
&mut self,
items: &[MergeDirectiveItem],
subgraphs: &[Subgraph<T>],
) {
let sources = subgraphs
.iter()
.enumerate()
.map(|(idx, subgraph)| {
let item_in_this_subgraph = items
.iter()
.find(|item| item.subgraph_name == subgraph.name);
(idx, item_in_this_subgraph.cloned())
})
.collect();
self.report_mismatch_error_without_supergraph(
CompositionError::DirectiveCompositionError {
message: "Composed directive is not named consistently in all subgraphs"
.to_string(),
},
&sources,
subgraphs,
|elt, _| Some(format!("\"@{}\"", elt.directive_name())),
);
}
}