use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::ast::FieldDefinition;
use apollo_compiler::ast::InputValueDefinition;
use apollo_compiler::collections::IndexSet;
use apollo_compiler::schema::Component;
use apollo_compiler::schema::EnumType;
use apollo_compiler::schema::EnumValueDefinition;
use crate::bail;
use crate::error::CompositionError;
use crate::error::FederationError;
use crate::link::inaccessible_spec_definition::IsInaccessibleExt;
use crate::merger::hints::HintCode;
use crate::merger::merge::Merger;
use crate::merger::merge::Sources;
use crate::merger::merge::map_sources;
use crate::schema::position::EnumTypeDefinitionPosition;
use crate::schema::position::EnumValueDefinitionPosition;
use crate::supergraph::CompositionHint;
#[derive(Debug, Clone)]
pub(crate) enum EnumExampleAst {
#[allow(dead_code)]
Field(Node<FieldDefinition>),
#[allow(dead_code)]
Input(Node<InputValueDefinition>),
}
#[derive(Debug, Clone)]
pub(crate) struct EnumExample {
#[allow(dead_code)]
pub coordinate: String,
#[allow(dead_code)]
pub element_ast: Option<EnumExampleAst>,
}
#[derive(Debug, Clone)]
pub(crate) enum EnumTypeUsage {
#[allow(dead_code)]
Input {
input_example: EnumExample,
},
#[allow(dead_code)]
Output {
output_example: EnumExample,
},
#[allow(dead_code)]
Both {
input_example: EnumExample,
output_example: EnumExample,
},
Unused,
}
impl Merger {
#[allow(dead_code)]
pub(crate) fn merge_enum(
&mut self,
sources: Sources<Node<EnumType>>,
dest: &EnumTypeDefinitionPosition,
) -> Result<(), FederationError> {
let usage = self.enum_usages.get(dest.type_name.as_str()).cloned().unwrap_or_else(|| {
let usage = EnumTypeUsage::Unused;
self.error_reporter.add_hint(CompositionHint {
code: HintCode::UnusedEnumType.code().to_string(),
message: format!(
"Enum type \"{}\" is defined but unused. It will be included in the supergraph with all the values appearing in any subgraph (\"as if\" it was only used as an output type).",
dest.type_name
),
locations: Default::default(), });
usage
});
let mut enum_values: IndexSet<Name> = Default::default();
enum_values.extend(
sources
.iter()
.filter_map(|(_, source)| source.as_ref())
.flat_map(|source| source.values.values())
.map(|value| value.node.value.clone()),
);
for value_name in enum_values {
let value_pos = dest.value(value_name);
self.merge_enum_value(&sources, &value_pos, &usage)?;
}
if dest.get(self.merged.schema())?.values.is_empty() {
self.error_reporter.add_error(CompositionError::EmptyMergedEnumType {
message: format!(
"None of the values of enum type \"{}\" are defined consistently in all the subgraphs defining that type. As only values common to all subgraphs are merged, this would result in an empty type.",
dest.type_name
),
locations: self.source_locations(&sources),
});
}
Ok(())
}
fn merge_enum_value(
&mut self,
sources: &Sources<Node<EnumType>>,
value_pos: &EnumValueDefinitionPosition,
usage: &EnumTypeUsage,
) -> Result<(), FederationError> {
let value_sources: Sources<&Component<EnumValueDefinition>> = sources
.iter()
.map(|(&idx, source)| {
let source_value = source
.as_ref()
.and_then(|enum_type| enum_type.values.get(&value_pos.value_name));
(idx, source_value)
})
.collect();
let dest = Component::new(EnumValueDefinition {
description: None,
value: value_pos.value_name.clone(),
directives: Default::default(),
});
value_pos.insert(&mut self.merged, dest)?;
let pos_sources = map_sources(sources, |source| source.as_ref().map(|_| value_pos.clone()));
self.merge_description(&pos_sources, value_pos)?;
self.record_applied_directives_to_merge(&pos_sources, value_pos)?;
self.add_join_enum_value(&value_sources, value_pos)?;
let is_inaccessible = match &self.inaccessible_directive_name_in_supergraph {
Some(name) => value_pos.is_inaccessible(&self.merged, name)?,
None => false,
};
let violates_intersection_requirement = !is_inaccessible
&& sources.values().any(|source| {
source
.as_ref()
.is_some_and(|enum_type| !enum_type.values.contains_key(&value_pos.value_name))
});
match usage {
EnumTypeUsage::Both {
input_example,
output_example,
} if violates_intersection_requirement => {
self.error_reporter.report_mismatch_error_with_specifics(
CompositionError::EnumValueMismatch {
message: format!(
"Enum type \"{}\" is used as both input type (for example, as type of \"{}\") and output type (for example, as type of \"{}\"), but value \"{}\" is not defined in all the subgraphs defining \"{}\": ",
&value_pos.type_name, input_example.coordinate, output_example.coordinate, &value_pos.value_name, &value_pos.type_name
),
},
&value_pos,
sources,
&self.subgraphs,
|_| Some("yes".to_string()),
|source, _| {
if source.values.contains_key(&value_pos.value_name) {
Some("yes".to_string())
} else {
Some("no".to_string())
}
},
|_, subgraphs| format!("\"{}\" is defined in {}", value_pos.value_name, subgraphs.unwrap_or_else(|| "no subgraphs".to_string())),
|_, subgraphs| format!(" but not in {subgraphs}"),
false,
);
}
EnumTypeUsage::Input { .. } if violates_intersection_requirement => {
self.error_reporter.report_mismatch_hint(
HintCode::InconsistentEnumValueForInputEnum,
format!(
"Value \"{}\" of enum type \"{}\" will not be part of the supergraph as it is not defined in all the subgraphs defining \"{}\": ", value_pos.value_name, value_pos.type_name, value_pos.type_name
),
&value_pos,
sources,
&self.subgraphs,
|_| Some("yes".to_string()),
|source, _| {
if source.values.contains_key(&value_pos.value_name) {
Some("yes".to_string())
} else {
Some("no".to_string())
}
},
|_, subgraphs| format!("\"{}\" is defined in {}", value_pos.value_name, subgraphs.unwrap_or_else(|| "no subgraphs".to_string())),
|_, subgraphs| format!(" but not in {subgraphs}"),
false,
false,
);
value_pos.remove(&mut self.merged)?;
}
EnumTypeUsage::Output { .. } | EnumTypeUsage::Unused => {
self.hint_on_inconsistent_output_enum_value(
sources,
&value_pos.type_name,
&value_pos.value_name,
);
}
_ => {
}
}
Ok(())
}
fn add_join_enum_value(
&mut self,
sources: &Sources<&Component<EnumValueDefinition>>,
value_pos: &EnumValueDefinitionPosition,
) -> Result<(), FederationError> {
for (&idx, source) in sources.iter() {
if source.is_some() {
let subgraph_name = &self.names[idx];
let Some(join_spec_name) = self.subgraph_names_to_join_spec_name.get(subgraph_name)
else {
bail!(
"Could not find join spec name for subgraph '{}'",
subgraph_name
);
};
let directive = self
.join_spec_definition
.enum_value_directive(&self.merged, join_spec_name)?;
let _ = value_pos.insert_directive(&mut self.merged, Node::new(directive));
}
}
Ok(())
}
fn hint_on_inconsistent_output_enum_value(
&mut self,
sources: &Sources<Node<EnumType>>,
dest_name: &Name,
value_name: &Name,
) {
for enum_type in sources.values().flatten() {
if !enum_type.values.contains_key(value_name) {
self.error_reporter.report_mismatch_hint(
HintCode::InconsistentEnumValueForOutputEnum,
format!(
"Value \"{value_name}\" of enum type \"{dest_name}\" has been added to the supergraph but is only defined in a subset of the subgraphs defining \"{dest_name}\": ",
),
dest_name,
sources,
&self.subgraphs,
|_| Some("yes".to_string()),
|source, _| {
if source.values.contains_key(value_name) {
Some("yes".to_string())
} else {
Some("no".to_string())
}
},
|_, subgraphs| format!("\"{}\" is defined in {}", value_name, subgraphs.unwrap_or_else(|| "no subgraphs".to_string())),
|_, subgraphs| format!(" but not in {subgraphs}"),
false,
false,
);
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use apollo_compiler::Node;
use apollo_compiler::Schema;
use apollo_compiler::name;
use apollo_compiler::schema::ComponentName;
use apollo_compiler::schema::InterfaceType;
use apollo_compiler::schema::ObjectType;
use apollo_compiler::schema::UnionType;
use super::*;
use crate::JOIN_VERSIONS;
use crate::SpecDefinition;
use crate::link::federation_spec_definition::FEDERATION_VERSIONS;
use crate::link::link_spec_definition::LINK_VERSIONS;
use crate::link::spec::Version;
use crate::merger::compose_directive_manager::ComposeDirectiveManager;
use crate::merger::error_reporter::ErrorReporter;
use crate::merger::merge::CompositionOptions;
use crate::schema::FederationSchema;
use crate::schema::position::EnumTypeDefinitionPosition;
use crate::utils::FallibleOnceCell;
fn insert_enum_type(schema: &mut FederationSchema, name: Name) -> Result<(), FederationError> {
let status_pos = EnumTypeDefinitionPosition {
type_name: name.clone(),
};
let dest = Node::new(EnumType {
name: name.clone(),
description: None,
directives: Default::default(),
values: Default::default(),
});
status_pos.pre_insert(schema)?;
status_pos.insert(schema, dest)?;
Ok(())
}
pub(crate) fn create_test_merger() -> Result<Merger, FederationError> {
let link_spec_definition = LINK_VERSIONS
.find(&Version { major: 1, minor: 0 })
.expect("LINK_VERSIONS should have version 1.0");
let join_spec_definition = JOIN_VERSIONS
.find(&Version { major: 0, minor: 5 })
.expect("JOIN_VERSIONS should have version 0.5");
let schema = Schema::builder()
.adopt_orphan_extensions()
.parse(
r#"
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
enum join__Graph {
A @join__graph(name: "A", url: "http://localhost:4002/")
B @join__graph(name: "B", url: "http://localhost:4003/")
}
scalar link__Import
enum link__Purpose {
SECURITY
EXECUTION
}
"#,
"",
)
.build()?;
let mut schema = FederationSchema::new(schema)?;
insert_enum_type(&mut schema, name!("Status"))?;
insert_enum_type(&mut schema, name!("UnusedStatus"))?;
let interface_pos = crate::schema::position::InterfaceTypeDefinitionPosition {
type_name: name!("I"),
};
let interface_type = Node::new(InterfaceType {
description: None,
name: name!("I"),
implements_interfaces: Default::default(),
directives: Default::default(),
fields: Default::default(),
});
interface_pos.pre_insert(&mut schema)?;
interface_pos.insert(&mut schema, interface_type)?;
let object_pos = crate::schema::position::ObjectTypeDefinitionPosition {
type_name: name!("A"),
};
let mut object_type = ObjectType {
description: None,
name: name!("A"),
implements_interfaces: Default::default(),
directives: Default::default(),
fields: Default::default(),
};
object_type
.implements_interfaces
.insert(ComponentName::from(name!("I")));
object_pos.pre_insert(&mut schema)?;
object_pos.insert(&mut schema, Node::new(object_type))?;
let union_pos = crate::schema::position::UnionTypeDefinitionPosition {
type_name: name!("U"),
};
let mut union_type = UnionType {
description: None,
name: name!("U"),
directives: Default::default(),
members: Default::default(),
};
union_type.members.insert(ComponentName::from(name!("A")));
union_pos.pre_insert(&mut schema)?;
union_pos.insert(&mut schema, Node::new(union_type))?;
Ok(Merger {
subgraphs: vec![],
options: CompositionOptions::default(),
names: vec!["subgraph1".to_string(), "subgraph2".to_string()],
compose_directive_manager: ComposeDirectiveManager::new(),
error_reporter: ErrorReporter::new(vec![
"subgraph1".to_string(),
"subgraph2".to_string(),
]),
merged: schema,
subgraph_names_to_join_spec_name: [
(
"subgraph1".to_string(),
Name::new("SUBGRAPH1").expect("Valid name"),
),
(
"subgraph2".to_string(),
Name::new("SUBGRAPH2").expect("Valid name"),
),
]
.into_iter()
.collect(),
merged_federation_directive_names: Default::default(),
merged_federation_directive_in_supergraph_by_directive_name: Default::default(),
enum_usages: Default::default(),
fields_with_from_context: Default::default(),
fields_with_override: Default::default(),
inaccessible_directive_name_in_supergraph: None,
link_spec_definition,
join_spec_definition,
join_directive_identities: Default::default(),
directives_using_join_directive: Default::default(),
schema_to_import_to_feature_url: Default::default(),
latest_federation_version_used: FEDERATION_VERSIONS.latest().version().clone(),
applied_directives_to_merge: Default::default(),
access_control_directives_in_supergraph: Default::default(),
access_control_additional_sources: FallibleOnceCell::new(),
})
}
}