use std::collections::HashSet;
use std::sync::Arc;
use apollo_compiler::Name;
use apollo_compiler::ast::Directive;
use apollo_compiler::schema::Component;
use apollo_compiler::ty;
use crate::bail;
use crate::error::FederationError;
use crate::error::MultipleFederationErrors;
use crate::error::SingleFederationError;
use crate::error::suggestion::did_you_mean;
use crate::error::suggestion::suggestion_list;
use crate::link::DEFAULT_LINK_NAME;
use crate::link::Link;
use crate::link::federation_spec_definition::FED_1;
use crate::link::federation_spec_definition::FEDERATION_FIELDS_ARGUMENT_NAME;
use crate::link::federation_spec_definition::FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC;
use crate::link::federation_spec_definition::FEDERATION_VERSIONS;
use crate::link::federation_spec_definition::FederationSpecDefinition;
use crate::link::federation_spec_definition::fed1_link_imports;
use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph;
use crate::link::link_spec_definition::LinkSpecDefinition;
use crate::link::spec::Identity;
use crate::link::spec::Url;
use crate::link::spec_definition::SpecDefinition;
use crate::schema::FederationSchema;
use crate::schema::ValidFederationSchema;
use crate::schema::compute_subgraph_metadata;
use crate::schema::position::DirectiveDefinitionPosition;
use crate::schema::validators::access_control::validate_no_access_control_on_interfaces;
use crate::schema::validators::cache_tag::validate_cache_tag_directives;
use crate::schema::validators::context::validate_context_directives;
use crate::schema::validators::cost::validate_cost_directives;
use crate::schema::validators::external::validate_external_directives;
use crate::schema::validators::from_context::validate_from_context_directives;
use crate::schema::validators::interface_object::validate_interface_object_directives;
use crate::schema::validators::key::validate_key_directives;
use crate::schema::validators::list_size::validate_list_size_directives;
use crate::schema::validators::provides::validate_provides_directives;
use crate::schema::validators::requires::validate_requires_directives;
use crate::schema::validators::shareable::validate_shareable_directives;
use crate::schema::validators::tag::validate_tag_directives;
use crate::subgraph::typestate::has_federation_spec_link;
use crate::supergraph::FEDERATION_ENTITIES_FIELD_NAME;
use crate::supergraph::FEDERATION_SERVICE_FIELD_NAME;
pub(crate) struct FederationBlueprint {}
impl FederationBlueprint {
pub(crate) fn on_missing_directive_definition(
schema: &mut FederationSchema,
directive: &Component<Directive>,
) -> Result<Option<DirectiveDefinitionPosition>, FederationError> {
if directive.name == DEFAULT_LINK_NAME {
let (alias, imports) =
LinkSpecDefinition::extract_alias_and_imports_on_missing_link_directive_definition(
directive,
)?;
LinkSpecDefinition::latest().add_definitions_to_schema(schema, alias, imports)?;
Ok(schema.get_directive_definition(&directive.name))
} else {
Ok(None)
}
}
pub(crate) fn on_directive_definition_and_schema_parsed(
schema: &mut FederationSchema,
) -> Result<(), FederationError> {
Self::complete_subgraph_schema(schema)
}
#[allow(unused)]
pub(crate) fn ignore_parsed_field(schema: &FederationSchema, field_name: &str) -> bool {
if !(FEDERATION_OPERATION_FIELDS.iter().any(|f| *f == field_name)) {
return false;
}
if let Some(metadata) = &schema.subgraph_metadata {
!metadata.is_fed_2_schema()
} else {
false
}
}
pub(crate) fn on_constructed(schema: &mut FederationSchema) -> Result<(), FederationError> {
if schema.subgraph_metadata.is_none() {
schema.subgraph_metadata = compute_subgraph_metadata(schema)?.map(Box::new);
}
Ok(())
}
#[allow(unused)]
fn on_added_core_feature(
schema: &mut FederationSchema,
feature: &Link,
) -> Result<(), FederationError> {
if feature.url.identity == Identity::federation_identity() {
FEDERATION_VERSIONS
.find(&feature.url.version)
.iter()
.try_for_each(|spec| spec.add_elements_to_schema(schema))?;
}
Ok(())
}
pub(crate) fn on_validation(schema: &ValidFederationSchema) -> Result<(), FederationError> {
let mut error_collector = MultipleFederationErrors { errors: Vec::new() };
let Some(meta) = schema.subgraph_metadata() else {
bail!("ValidFederationSchema should contain subgraph metadata");
};
if !meta.is_fed_2_schema() {
return error_collector.into_result();
}
let context_map = validate_context_directives(schema, &mut error_collector)?;
validate_from_context_directives(schema, meta, &context_map, &mut error_collector)?;
validate_key_directives(schema, meta, &mut error_collector)?;
validate_provides_directives(schema, meta, &mut error_collector)?;
validate_requires_directives(schema, meta, &mut error_collector)?;
validate_external_directives(schema, meta, &mut error_collector)?;
validate_interface_object_directives(schema, meta, &mut error_collector)?;
validate_shareable_directives(schema, meta, &mut error_collector)?;
validate_cost_directives(schema, &mut error_collector)?;
validate_list_size_directives(schema, &mut error_collector)?;
validate_tag_directives(schema, &mut error_collector)?;
validate_cache_tag_directives(schema, &mut error_collector)?;
validate_no_access_control_on_interfaces(schema, meta, &mut error_collector)?;
error_collector.into_result()
}
pub(crate) fn on_invalid_graphql_error(
schema: &FederationSchema,
message: String,
) -> SingleFederationError {
let matcher = regex::Regex::new(r#"^Error: cannot find directive `@([^`]+)`"#).unwrap();
let Some(capture) = matcher.captures(&message) else {
return SingleFederationError::InvalidGraphQL { message };
};
let Some(matched) = capture.get(1) else {
return SingleFederationError::InvalidGraphQL { message };
};
let directive_name = matched.as_str();
let options: Vec<_> = schema
.get_directive_definitions()
.map(|d| d.directive_name.to_string())
.collect();
let suggestions = suggestion_list(directive_name, options);
if suggestions.is_empty() {
return Self::on_unknown_directive_validation_error(schema, directive_name, &message);
}
let did_you_mean = did_you_mean(suggestions.iter().map(|s| format!("@{s}")));
SingleFederationError::InvalidGraphQL {
message: format!("{message}{did_you_mean}\n"),
}
}
fn on_unknown_directive_validation_error(
schema: &FederationSchema,
unknown_directive_name: &str,
error_message: &str,
) -> SingleFederationError {
let Some(metadata) = &schema.subgraph_metadata else {
return SingleFederationError::Internal {
message: "Missing subgraph metadata".to_string(),
};
};
let is_fed2 = metadata.is_fed_2_schema();
let all_directive_names = all_default_federation_directive_names();
if all_directive_names.contains(unknown_directive_name) {
if !is_fed2 {
return SingleFederationError::InvalidGraphQL {
message: format!(
r#"{error_message} If you meant the "@{unknown_directive_name}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specification v2."#
),
};
}
let Ok(Some(name_in_schema)) = metadata
.federation_spec_definition()
.directive_name_in_schema(schema, &Name::new_unchecked(unknown_directive_name))
else {
return SingleFederationError::Internal {
message: format!(
"Unexpectedly could not find directive \"@{unknown_directive_name}\" in schema"
),
};
};
let federation_link_name = &metadata.federation_spec_definition().identity().name;
let federation_prefix = format!("{federation_link_name}__");
if name_in_schema.starts_with(&federation_prefix) {
return SingleFederationError::InvalidGraphQL {
message: format!(
r#"{error_message} If you meant the "@{unknown_directive_name}" federation directive, you should use fully-qualified name "@{name_in_schema}" or add "@{unknown_directive_name}" to the \`import\` argument of the @link to the federation specification."#
),
};
} else {
return SingleFederationError::InvalidGraphQL {
message: format!(
r#"{error_message} If you meant the "@{unknown_directive_name}" federation directive, you should use "@{name_in_schema}" as it is imported under that name in the @link to the federation specification of this schema."#
),
};
}
} else if !is_fed2 {
let suggestions = suggestion_list(
unknown_directive_name,
all_directive_names.iter().map(|name| name.to_string()),
);
if !suggestions.is_empty() {
let did_you_mean = did_you_mean(suggestions.iter().map(|s| format!("@{s}")));
let note = if suggestions.len() == 1 {
"it is a federation 2 directive"
} else {
"they are federation 2 directives"
};
return SingleFederationError::InvalidGraphQL {
message: format!(
"{error_message}{did_you_mean} If so, note that {note} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specification v2."
),
};
}
}
SingleFederationError::InvalidGraphQL {
message: error_message.to_string(),
}
}
pub(crate) fn complete_subgraph_schema(
schema: &mut FederationSchema,
) -> Result<(), FederationError> {
if schema.is_fed_2() {
Self::complete_fed_2_subgraph_schema(schema)
} else if has_federation_spec_link(schema.schema()) {
LinkSpecDefinition::latest().add_to_schema(schema, None)?;
Self::complete_fed_2_subgraph_schema(schema)
} else {
Self::complete_fed_1_subgraph_schema(schema)
}
}
fn complete_fed_2_subgraph_schema(
schema: &mut FederationSchema,
) -> Result<(), FederationError> {
let federation_spec = get_federation_spec_definition_from_subgraph(schema)?;
federation_spec.add_elements_to_schema(schema)?;
Self::expand_known_features(schema)
}
fn complete_fed_1_subgraph_schema(
schema: &mut FederationSchema,
) -> Result<(), FederationError> {
Self::remove_federation_definitions_broken_in_known_ways(schema)?;
let mut errors = MultipleFederationErrors { errors: vec![] };
let fed_1_link_spec_definition = LinkSpecDefinition::fed1_latest();
let fed_1_link = Arc::new(Link {
url: Url {
identity: fed_1_link_spec_definition.url().identity.clone(),
version: fed_1_link_spec_definition.url().version.clone(),
},
imports: fed1_link_imports(),
spec_alias: None,
purpose: None,
});
for type_spec in &FED_1.type_specs() {
if let Err(err) = type_spec.check_or_add(schema, Some(&fed_1_link)) {
errors.push(err);
}
}
for directive_spec in &FED_1.directive_specs() {
if let Err(err) = directive_spec.check_or_add(schema, Some(&fed_1_link)) {
errors.push(err);
}
}
match errors.errors.as_slice() {
[] => Self::expand_known_features(schema),
[error] => Err(FederationError::SingleFederationError(error.clone())),
_ => Err(FederationError::MultipleFederationErrors(errors)),
}
}
fn remove_federation_definitions_broken_in_known_ways(
schema: &mut FederationSchema,
) -> Result<(), FederationError> {
for directive_name in &[
FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC,
FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC,
FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC,
] {
if let Some(pos) = schema.get_directive_definition(directive_name) {
let directive = pos.get(schema.schema())?;
if !schema
.referencers()
.get_directive(directive_name)
.is_empty()
{
bail!(
"Subgraph has applications of @{directive_name} but we are trying to remove the definition."
);
}
if directive.arguments.is_empty()
|| (directive.arguments.len() == 1
&& directive
.argument_by_name(&FEDERATION_FIELDS_ARGUMENT_NAME)
.is_some_and(|fields| {
*fields.ty == ty!(String)
|| *fields.ty == ty!(_FieldSet)
|| *fields.ty == ty!(FieldSet)
}))
{
pos.remove(schema)?;
}
}
}
Ok(())
}
fn expand_known_features(schema: &mut FederationSchema) -> Result<(), FederationError> {
for feature in schema.all_features()? {
feature.add_elements_to_schema(schema)?;
}
Ok(())
}
#[allow(unused)]
fn apply_directives_after_parsing() -> bool {
true
}
}
pub(crate) const FEDERATION_OPERATION_FIELDS: [Name; 2] = [
FEDERATION_SERVICE_FIELD_NAME,
FEDERATION_ENTITIES_FIELD_NAME,
];
fn all_default_federation_directive_names() -> HashSet<Name> {
FederationSpecDefinition::latest()
.directive_specs()
.iter()
.map(|spec| spec.name().clone())
.collect()
}