use crate::ast::Type;
use crate::ast::Value;
use crate::collections::HashMap;
use crate::executable::Field;
use crate::executable::Operation;
use crate::parser::SourceMap;
use crate::parser::SourceSpan;
use crate::resolvers::execution::ExecutionContext;
use crate::resolvers::execution::LinkedPath;
use crate::resolvers::execution::PropagateNull;
use crate::response::GraphQLError;
use crate::response::JsonMap;
use crate::response::JsonValue;
use crate::schema::ExtendedType;
use crate::schema::FieldDefinition;
use crate::validation::SuspectedValidationBug;
use crate::validation::Valid;
use crate::Node;
use crate::Schema;
const MAX_SAFE_INT: i64 = (1_i64 << 53) - 1;
#[derive(Debug, Clone)]
pub(crate) enum InputCoercionError {
SuspectedValidationBug(SuspectedValidationBug),
ValueError {
message: String,
location: Option<SourceSpan>,
},
}
pub(crate) fn coerce_variable_values(
schema: &Valid<Schema>,
operation: &Operation,
values: &JsonMap,
) -> Result<Valid<JsonMap>, InputCoercionError> {
let mut coerced_values = JsonMap::new();
for variable_def in &operation.variables {
let name = variable_def.name.as_str();
if let Some((key, value)) = values.get_key_value(name) {
let value = coerce_variable_value(
schema,
&format_args!("variable {name}"),
&variable_def.ty,
value,
)?;
coerced_values.insert(key.clone(), value);
} else if let Some(default) = &variable_def.default_value {
let value =
graphql_value_to_json(&format_args!("default value of variable {name}"), default)?;
coerced_values.insert(name, value);
} else if variable_def.ty.is_non_null() {
return Err(InputCoercionError::ValueError {
message: format!("missing value for non-null variable '{name}'"),
location: variable_def.location(),
});
} else {
}
}
Ok(Valid(coerced_values))
}
fn coerce_variable_value(
schema: &Valid<Schema>,
description: &std::fmt::Arguments<'_>,
ty: &Type,
value: &JsonValue,
) -> Result<JsonValue, InputCoercionError> {
if value.is_null() {
if ty.is_non_null() {
return Err(InputCoercionError::ValueError {
message: format!("null value for {description} of non-null type {ty}"),
location: None,
});
} else {
return Ok(JsonValue::Null);
}
}
let ty_name = match ty {
Type::List(inner) | Type::NonNullList(inner) => {
return value
.as_array()
.map(Vec::as_slice)
.unwrap_or(std::slice::from_ref(value))
.iter()
.map(|item| coerce_variable_value(schema, description, inner, item))
.collect();
}
Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name,
};
let Some(ty_def) = schema.types.get(ty_name) else {
Err(SuspectedValidationBug {
message: format!("undefined type {ty_name} for {description}"),
location: ty_name.location(),
})?
};
match ty_def {
ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
Err(SuspectedValidationBug {
message: format!("non-input type {ty_name} for {description}."),
location: ty_name.location(),
})?
}
ExtendedType::Scalar(_) => match ty_name.as_str() {
"Int" => {
if value
.as_i64()
.is_some_and(|value| i32::try_from(value).is_ok())
{
return Ok(value.clone());
}
}
"Float" => {
if value.is_f64()
|| value
.as_f64()
.is_some_and(|f| f.abs() < MAX_SAFE_INT as f64)
{
return Ok(value.clone());
}
}
"String" => {
if value.is_string() {
return Ok(value.clone());
}
}
"Boolean" => {
if value.is_boolean() {
return Ok(value.clone());
}
}
"ID" => {
if value.is_string() || value.is_i64() {
return Ok(value.clone());
}
}
_ => {
return Ok(value.clone());
}
},
ExtendedType::Enum(ty_def) => {
if let Some(str) = value.as_str() {
if ty_def.values.keys().any(|value_name| value_name == str) {
return Ok(value.clone());
}
}
}
ExtendedType::InputObject(ty_def) => {
if let Some(object) = value.as_object() {
if let Some(key) = object
.keys()
.find(|key| !ty_def.fields.contains_key(key.as_str()))
{
return Err(InputCoercionError::ValueError {
message: format!(
"Input object has key {} not in type {ty_name}",
key.as_str()
),
location: None,
});
}
let mut object = object.clone();
for (field_name, field_def) in &ty_def.fields {
if let Some(field_value) = object.get_mut(field_name.as_str()) {
*field_value = coerce_variable_value(
schema,
&format_args!("input field {ty_name}.{field_name}"),
&field_def.ty,
field_value,
)?
} else if let Some(default) = &field_def.default_value {
let default = graphql_value_to_json(
&format_args!("input field {ty_name}.{field_name}"),
default,
)?;
object.insert(field_name.as_str(), default);
} else if field_def.ty.is_non_null() {
return Err(InputCoercionError::ValueError {
message: format!("Missing value for non-null input object field {ty_name}.{field_name}"),
location: None,
});
} else {
}
}
return Ok(object.into());
}
}
}
Err(InputCoercionError::ValueError {
message: format!("could not coerce {description}: {value} to type {ty_name}"),
location: None,
})
}
fn graphql_value_to_json(
description: &std::fmt::Arguments<'_>,
value: &Node<Value>,
) -> Result<JsonValue, InputCoercionError> {
match value.as_ref() {
Value::Null => Ok(JsonValue::Null),
Value::Variable(_) => {
Err(InputCoercionError::SuspectedValidationBug(
SuspectedValidationBug {
message: format!("variable in default value of {description}."),
location: value.location(),
},
))
}
Value::Enum(value) => Ok(value.as_str().into()),
Value::String(value) => Ok(value.as_str().into()),
Value::Boolean(value) => Ok((*value).into()),
Value::Int(i) => Ok(JsonValue::Number(i.as_str().parse().map_err(|_| {
InputCoercionError::ValueError {
message: format!("int value overflow in {description}"),
location: value.location(),
}
})?)),
Value::Float(f) => Ok(JsonValue::Number(f.as_str().parse().map_err(|_| {
InputCoercionError::ValueError {
message: format!("float value overflow in {description}"),
location: value.location(),
}
})?)),
Value::List(value) => value
.iter()
.map(|value| graphql_value_to_json(description, value))
.collect(),
Value::Object(value) => value
.iter()
.map(|(key, value)| Ok((key.as_str(), graphql_value_to_json(description, value)?)))
.collect(),
}
}
pub(crate) fn coerce_argument_values(
ctx: &mut ExecutionContext<'_>,
path: LinkedPath<'_>,
field_def: &FieldDefinition,
field: &Field,
) -> Result<JsonMap, PropagateNull> {
let mut coerced_values = JsonMap::new();
for arg_def in &field_def.arguments {
let arg_name = &arg_def.name;
if let Some(arg) = field.arguments.iter().find(|arg| arg.name == *arg_name) {
if let Value::Variable(var_name) = arg.value.as_ref() {
if let Some(var_value) = ctx.variable_values.get(var_name.as_str()) {
if var_value.is_null() && arg_def.ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("null value for non-nullable argument {arg_name}"),
path,
arg_def.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
coerced_values.insert(arg_name.as_str(), var_value.clone());
continue;
}
}
} else if arg.value.is_null() && arg_def.ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("null value for non-nullable argument {arg_name}"),
path,
arg_def.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
let coerced_value = coerce_argument_value(
ctx,
path,
&format_args!("argument {arg_name}"),
&arg_def.ty,
&arg.value,
)?;
coerced_values.insert(arg_name.as_str(), coerced_value);
continue;
}
}
if let Some(default) = &arg_def.default_value {
let value = graphql_value_to_json(&format_args!("argument {arg_name}"), default)
.map_err(|err| {
ctx.errors
.push(err.into_field_error(path, &ctx.document.sources));
PropagateNull
})?;
coerced_values.insert(arg_def.name.as_str(), value);
continue;
}
if arg_def.ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("missing value for required argument {arg_name}"),
path,
arg_def.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
}
}
Ok(coerced_values)
}
fn coerce_argument_value(
ctx: &mut ExecutionContext<'_>,
path: LinkedPath<'_>,
description: &std::fmt::Arguments<'_>,
ty: &Type,
value: &Node<Value>,
) -> Result<JsonValue, PropagateNull> {
if value.is_null() {
if ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("null value for non-null {description}"),
path,
value.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
return Ok(JsonValue::Null);
}
}
if let Some(var_name) = value.as_variable() {
if let Some(var_value) = ctx.variable_values.get(var_name.as_str()) {
if var_value.is_null() && ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("null variable value for non-null {description}"),
path,
value.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
return Ok(var_value.clone());
}
} else if ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!("missing variable for non-null {description}"),
path,
value.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
return Ok(JsonValue::Null);
}
}
let ty_name = match ty {
Type::List(inner_ty) | Type::NonNullList(inner_ty) => {
return value
.as_list()
.unwrap_or(std::slice::from_ref(value))
.iter()
.map(|item| coerce_argument_value(ctx, path, description, inner_ty, item))
.collect();
}
Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name,
};
let Some(ty_def) = ctx.schema.types.get(ty_name) else {
ctx.errors.push(
SuspectedValidationBug {
message: format!("undefined type {ty_name} for {description}"),
location: value.location(),
}
.into_field_error(&ctx.document.sources, path),
);
return Err(PropagateNull);
};
match ty_def {
ExtendedType::InputObject(ty_def) => {
if let Some(object) = value.as_object() {
if let Some((key, _value)) = object
.iter()
.find(|(key, _value)| !ty_def.fields.contains_key(key))
{
ctx.errors.push(GraphQLError::field_error(
format!("input object has key {key} not in type {ty_name}",),
path,
value.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
}
#[allow(clippy::map_identity)] let object: HashMap<_, _> = object.iter().map(|(k, v)| (k, v)).collect();
let mut coerced_object = JsonMap::new();
for (field_name, field_def) in &ty_def.fields {
if let Some(field_value) = object.get(field_name) {
let coerced_value = coerce_argument_value(
ctx,
path,
&format_args!("input field {ty_name}.{field_name}"),
&field_def.ty,
field_value,
)?;
coerced_object.insert(field_name.as_str(), coerced_value);
} else if let Some(default) = &field_def.default_value {
let default = graphql_value_to_json(
&format_args!("input field {ty_name}.{field_name}"),
default,
)
.map_err(|err| {
ctx.errors
.push(err.into_field_error(path, &ctx.document.sources));
PropagateNull
})?;
coerced_object.insert(field_name.as_str(), default);
} else if field_def.ty.is_non_null() {
ctx.errors.push(GraphQLError::field_error(
format!(
"Missing value for non-null input object field {ty_name}.{field_name}"
),
path,
value.location(),
&ctx.document.sources,
));
return Err(PropagateNull);
} else {
}
}
return Ok(coerced_object.into());
}
}
_ => {
return graphql_value_to_json(description, value).map_err(|err| {
ctx.errors
.push(err.into_field_error(path, &ctx.document.sources));
PropagateNull
});
}
}
ctx.errors.push(GraphQLError::field_error(
format!("could not coerce {description}: {value} to type {ty_name}"),
path,
value.location(),
&ctx.document.sources,
));
Err(PropagateNull)
}
impl From<SuspectedValidationBug> for InputCoercionError {
fn from(value: SuspectedValidationBug) -> Self {
Self::SuspectedValidationBug(value)
}
}
impl InputCoercionError {
pub(crate) fn into_field_error(
self,
path: LinkedPath<'_>,
sources: &SourceMap,
) -> GraphQLError {
match self {
Self::SuspectedValidationBug(s) => s.into_field_error(sources, path),
Self::ValueError { message, location } => {
GraphQLError::field_error(message, path, location, sources)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::Valid;
use crate::ExecutableDocument;
use crate::Schema;
fn schema_and_doc_with_float_arg() -> (Valid<Schema>, Valid<ExecutableDocument>) {
let schema = Schema::parse_and_validate(
r#"
type Query {
foo(bar: Float!): Float!
}
"#,
"sdl",
)
.unwrap();
let doc = ExecutableDocument::parse_and_validate(
&schema,
"query ($bar: Float!) { foo(bar: $bar) }",
"op.graphql",
)
.unwrap();
(schema, doc)
}
#[test]
fn coerces_float_to_float() {
let float_beyond_integer_max = (MAX_SAFE_INT as f64) + 0.5;
let variables = serde_json_bytes::json!({ "bar": float_beyond_integer_max });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap();
}
#[test]
fn coerces_int_to_float() {
let variables = serde_json_bytes::json!({ "bar": 14 });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap();
}
#[test]
fn fails_to_coerce_int_to_float_beyond_precision_bound() {
let variables = serde_json_bytes::json!({ "bar": i64::MAX });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap_err();
}
#[test]
fn fails_to_numeric_string_to_float() {
let variables = serde_json_bytes::json!({ "bar": "14" });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap_err();
}
#[test]
fn fails_to_coerce_inf_to_float() {
let variables = serde_json_bytes::json!({ "bar": f64::INFINITY });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap_err();
}
#[test]
fn fails_to_coerce_nan_to_float() {
let variables = serde_json_bytes::json!({ "bar": f64::NAN });
let (schema, doc) = schema_and_doc_with_float_arg();
let _ = coerce_variable_values(
&schema,
doc.operations.anonymous.as_ref().unwrap(),
variables.as_object().unwrap(),
)
.unwrap_err();
}
}