use async_graphql::{ObjectType, Request, Response, Schema, SubscriptionType, Variables};
use serde_json::Value;
use std::sync::Arc;
use crate::error::GraphQLError;
pub struct GraphQLExecutor<Query, Mutation, Subscription> {
schema: Arc<Schema<Query, Mutation, Subscription>>,
}
impl<Query, Mutation, Subscription> std::fmt::Debug for GraphQLExecutor<Query, Mutation, Subscription> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GraphQLExecutor")
.field("schema", &"<async_graphql::Schema>")
.finish()
}
}
impl<Query, Mutation, Subscription> GraphQLExecutor<Query, Mutation, Subscription>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
#[must_use]
pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
Self {
schema: Arc::new(schema),
}
}
pub async fn execute(
&self,
query: &str,
variables: Option<&Value>,
operation_name: Option<&str>,
) -> Result<Value, GraphQLError> {
if query.trim().is_empty() {
return Err(GraphQLError::ValidationError(
"Query string cannot be empty".to_string(),
));
}
let mut request = Request::new(query);
if let Some(vars) = variables {
request = request.variables(Variables::from_json(vars.clone()));
}
if let Some(name) = operation_name {
request = request.operation_name(name);
}
let response = self.schema.execute(request).await;
if let Some(limit_error) = map_query_limit_error(&response) {
return Err(limit_error);
}
serde_json::to_value(response)
.map_err(|error| GraphQLError::SerializationError(format!("Failed to serialize GraphQL response: {error}")))
}
#[must_use]
pub const fn schema_ref(&self) -> &Arc<Schema<Query, Mutation, Subscription>> {
&self.schema
}
#[must_use]
pub fn clone_schema(&self) -> Arc<Schema<Query, Mutation, Subscription>> {
Arc::clone(&self.schema)
}
}
fn map_query_limit_error(response: &Response) -> Option<GraphQLError> {
for error in &response.errors {
let message = error.message.to_ascii_lowercase();
if message.contains("complexity") {
return Some(GraphQLError::ComplexityLimitExceeded);
}
if message.contains("depth") {
return Some(GraphQLError::DepthLimitExceeded);
}
if let Some(extensions) = &error.extensions {
let extension_text = format!("{extensions:?}").to_ascii_lowercase();
if extension_text.contains("complexity") {
return Some(GraphQLError::ComplexityLimitExceeded);
}
if extension_text.contains("depth") {
return Some(GraphQLError::DepthLimitExceeded);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use async_graphql::{EmptyMutation, EmptySubscription, Object};
#[derive(Default)]
struct TestQuery;
#[Object]
impl TestQuery {
async fn hello(&self) -> &'static str {
"world"
}
async fn greet(&self, name: String) -> String {
format!("Hello, {name}")
}
}
fn make_executor() -> GraphQLExecutor<TestQuery, EmptyMutation, EmptySubscription> {
let schema = Schema::build(TestQuery, EmptyMutation, EmptySubscription).finish();
GraphQLExecutor::new(schema)
}
#[tokio::test]
async fn test_executor_creation() {
let executor = make_executor();
let _ = executor.schema_ref();
let _ = executor.clone_schema();
}
#[tokio::test]
async fn test_execute_with_empty_query_fails() {
let executor = make_executor();
let result = executor.execute("", None, None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_with_variables() {
let executor = make_executor();
let variables = serde_json::json!({ "name": "Alice" });
let result = executor
.execute("query($name: String!) { greet(name: $name) }", Some(&variables), None)
.await
.expect("query should execute");
assert_eq!(result["data"]["greet"], "Hello, Alice");
}
#[tokio::test]
async fn test_execute_with_operation_name() {
let executor = make_executor();
let result = executor
.execute(
"query First { hello } query Second { greet(name: \"Bob\") }",
None,
Some("First"),
)
.await
.expect("query with operation name should execute");
assert_eq!(result["data"]["hello"], "world");
}
}