Skip to content
90 changes: 90 additions & 0 deletions apollo-federation/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,16 @@ pub enum SingleFederationError {
PlanningCancelled,
#[error("No plan was found when subgraphs were disabled")]
NoPlanFoundWithDisabledSubgraphs,
#[error("@cost cannot be applied to interface \"{interface}.{field}\"")]
CostAppliedToInterfaceField { interface: Name, field: Name },
#[error("{message}")]
ListSizeAppliedToNonList { message: String },
#[error("{message}")]
ListSizeInvalidAssumedSize { message: String },
#[error("{message}")]
ListSizeInvalidSlicingArgument { message: String },
#[error("{message}")]
ListSizeInvalidSizedField { message: String },
}

impl SingleFederationError {
Expand Down Expand Up @@ -536,6 +546,21 @@ impl SingleFederationError {
SingleFederationError::NoPlanFoundWithDisabledSubgraphs => {
ErrorCode::NoPlanFoundWithDisabledSubgraphs
}
SingleFederationError::CostAppliedToInterfaceField { .. } => {
ErrorCode::CostAppliedToInterfaceField
}
SingleFederationError::ListSizeAppliedToNonList { .. } => {
ErrorCode::ListSizeAppliedToNonList
}
SingleFederationError::ListSizeInvalidAssumedSize { .. } => {
ErrorCode::ListSizeInvalidAssumedSize
}
SingleFederationError::ListSizeInvalidSlicingArgument { .. } => {
ErrorCode::ListSizeInvalidSlicingArgument
}
SingleFederationError::ListSizeInvalidSizedField { .. } => {
ErrorCode::ListSizeInvalidSizedField
}
}
}

Expand Down Expand Up @@ -1621,6 +1646,61 @@ static NO_PLAN_FOUND_WITH_DISABLED_SUBGRAPHS: LazyLock<ErrorCodeDefinition> = La
)
});

static COST_APPLIED_TO_INTERFACE_FIELD: LazyLock<ErrorCodeDefinition> = LazyLock::new(|| {
ErrorCodeDefinition::new(
"COST_APPLIED_TO_INTERFACE_FIELD".to_owned(),
"The `@cost` directive must be applied to concrete types".to_owned(),
Some(ErrorCodeMetadata {
added_in: "2.9.2",
replaces: &[],
}),
)
});

static LIST_SIZE_APPLIED_TO_NON_LIST: LazyLock<ErrorCodeDefinition> = LazyLock::new(|| {
ErrorCodeDefinition::new(
"LIST_SIZE_APPLIED_TO_NON_LIST".to_owned(),
"The `@listSize` directive must be applied to list types".to_owned(),
Some(ErrorCodeMetadata {
added_in: "2.9.2",
replaces: &[],
}),
)
});

static LIST_SIZE_INVALID_ASSUMED_SIZE: LazyLock<ErrorCodeDefinition> = LazyLock::new(|| {
ErrorCodeDefinition::new(
"LIST_SIZE_INVALID_ASSUMED_SIZE".to_owned(),
"The `@listSize` directive assumed size cannot be negative".to_owned(),
Some(ErrorCodeMetadata {
added_in: "2.9.2",
replaces: &[],
}),
)
});

static LIST_SIZE_INVALID_SLICING_ARGUMENT: LazyLock<ErrorCodeDefinition> = LazyLock::new(|| {
ErrorCodeDefinition::new(
"LIST_SIZE_INVALID_SLICING_ARGUMENT".to_owned(),
"The `@listSize` directive must have existing integer slicing arguments".to_owned(),
Some(ErrorCodeMetadata {
added_in: "2.9.2",
replaces: &[],
}),
)
});

static LIST_SIZE_INVALID_SIZED_FIELD: LazyLock<ErrorCodeDefinition> = LazyLock::new(|| {
ErrorCodeDefinition::new(
"LIST_SIZE_INVALID_SIZED_FIELD".to_owned(),
"The `@listSize` directive must reference existing list fields as sized fields".to_owned(),
Some(ErrorCodeMetadata {
added_in: "2.9.2",
replaces: &[],
}),
)
});

#[derive(Debug, strum_macros::EnumIter)]
pub enum ErrorCode {
Internal,
Expand Down Expand Up @@ -1704,6 +1784,11 @@ pub enum ErrorCode {
UnsupportedFederationDirective,
QueryPlanComplexityExceededError,
NoPlanFoundWithDisabledSubgraphs,
CostAppliedToInterfaceField,
ListSizeAppliedToNonList,
ListSizeInvalidAssumedSize,
ListSizeInvalidSlicingArgument,
ListSizeInvalidSizedField,
}

impl ErrorCode {
Expand Down Expand Up @@ -1805,6 +1890,11 @@ impl ErrorCode {
ErrorCode::UnsupportedFederationDirective => &UNSUPPORTED_FEDERATION_DIRECTIVE,
ErrorCode::QueryPlanComplexityExceededError => &QUERY_PLAN_COMPLEXITY_EXCEEDED,
ErrorCode::NoPlanFoundWithDisabledSubgraphs => &NO_PLAN_FOUND_WITH_DISABLED_SUBGRAPHS,
ErrorCode::CostAppliedToInterfaceField => &COST_APPLIED_TO_INTERFACE_FIELD,
ErrorCode::ListSizeAppliedToNonList => &LIST_SIZE_APPLIED_TO_NON_LIST,
ErrorCode::ListSizeInvalidAssumedSize => &LIST_SIZE_INVALID_ASSUMED_SIZE,
ErrorCode::ListSizeInvalidSlicingArgument => &LIST_SIZE_INVALID_SLICING_ARGUMENT,
ErrorCode::ListSizeInvalidSizedField => &LIST_SIZE_INVALID_SIZED_FIELD,
}
}
}
110 changes: 99 additions & 11 deletions apollo-federation/src/link/cost_spec_definition.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use std::collections::HashSet;
use std::sync::LazyLock;

use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::ast::Argument;
use apollo_compiler::ast::Directive;
use apollo_compiler::ast::DirectiveList;
use apollo_compiler::ast::DirectiveLocation;
use apollo_compiler::ast::FieldDefinition;
use apollo_compiler::ast::InputValueDefinition;
use apollo_compiler::name;
use apollo_compiler::schema::Component;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::schema::Value;
use apollo_compiler::ty;
use indexmap::IndexSet;

use crate::error::FederationError;
use crate::internal_error;
Expand All @@ -21,9 +24,13 @@ use crate::link::spec::Version;
use crate::link::spec_definition::SpecDefinition;
use crate::link::spec_definition::SpecDefinitions;
use crate::schema::FederationSchema;
use crate::schema::argument_composition_strategies::ArgumentCompositionStrategy;
use crate::schema::position::EnumTypeDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
use crate::schema::position::ScalarTypeDefinitionPosition;
use crate::schema::type_and_directive_specification::ArgumentSpecification;
use crate::schema::type_and_directive_specification::DirectiveArgumentSpecification;
use crate::schema::type_and_directive_specification::DirectiveSpecification;
use crate::schema::type_and_directive_specification::TypeAndDirectiveSpecification;

const COST_DIRECTIVE_NAME: Name = name!("cost");
Expand Down Expand Up @@ -171,7 +178,9 @@ impl CostSpecDefinition {
/// Returns the name of the `@cost` directive in the given schema, accounting for import aliases or specification name
/// prefixes such as `@federation__cost`. This checks the linked cost specification, if there is one, and falls back
/// to the federation spec.
fn cost_directive_name(schema: &FederationSchema) -> Result<Option<Name>, FederationError> {
pub(crate) fn cost_directive_name(
schema: &FederationSchema,
) -> Result<Option<Name>, FederationError> {
if let Some(spec) = Self::for_federation_schema(schema) {
spec.directive_name_in_schema(schema, &COST_DIRECTIVE_NAME)
} else if let Ok(fed_spec) = get_federation_spec_definition_from_subgraph(schema) {
Expand All @@ -184,7 +193,7 @@ impl CostSpecDefinition {
/// Returns the name of the `@listSize` directive in the given schema, accounting for import aliases or specification name
/// prefixes such as `@federation__listSize`. This checks the linked cost specification, if there is one, and falls back
/// to the federation spec.
fn list_size_directive_name(
pub(crate) fn list_size_directive_name(
schema: &FederationSchema,
) -> Result<Option<Name>, FederationError> {
if let Some(spec) = Self::for_federation_schema(schema) {
Expand Down Expand Up @@ -235,6 +244,79 @@ impl CostSpecDefinition {
Ok(None)
}
}

fn cost_directive_specification() -> DirectiveSpecification {
DirectiveSpecification::new(
COST_DIRECTIVE_NAME,
&[DirectiveArgumentSpecification {
base_spec: ArgumentSpecification {
name: COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME,
get_type: |_, _| Ok(ty!(Int!)),
default_value: None,
},
composition_strategy: Some(ArgumentCompositionStrategy::Max),
}],
false,
&[
DirectiveLocation::ArgumentDefinition,
DirectiveLocation::Enum,
DirectiveLocation::FieldDefinition,
DirectiveLocation::InputFieldDefinition,
DirectiveLocation::Object,
DirectiveLocation::Scalar,
],
false,
// TODO: Set up supergraph spec later, but the type is hard to work with at the moment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be handled in a follow-up ticket

None,
None,
)
}

fn list_size_directive_specification() -> DirectiveSpecification {
DirectiveSpecification::new(
LIST_SIZE_DIRECTIVE_NAME,
&[
DirectiveArgumentSpecification {
base_spec: ArgumentSpecification {
name: LIST_SIZE_DIRECTIVE_ASSUMED_SIZE_ARGUMENT_NAME,
get_type: |_, _| Ok(ty!(Int)),
default_value: None,
},
composition_strategy: Some(ArgumentCompositionStrategy::Max),
},
DirectiveArgumentSpecification {
base_spec: ArgumentSpecification {
name: LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME,
get_type: |_, _| Ok(ty!([String!])),
default_value: None,
},
composition_strategy: Some(ArgumentCompositionStrategy::Union),
},
DirectiveArgumentSpecification {
base_spec: ArgumentSpecification {
name: LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME,
get_type: |_, _| Ok(ty!([String!])),
default_value: None,
},
composition_strategy: Some(ArgumentCompositionStrategy::Union),
},
DirectiveArgumentSpecification {
base_spec: ArgumentSpecification {
name: LIST_SIZE_DIRECTIVE_REQUIRE_ONE_SLICING_ARGUMENT_ARGUMENT_NAME,
get_type: |_, _| Ok(ty!(Boolean)),
default_value: Some(Value::Boolean(true)),
},
composition_strategy: Some(ArgumentCompositionStrategy::Max),
},
],
false,
&[DirectiveLocation::FieldDefinition],
false,
// TODO: Set up supergraph spec later, but the type is hard to work with at the moment
None,
None,
)
}
}

impl SpecDefinition for CostSpecDefinition {
Expand All @@ -243,11 +325,14 @@ impl SpecDefinition for CostSpecDefinition {
}

fn directive_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
todo!()
vec![
Box::new(Self::cost_directive_specification()),
Box::new(Self::list_size_directive_specification()),
]
}

fn type_specs(&self) -> Vec<Box<dyn TypeAndDirectiveSpecification>> {
todo!()
vec![]
}
}

Expand All @@ -267,15 +352,18 @@ impl CostDirective {
self.weight as f64
}

fn from_directives(directive_name: &Name, directives: &DirectiveList) -> Option<Self> {
pub(crate) fn from_directives(
directive_name: &Name,
directives: &DirectiveList,
) -> Option<Self> {
directives
.get(directive_name)?
.specified_argument_by_name(&COST_DIRECTIVE_WEIGHT_ARGUMENT_NAME)?
.to_i32()
.map(|weight| Self { weight })
}

fn from_schema_directives(
pub(crate) fn from_schema_directives(
directive_name: &Name,
directives: &apollo_compiler::schema::DirectiveList,
) -> Option<Self> {
Expand All @@ -289,8 +377,8 @@ impl CostDirective {

pub struct ListSizeDirective {
pub assumed_size: Option<i32>,
pub slicing_argument_names: Option<HashSet<String>>,
pub sized_fields: Option<HashSet<String>>,
pub slicing_argument_names: Option<IndexSet<String>>,
pub sized_fields: Option<IndexSet<String>>,
pub require_one_slicing_argument: bool,
}

Expand Down Expand Up @@ -320,7 +408,7 @@ impl ListSizeDirective {
.to_i32()
}

fn slicing_argument_names(directive: &Directive) -> Option<HashSet<String>> {
fn slicing_argument_names(directive: &Directive) -> Option<IndexSet<String>> {
let names = directive
.specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SLICING_ARGUMENTS_ARGUMENT_NAME)?
.as_list()?
Expand All @@ -331,7 +419,7 @@ impl ListSizeDirective {
Some(names)
}

fn sized_fields(directive: &Directive) -> Option<HashSet<String>> {
fn sized_fields(directive: &Directive) -> Option<IndexSet<String>> {
let fields = directive
.specified_argument_by_name(&LIST_SIZE_DIRECTIVE_SIZED_FIELDS_ARGUMENT_NAME)?
.as_list()?
Expand Down
19 changes: 18 additions & 1 deletion apollo-federation/src/schema/blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use apollo_compiler::ast::Directive;
use apollo_compiler::ast::NamedType;
use apollo_compiler::ast::OperationType;
use apollo_compiler::ty;
use itertools::Itertools;

use crate::bail;
use crate::error::FederationError;
Expand All @@ -14,12 +15,14 @@ use crate::error::SingleFederationError;
use crate::link::DEFAULT_LINK_NAME;
use crate::link::Import;
use crate::link::Purpose;
use crate::link::cost_spec_definition::COST_VERSIONS;
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::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;
Expand All @@ -29,8 +32,10 @@ use crate::schema::field_set::parse_field_set;
use crate::schema::position::DirectiveDefinitionPosition;
use crate::schema::position::InterfaceTypeDefinitionPosition;
use crate::schema::subgraph_metadata::SubgraphMetadata;
use crate::schema::validators::cost::validate_cost_directives;
use crate::schema::validators::external::validate_external_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::supergraph::GRAPHQL_MUTATION_TYPE_NAME;
Expand Down Expand Up @@ -158,6 +163,9 @@ impl FederationBlueprint {
)?;
Self::validate_interface_objects_are_on_entities(&schema, meta, &mut error_collector)?;

validate_cost_directives(&schema, &mut error_collector)?;
validate_list_size_directives(&schema, &mut error_collector)?;

error_collector.into_result().map(|_| schema)
}

Expand Down Expand Up @@ -245,9 +253,18 @@ impl FederationBlueprint {
return Ok(());
};

for _link in links_metadata.links.clone() {
for link in links_metadata.links.clone() {
// TODO: Pick out known features by link identity and call `add_elements_to_schema`.
// JS calls coreFeatureDefinitionIfKnown here, but we don't have a feature registry yet.

if link.url.identity == Identity::cost_identity() {
let spec = COST_VERSIONS
.find(&link.url.version)
.ok_or_else(|| SingleFederationError::UnknownLinkVersion {
message: format!("Detected unsupported cost specification version {}. Please upgrade to a composition version which supports that version, or select one of the following supported versions: {}.", link.url.version, COST_VERSIONS.versions().join(", "))
})?;
spec.add_elements_to_schema(schema)?;
}
}
Ok(())
}
Expand Down
Loading