use apollo_compiler::ExecutableDocument;
use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::Schema;
use apollo_compiler::ast::DirectiveDefinition;
use apollo_compiler::ast::Value;
use apollo_compiler::collections::IndexMap;
use apollo_compiler::executable;
use apollo_compiler::schema;
use apollo_compiler::schema::Directive;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::schema::InputValueDefinition;
use apollo_compiler::schema::Type;
use apollo_compiler::validation::Valid;
use either::Either;
use crate::error::FederationError;
use crate::error::MultipleFederationErrors;
use crate::error::SingleFederationError;
use crate::schema::position::DirectiveDefinitionPosition;
use crate::schema::position::InputObjectFieldDefinitionPosition;
use crate::schema::position::InterfaceFieldDefinitionPosition;
use crate::schema::position::ObjectFieldDefinitionPosition;
use crate::schema::position::ObjectOrInterfaceFieldDefinitionPosition;
fn is_semantic_directive_application(directive: &Directive) -> bool {
match directive.name.as_str() {
"specifiedBy" => true,
"deprecated"
if directive
.specified_argument_by_name("reason")
.is_some_and(|value| value.is_null()) =>
{
false
}
"deprecated" => true,
_ => false,
}
}
fn standardize_deprecated(directive: &mut Directive) {
if directive.name == "deprecated"
&& directive
.specified_argument_by_name("reason")
.and_then(|value| value.as_str())
.is_some_and(|reason| reason == "No longer supported")
{
directive.arguments.clear();
}
}
fn retain_semantic_directives(directives: &mut schema::DirectiveList) {
directives
.0
.retain(|directive| is_semantic_directive_application(directive));
for directive in directives {
standardize_deprecated(directive.make_mut());
}
}
fn retain_semantic_directives_ast(directives: &mut apollo_compiler::ast::DirectiveList) {
directives
.0
.retain(|directive| is_semantic_directive_application(directive));
for directive in directives {
standardize_deprecated(directive.make_mut());
}
}
pub(crate) fn remove_non_semantic_directives(schema: &mut Schema) {
let root_definitions = schema.schema_definition.make_mut();
retain_semantic_directives(&mut root_definitions.directives);
for ty in schema.types.values_mut() {
match ty {
ExtendedType::Object(object) => {
let object = object.make_mut();
retain_semantic_directives(&mut object.directives);
for field in object.fields.values_mut() {
let field = field.make_mut();
retain_semantic_directives_ast(&mut field.directives);
for arg in &mut field.arguments {
let arg = arg.make_mut();
retain_semantic_directives_ast(&mut arg.directives);
}
}
}
ExtendedType::Interface(interface) => {
let interface = interface.make_mut();
retain_semantic_directives(&mut interface.directives);
for field in interface.fields.values_mut() {
let field = field.make_mut();
retain_semantic_directives_ast(&mut field.directives);
for arg in &mut field.arguments {
let arg = arg.make_mut();
retain_semantic_directives_ast(&mut arg.directives);
}
}
}
ExtendedType::InputObject(input_object) => {
let input_object = input_object.make_mut();
retain_semantic_directives(&mut input_object.directives);
for field in input_object.fields.values_mut() {
let field = field.make_mut();
retain_semantic_directives_ast(&mut field.directives);
}
}
ExtendedType::Union(union_) => {
let union_ = union_.make_mut();
retain_semantic_directives(&mut union_.directives);
}
ExtendedType::Scalar(scalar) => {
let scalar = scalar.make_mut();
retain_semantic_directives(&mut scalar.directives);
}
ExtendedType::Enum(enum_) => {
let enum_ = enum_.make_mut();
retain_semantic_directives(&mut enum_.directives);
for value in enum_.values.values_mut() {
let value = value.make_mut();
retain_semantic_directives_ast(&mut value.directives);
}
}
}
}
for directive in schema.directive_definitions.values_mut() {
let directive = directive.make_mut();
for arg in &mut directive.arguments {
let arg = arg.make_mut();
retain_semantic_directives_ast(&mut arg.directives);
}
}
}
type CoerceResult = Result<(), ()>;
#[derive(Default)]
struct CoerceOptions {
coerce_non_list_values_for_list_types: bool,
}
fn coerce_value(
types: &IndexMap<Name, ExtendedType>,
target: &mut Node<Value>,
ty: &Type,
options: &CoerceOptions,
) -> CoerceResult {
match ty {
Type::NonNullNamed(ty) => {
if target.is_null() {
return Err(());
}
coerce_value(types, target, &Type::Named(ty.clone()), options)
}
Type::NonNullList(ty) => {
if target.is_null() {
return Err(());
}
coerce_value(types, target, &Type::List(ty.clone()), options)
}
Type::List(ty) => {
if target.is_null() {
return Ok(());
}
if matches!(target.as_ref(), Value::Variable(_)) {
return Ok(());
}
if matches!(target.as_ref(), Value::List(_)) {
let Value::List(target) = target.make_mut() else {
return Err(());
};
for element in target {
coerce_value(types, element, ty, options)?;
}
} else {
coerce_value(types, target, ty, options)?;
if options.coerce_non_list_values_for_list_types {
*target.make_mut() = Value::List(vec![target.clone()]);
}
}
Ok(())
}
Type::Named(ty) => {
if target.is_null() {
return Ok(());
}
if matches!(target.as_ref(), Value::Variable(_)) {
return Ok(());
}
let Some(ty) = types.get(ty) else {
return Err(());
};
match ty {
ExtendedType::InputObject(ty) => {
let Value::Object(target) = target.make_mut() else {
return Err(());
};
let mut is_error = false;
for (field, target) in target.iter_mut() {
let Some(definition) = ty.fields.get(field) else {
is_error = true;
continue;
};
if coerce_value(types, target, &definition.ty, options).is_err() {
is_error = true;
}
}
if is_error {
return Err(());
}
Ok(())
}
ExtendedType::Scalar(ty) => {
if ty.is_built_in() {
match ty.name.as_str() {
"Int" => {
if !matches!(target.as_ref(), Value::Int(_)) {
return Err(());
}
}
"Float" => {
if !matches!(target.as_ref(), Value::Int(_) | Value::Float(_)) {
return Err(());
}
}
"String" => {
if let Value::Enum(name) = target.as_ref() {
*target.make_mut() = Value::String(name.to_string());
}
if !matches!(target.as_ref(), Value::String(_)) {
return Err(());
}
}
"Boolean" => {
if !matches!(target.as_ref(), Value::Boolean(_)) {
return Err(());
}
}
"ID" => {
if !matches!(target.as_ref(), Value::Int(_) | Value::String(_)) {
return Err(());
}
}
_ => {
return Err(());
}
}
}
Ok(())
}
ExtendedType::Enum(ty) => {
if let Value::String(name) = target.as_ref() {
let Ok(name) = Name::new(name) else {
return Err(());
};
*target.make_mut() = Value::Enum(name);
}
let Value::Enum(name) = target.as_ref() else {
return Err(());
};
if !ty.values.contains_key(name) {
return Err(());
}
Ok(())
}
ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
Err(())
}
}
}
}
}
fn coerce_and_validate_arguments_default_values(
types: &IndexMap<Name, ExtendedType>,
arguments: &mut Vec<Node<InputValueDefinition>>,
position: &Either<ObjectOrInterfaceFieldDefinitionPosition, DirectiveDefinitionPosition>,
errors: &mut Vec<SingleFederationError>,
) {
for arg in arguments {
let name = arg.name.clone();
let coordinate = match &position {
Either::Left(ObjectOrInterfaceFieldDefinitionPosition::Object(position)) => {
position.argument(name).to_string()
}
Either::Left(ObjectOrInterfaceFieldDefinitionPosition::Interface(position)) => {
position.argument(name).to_string()
}
Either::Right(position) => position.argument(name).to_string(),
};
let arg = arg.make_mut();
if arg.is_required() && arg.directives.has("deprecated") {
errors.push(SingleFederationError::InvalidGraphQL {
message: format!("Required argument {} cannot be deprecated.", coordinate,),
})
}
let Some(default_value) = &mut arg.default_value else {
continue;
};
if coerce_value(types, default_value, &arg.ty, &Default::default()).is_err() {
errors.push(SingleFederationError::InvalidGraphQL {
message: format!(
"Invalid default value (got: {}) provided for argument {} of type {}.",
default_value.serialize().no_indent(),
coordinate,
arg.ty
),
})
}
}
}
pub fn coerce_and_validate_schema_values(schema: &mut Schema) -> Result<(), FederationError> {
let mut errors: Vec<SingleFederationError> = Default::default();
let types = schema.types.clone();
let directive_definitions = schema.directive_definitions.clone();
for ty in schema.types.values_mut() {
match ty {
ExtendedType::Object(object) => {
let object = object.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut object.directives,
);
for field in object.fields.values_mut() {
let field = field.make_mut();
coerce_and_validate_arguments_default_values(
&types,
&mut field.arguments,
&Either::Left(
ObjectFieldDefinitionPosition {
type_name: object.name.clone(),
field_name: field.name.clone(),
}
.into(),
),
&mut errors,
);
coerce_directive_application_values_ast(
&directive_definitions,
&types,
&mut field.directives,
);
coerce_argument_directive_application_values(
&directive_definitions,
&types,
&mut field.arguments,
);
}
}
ExtendedType::Interface(interface) => {
let interface = interface.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut interface.directives,
);
for field in interface.fields.values_mut() {
let field = field.make_mut();
coerce_and_validate_arguments_default_values(
&types,
&mut field.arguments,
&Either::Left(
InterfaceFieldDefinitionPosition {
type_name: interface.name.clone(),
field_name: field.name.clone(),
}
.into(),
),
&mut errors,
);
coerce_directive_application_values_ast(
&directive_definitions,
&types,
&mut field.directives,
);
coerce_argument_directive_application_values(
&directive_definitions,
&types,
&mut field.arguments,
);
}
}
ExtendedType::InputObject(input_object) => {
let input_object = input_object.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut input_object.directives,
);
for field in input_object.fields.values_mut() {
let field = field.make_mut();
let coordinate = InputObjectFieldDefinitionPosition {
type_name: input_object.name.clone(),
field_name: field.name.clone(),
}
.to_string();
coerce_directive_application_values_ast(
&directive_definitions,
&types,
&mut field.directives,
);
if field.is_required() && field.directives.has("deprecated") {
errors.push(SingleFederationError::InvalidGraphQL {
message: format!(
"Required argument {} cannot be deprecated.",
coordinate,
),
})
}
let Some(default_value) = &mut field.default_value else {
continue;
};
if coerce_value(&types, default_value, &field.ty, &Default::default()).is_err()
{
errors.push(SingleFederationError::InvalidGraphQL {
message: format!(
"Invalid default value (got: {}) provided for input field {} of type {}.",
default_value.serialize().no_indent(),
coordinate,
field.ty
)
})
}
}
}
ExtendedType::Union(union_) => {
let union_ = union_.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut union_.directives,
);
}
ExtendedType::Scalar(scalar) => {
let scalar = scalar.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut scalar.directives,
);
}
ExtendedType::Enum(enum_) => {
let enum_ = enum_.make_mut();
coerce_directive_application_values_schema(
&directive_definitions,
&types,
&mut enum_.directives,
);
for value in enum_.values.values_mut() {
let value = value.make_mut();
coerce_directive_application_values_ast(
&directive_definitions,
&types,
&mut value.directives,
);
}
}
}
}
for directive in schema.directive_definitions.values_mut() {
let directive = directive.make_mut();
coerce_and_validate_arguments_default_values(
&types,
&mut directive.arguments,
&Either::Right(DirectiveDefinitionPosition {
directive_name: directive.name.clone(),
}),
&mut errors,
);
coerce_argument_directive_application_values(
&directive_definitions,
&types,
&mut directive.arguments,
);
}
if !errors.is_empty() {
return Err(MultipleFederationErrors { errors }.into());
}
Ok(())
}
fn coerce_directive_application_values(
schema: &Valid<Schema>,
directives: &mut executable::DirectiveList,
) {
for directive in directives {
let Some(definition) = schema.directive_definitions.get(&directive.name) else {
continue;
};
let directive = directive.make_mut();
for arg in &mut directive.arguments {
let Some(definition) = definition.argument_by_name(&arg.name) else {
continue;
};
let arg = arg.make_mut();
_ = coerce_value(
&schema.types,
&mut arg.value,
&definition.ty,
&CoerceOptions {
coerce_non_list_values_for_list_types: true,
},
);
}
}
}
fn coerce_directive_application_values_schema(
directive_definitions: &IndexMap<Name, Node<DirectiveDefinition>>,
type_definitions: &IndexMap<Name, ExtendedType>,
directives: &mut schema::DirectiveList,
) {
for directive in directives {
let Some(definition) = directive_definitions.get(&directive.name) else {
continue;
};
let directive = directive.make_mut();
for arg in &mut directive.arguments {
let Some(definition) = definition.argument_by_name(&arg.name) else {
continue;
};
let arg = arg.make_mut();
_ = coerce_value(
type_definitions,
&mut arg.value,
&definition.ty,
&Default::default(),
);
}
}
}
fn coerce_directive_application_values_ast(
directive_definitions: &IndexMap<Name, Node<DirectiveDefinition>>,
type_definitions: &IndexMap<Name, ExtendedType>,
directives: &mut apollo_compiler::ast::DirectiveList,
) {
for directive in directives {
let Some(definition) = directive_definitions.get(&directive.name) else {
continue;
};
let directive = directive.make_mut();
for arg in &mut directive.arguments {
let Some(definition) = definition.argument_by_name(&arg.name) else {
continue;
};
let arg = arg.make_mut();
_ = coerce_value(
type_definitions,
&mut arg.value,
&definition.ty,
&Default::default(),
);
}
}
}
fn coerce_argument_directive_application_values(
directive_definitions: &IndexMap<Name, Node<DirectiveDefinition>>,
type_definitions: &IndexMap<Name, ExtendedType>,
arguments: &mut [Node<InputValueDefinition>],
) {
for arg in arguments {
let arg = arg.make_mut();
coerce_directive_application_values_ast(
directive_definitions,
type_definitions,
&mut arg.directives,
);
}
}
fn coerce_selection_set_values(
schema: &Valid<Schema>,
selection_set: &mut executable::SelectionSet,
) {
for selection in &mut selection_set.selections {
match selection {
executable::Selection::Field(field) => {
let definition = field.definition.clone(); let field = field.make_mut();
for arg in &mut field.arguments {
let Some(definition) = definition.argument_by_name(&arg.name) else {
continue;
};
let arg = arg.make_mut();
_ = coerce_value(
&schema.types,
&mut arg.value,
&definition.ty,
&CoerceOptions {
coerce_non_list_values_for_list_types: true,
},
);
}
coerce_directive_application_values(schema, &mut field.directives);
coerce_selection_set_values(schema, &mut field.selection_set);
}
executable::Selection::FragmentSpread(frag) => {
let frag = frag.make_mut();
coerce_directive_application_values(schema, &mut frag.directives);
}
executable::Selection::InlineFragment(frag) => {
let frag = frag.make_mut();
coerce_directive_application_values(schema, &mut frag.directives);
coerce_selection_set_values(schema, &mut frag.selection_set);
}
}
}
}
fn coerce_operation_values(schema: &Valid<Schema>, operation: &mut Node<executable::Operation>) {
let operation = operation.make_mut();
for variable in &mut operation.variables {
let variable = variable.make_mut();
let Some(default_value) = &mut variable.default_value else {
continue;
};
_ = coerce_value(
&schema.types,
default_value,
&variable.ty,
&CoerceOptions {
coerce_non_list_values_for_list_types: true,
},
);
}
coerce_selection_set_values(schema, &mut operation.selection_set);
}
pub(crate) fn coerce_executable_values(schema: &Valid<Schema>, document: &mut ExecutableDocument) {
if let Some(operation) = &mut document.operations.anonymous {
coerce_operation_values(schema, operation);
}
for operation in document.operations.named.values_mut() {
coerce_operation_values(schema, operation);
}
for fragment in document.fragments.values_mut() {
let fragment = fragment.make_mut();
coerce_directive_application_values(schema, &mut fragment.directives);
coerce_selection_set_values(schema, &mut fragment.selection_set);
}
}
pub(crate) fn make_print_schema_compatible(schema: &mut Schema) {
remove_non_semantic_directives(schema);
}
#[cfg(test)]
mod tests {
use apollo_compiler::ExecutableDocument;
use apollo_compiler::Schema;
use apollo_compiler::validation::Valid;
use super::coerce_executable_values;
fn parse_and_coerce(schema: &Valid<Schema>, input: &str) -> String {
let mut document = ExecutableDocument::parse(schema, input, "test.graphql").unwrap();
coerce_executable_values(schema, &mut document);
document.to_string()
}
#[test]
fn coerces_list_values() {
let schema = Schema::parse_and_validate(
r#"
type Query {
test(
bools: [Boolean],
ints: [Int],
strings: [String],
floats: [Float],
): Int
}
"#,
"schema.graphql",
)
.unwrap();
insta::assert_snapshot!(parse_and_coerce(&schema, r#"
{
test(bools: true, ints: 1, strings: "string", floats: 2.0)
}
"#), @r#"
{
test(bools: [true], ints: [1], strings: ["string"], floats: [2.0])
}
"#);
}
#[test]
fn coerces_enum_values() {
let schema = Schema::parse_and_validate(
r#"
scalar CustomScalar
type Query {
test(
string: String!,
strings: [String!]!,
custom: CustomScalar!,
customList: [CustomScalar!]!,
): Int
}
"#,
"schema.graphql",
)
.unwrap();
insta::assert_snapshot!(parse_and_coerce(&schema, r#"
{
test(string: enumVal1, strings: enumVal2, custom: enumVal1, customList: enumVal2)
}
"#), @r#"
{
test(string: "enumVal1", strings: ["enumVal2"], custom: enumVal1, customList: [enumVal2])
}
"#);
}
#[test]
fn coerces_enum_string_in_field_argument_directive() {
let mut schema = Schema::parse(
r#"
directive @d(x: E!) on ARGUMENT_DEFINITION
enum E { A B }
type Query { f(a: Int @d(x: "A")): Int }
"#,
"schema.graphql",
)
.expect("valid subgraph schema");
super::coerce_and_validate_schema_values(&mut schema).unwrap();
let sdl = schema.to_string();
insta::assert_snapshot!(sdl, @"
directive @d(x: E!) on ARGUMENT_DEFINITION
enum E {
A
B
}
type Query {
f(
a: Int @d(x: A),
): Int
}
");
}
#[test]
fn coerces_in_fragment_definitions() {
let schema = Schema::parse_and_validate(
r#"
type T {
get(bools: [Boolean!]!): Int
}
type Query {
test: T
}
"#,
"schema.graphql",
)
.unwrap();
insta::assert_snapshot!(parse_and_coerce(&schema, r#"
{
test {
...f
}
}
fragment f on T {
get(bools: true)
}
"#), @"
{
test {
...f
}
}
fragment f on T {
get(bools: [true])
}
");
}
}