use crate::ast::Type;
use crate::ast::Value;
use crate::executable::Field;
use crate::executable::Operation;
use crate::execution::engine::LinkedPath;
use crate::execution::engine::PropagateNull;
use crate::execution::GraphQLError;
use crate::execution::JsonMap;
use crate::execution::JsonValue;
use crate::execution::Response;
use crate::node::NodeLocation;
use crate::schema::ExtendedType;
use crate::schema::FieldDefinition;
use crate::validation::SuspectedValidationBug;
use crate::validation::Valid;
use crate::ExecutableDocument;
use crate::Node;
use crate::Schema;
use crate::SourceMap;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum InputCoercionError {
SuspectedValidationBug(SuspectedValidationBug),
ValueError {
message: String,
location: Option<NodeLocation>,
},
}
pub 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, "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("variable default value", "", "", 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))
}
#[allow(clippy::too_many_arguments)] fn coerce_variable_value(
schema: &Valid<Schema>,
kind: &str,
parent: &str,
sep: &str,
name: &str,
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 {kind} {parent}{sep}{name} 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, kind, parent, sep, name, 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 {kind} {parent}{sep}{name}"),
location: ty_name.location(),
})?
};
match ty_def {
ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
Err(SuspectedValidationBug {
message: format!("Non-input type {ty_name} for {kind} {parent}{sep}{name}."),
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() {
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,
"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(
"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 {kind} {parent}{sep}{name}: {value} to type {ty_name}"),
location: None,
})
}
fn graphql_value_to_json(
kind: &str,
parent: &str,
sep: &str,
name: &str,
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 {kind} {parent}{sep}{name}."),
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!("IntValue overflow in {kind} {parent}{sep}{name}"),
location: value.location(),
}
})?)),
Value::Float(f) => Ok(JsonValue::Number(f.as_str().parse().map_err(|_| {
InputCoercionError::ValueError {
message: format!("FloatValue overflow in {kind} {parent}{sep}{name}"),
location: value.location(),
}
})?)),
Value::List(value) => value
.iter()
.map(|value| graphql_value_to_json(kind, parent, sep, name, value))
.collect(),
Value::Object(value) => value
.iter()
.map(|(key, value)| {
Ok((
key.as_str(),
graphql_value_to_json(kind, parent, sep, name, value)?,
))
})
.collect(),
}
}
pub(crate) fn coerce_argument_values(
schema: &Schema,
document: &Valid<ExecutableDocument>,
variable_values: &Valid<JsonMap>,
errors: &mut Vec<GraphQLError>,
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) = variable_values.get(var_name.as_str()) {
if var_value.is_null() && arg_def.ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!("null value for non-nullable argument {arg_name}"),
path,
arg_def.location(),
&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() {
errors.push(GraphQLError::field_error(
format!("null value for non-nullable argument {arg_name}"),
path,
arg_def.location(),
&document.sources,
));
return Err(PropagateNull);
} else {
let coerced_value = coerce_argument_value(
schema,
document,
variable_values,
errors,
path,
"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("argument", "", "", arg_name, default).map_err(|err| {
errors.push(err.into_field_error(path, &document.sources));
PropagateNull
})?;
coerced_values.insert(arg_def.name.as_str(), value);
continue;
}
if arg_def.ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!("missing value for required argument {arg_name}"),
path,
arg_def.location(),
&document.sources,
));
return Err(PropagateNull);
}
}
Ok(coerced_values)
}
#[allow(clippy::too_many_arguments)] fn coerce_argument_value(
schema: &Schema,
document: &Valid<ExecutableDocument>,
variable_values: &Valid<JsonMap>,
errors: &mut Vec<GraphQLError>,
path: LinkedPath<'_>,
kind: &str,
parent: &str,
sep: &str,
name: &str,
ty: &Type,
value: &Node<Value>,
) -> Result<JsonValue, PropagateNull> {
if value.is_null() {
if ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!("null value for non-null {kind} {parent}{sep}{name}"),
path,
value.location(),
&document.sources,
));
return Err(PropagateNull);
} else {
return Ok(JsonValue::Null);
}
}
if let Some(var_name) = value.as_variable() {
if let Some(var_value) = variable_values.get(var_name.as_str()) {
if var_value.is_null() && ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!("null variable value for non-null {kind} {parent}{sep}{name}"),
path,
value.location(),
&document.sources,
));
return Err(PropagateNull);
} else {
return Ok(var_value.clone());
}
} else if ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!("missing variable for non-null {kind} {parent}{sep}{name}"),
path,
value.location(),
&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(
schema,
document,
variable_values,
errors,
path,
kind,
parent,
sep,
name,
inner_ty,
item,
)
})
.collect();
}
Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name,
};
let Some(ty_def) = schema.types.get(ty_name) else {
errors.push(
SuspectedValidationBug {
message: format!("Undefined type {ty_name} for {kind} {parent}{sep}{name}"),
location: value.location(),
}
.into_field_error(&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))
{
errors.push(GraphQLError::field_error(
format!("Input object has key {key} not in type {ty_name}",),
path,
value.location(),
&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(
schema,
document,
variable_values,
errors,
path,
"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("input field", ty_name, ".", field_name, default)
.map_err(|err| {
errors.push(err.into_field_error(path, &document.sources));
PropagateNull
})?;
coerced_object.insert(field_name.as_str(), default);
} else if field_def.ty.is_non_null() {
errors.push(GraphQLError::field_error(
format!(
"Missing value for non-null input object field {ty_name}.{field_name}"
),
path,
value.location(),
&document.sources,
));
return Err(PropagateNull);
} else {
}
}
return Ok(coerced_object.into());
}
}
_ => {
return graphql_value_to_json(kind, parent, sep, name, value).map_err(|err| {
errors.push(err.into_field_error(path, &document.sources));
PropagateNull
});
}
}
errors.push(GraphQLError::field_error(
format!("Could not coerce {kind} {parent}{sep}{name}: {value} to type {ty_name}"),
path,
value.location(),
&document.sources,
));
Err(PropagateNull)
}
impl From<SuspectedValidationBug> for InputCoercionError {
fn from(value: SuspectedValidationBug) -> Self {
Self::SuspectedValidationBug(value)
}
}
impl InputCoercionError {
pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
match self {
Self::SuspectedValidationBug(s) => s.into_graphql_error(sources),
Self::ValueError { message, location } => GraphQLError::new(message, location, sources),
}
}
pub fn into_response(self, sources: &SourceMap) -> Response {
Response::from_request_error(self.into_graphql_error(sources))
}
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)
}
}
}
}