spikard_graphql/
executor.rs1use async_graphql::{ObjectType, Request, Response, Schema, SubscriptionType, Variables};
7use serde_json::Value;
8use std::sync::Arc;
9
10use crate::error::GraphQLError;
11
12pub struct GraphQLExecutor<Query, Mutation, Subscription> {
20 schema: Arc<Schema<Query, Mutation, Subscription>>,
22}
23
24impl<Query, Mutation, Subscription> std::fmt::Debug for GraphQLExecutor<Query, Mutation, Subscription> {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("GraphQLExecutor")
27 .field("schema", &"<async_graphql::Schema>")
28 .finish()
29 }
30}
31
32impl<Query, Mutation, Subscription> GraphQLExecutor<Query, Mutation, Subscription>
33where
34 Query: ObjectType + Send + Sync + 'static,
35 Mutation: ObjectType + Send + Sync + 'static,
36 Subscription: SubscriptionType + Send + Sync + 'static,
37{
38 #[must_use]
40 pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
41 Self {
42 schema: Arc::new(schema),
43 }
44 }
45
46 pub async fn execute(
56 &self,
57 query: &str,
58 variables: Option<&Value>,
59 operation_name: Option<&str>,
60 ) -> Result<Value, GraphQLError> {
61 if query.trim().is_empty() {
62 return Err(GraphQLError::ValidationError(
63 "Query string cannot be empty".to_string(),
64 ));
65 }
66
67 let mut request = Request::new(query);
68 if let Some(vars) = variables {
69 request = request.variables(Variables::from_json(vars.clone()));
70 }
71 if let Some(name) = operation_name {
72 request = request.operation_name(name);
73 }
74
75 let response = self.schema.execute(request).await;
76
77 if let Some(limit_error) = map_query_limit_error(&response) {
78 return Err(limit_error);
79 }
80
81 serde_json::to_value(response)
82 .map_err(|error| GraphQLError::SerializationError(format!("Failed to serialize GraphQL response: {error}")))
83 }
84
85 #[must_use]
87 pub const fn schema_ref(&self) -> &Arc<Schema<Query, Mutation, Subscription>> {
88 &self.schema
89 }
90
91 #[must_use]
93 pub fn clone_schema(&self) -> Arc<Schema<Query, Mutation, Subscription>> {
94 Arc::clone(&self.schema)
95 }
96}
97
98fn map_query_limit_error(response: &Response) -> Option<GraphQLError> {
99 for error in &response.errors {
100 let message = error.message.to_ascii_lowercase();
101 if message.contains("complexity") {
102 return Some(GraphQLError::ComplexityLimitExceeded);
103 }
104 if message.contains("depth") {
105 return Some(GraphQLError::DepthLimitExceeded);
106 }
107
108 if let Some(extensions) = &error.extensions {
109 let extension_text = format!("{extensions:?}").to_ascii_lowercase();
110 if extension_text.contains("complexity") {
111 return Some(GraphQLError::ComplexityLimitExceeded);
112 }
113 if extension_text.contains("depth") {
114 return Some(GraphQLError::DepthLimitExceeded);
115 }
116 }
117 }
118
119 None
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use async_graphql::{EmptyMutation, EmptySubscription, Object};
126
127 #[derive(Default)]
128 struct TestQuery;
129
130 #[Object]
131 impl TestQuery {
132 async fn hello(&self) -> &'static str {
133 "world"
134 }
135
136 async fn greet(&self, name: String) -> String {
137 format!("Hello, {name}")
138 }
139 }
140
141 fn make_executor() -> GraphQLExecutor<TestQuery, EmptyMutation, EmptySubscription> {
142 let schema = Schema::build(TestQuery, EmptyMutation, EmptySubscription).finish();
143 GraphQLExecutor::new(schema)
144 }
145
146 #[tokio::test]
147 async fn test_executor_creation() {
148 let executor = make_executor();
149 let _ = executor.schema_ref();
150 let _ = executor.clone_schema();
151 }
152
153 #[tokio::test]
154 async fn test_execute_with_empty_query_fails() {
155 let executor = make_executor();
156 let result = executor.execute("", None, None).await;
157 assert!(result.is_err());
158 }
159
160 #[tokio::test]
161 async fn test_execute_with_variables() {
162 let executor = make_executor();
163 let variables = serde_json::json!({ "name": "Alice" });
164 let result = executor
165 .execute("query($name: String!) { greet(name: $name) }", Some(&variables), None)
166 .await
167 .expect("query should execute");
168
169 assert_eq!(result["data"]["greet"], "Hello, Alice");
170 }
171
172 #[tokio::test]
173 async fn test_execute_with_operation_name() {
174 let executor = make_executor();
175 let result = executor
176 .execute(
177 "query First { hello } query Second { greet(name: \"Bob\") }",
178 None,
179 Some("First"),
180 )
181 .await
182 .expect("query with operation name should execute");
183
184 assert_eq!(result["data"]["hello"], "world");
185 }
186}