1use axum::Router;
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::routing::post;
6use serde::Deserialize;
7use serde_json::{Map, Value};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11mod parser;
12mod validation;
13
14pub use parser::{FieldNode, Operation, SelectionSet};
15pub use validation::QueryLimits;
16
17type Resolver = Arc<
18 dyn Fn(
19 serde_json::Value,
20 ) -> std::pin::Pin<
21 Box<dyn std::future::Future<Output = Result<serde_json::Value, String>> + Send>,
22 > + Send
23 + Sync,
24>;
25
26#[derive(Debug, Clone)]
28pub struct FieldRequest {
29 pub args: Map<String, Value>,
30 pub variables: Value,
31 pub selection: Option<SelectionSet>,
32}
33
34#[async_trait::async_trait]
39pub trait GraphQLField: Send + Sync {
40 async fn resolve(&self, req: FieldRequest) -> Result<Value, String>;
41}
42
43enum FieldHandler {
44 Legacy(Resolver),
45 Rich(Arc<dyn GraphQLField>),
46}
47
48pub struct GraphQLSchema {
49 query_resolvers: HashMap<String, FieldHandler>,
50 mutation_resolvers: HashMap<String, FieldHandler>,
51 limits: QueryLimits,
52}
53
54impl GraphQLSchema {
55 pub fn new() -> Self {
56 Self {
57 query_resolvers: HashMap::new(),
58 mutation_resolvers: HashMap::new(),
59 limits: QueryLimits::default(),
60 }
61 }
62
63 pub fn with_limits(mut self, limits: QueryLimits) -> Self {
65 self.limits = limits;
66 self
67 }
68
69 pub fn query(mut self, name: impl Into<String>, r: Resolver) -> Self {
70 self.query_resolvers
71 .insert(name.into(), FieldHandler::Legacy(r));
72 self
73 }
74
75 pub fn mutation(mut self, name: impl Into<String>, r: Resolver) -> Self {
76 self.mutation_resolvers
77 .insert(name.into(), FieldHandler::Legacy(r));
78 self
79 }
80
81 pub fn query_fn(
82 self,
83 name: impl Into<String>,
84 f: impl Fn(
85 serde_json::Value,
86 ) -> std::pin::Pin<
87 Box<dyn std::future::Future<Output = Result<serde_json::Value, String>> + Send>,
88 > + Send
89 + Sync
90 + 'static,
91 ) -> Self {
92 self.query(name, Arc::new(f))
93 }
94
95 pub fn query_field(mut self, name: impl Into<String>, f: impl GraphQLField + 'static) -> Self {
97 self.query_resolvers
98 .insert(name.into(), FieldHandler::Rich(Arc::new(f)));
99 self
100 }
101
102 pub fn mutation_field(
104 mut self,
105 name: impl Into<String>,
106 f: impl GraphQLField + 'static,
107 ) -> Self {
108 self.mutation_resolvers
109 .insert(name.into(), FieldHandler::Rich(Arc::new(f)));
110 self
111 }
112}
113
114impl Default for GraphQLSchema {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120#[derive(Debug, Deserialize)]
121struct GqlReq {
122 query: String,
123 #[serde(default)]
124 variables: serde_json::Value,
125}
126
127pub fn graphql_router(schema: GraphQLSchema) -> Router {
128 let schema = Arc::new(schema);
129
130 async fn handler(axum::Json(req): axum::Json<GqlReq>, schema: Arc<GraphQLSchema>) -> Response {
131 match execute(&schema, &req.query, &req.variables).await {
132 Ok(data) => axum::Json(serde_json::json!({"data": data})).into_response(),
133 Err(errors) => (
134 StatusCode::BAD_REQUEST,
135 axum::Json(serde_json::json!({"errors": errors})),
136 )
137 .into_response(),
138 }
139 }
140
141 let s = Arc::clone(&schema);
142 Router::new().route("/graphql", post(move |body| handler(body, Arc::clone(&s))))
143}
144
145async fn execute(
146 schema: &GraphQLSchema,
147 query: &str,
148 variables: &serde_json::Value,
149) -> Result<serde_json::Value, Vec<String>> {
150 let trimmed = query.trim();
151 let mut errors = Vec::new();
152
153 let field = match parser::parse_query(trimmed, variables) {
154 Ok(f) => f,
155 Err(e) => {
156 errors.push(e);
157 return Err(errors);
158 }
159 };
160
161 if let Err(e) = validation::validate(&field, &schema.limits) {
162 errors.push(e);
163 return Err(errors);
164 }
165
166 let (resolvers, field_name) = if field.operation == Operation::Mutation {
167 (&schema.mutation_resolvers, &field.name)
168 } else {
169 (&schema.query_resolvers, &field.name)
170 };
171
172 match resolvers.get(field_name) {
173 Some(FieldHandler::Legacy(resolver)) => {
174 let vars = match merge_args(variables, &field.args) {
175 Ok(v) => v,
176 Err(e) => {
177 errors.push(e);
178 return Err(errors);
179 }
180 };
181 match resolver(vars).await {
182 Ok(data) => {
183 let mut result = serde_json::Map::new();
184 result.insert(field.name.clone(), data);
185 return Ok(serde_json::Value::Object(result));
186 }
187 Err(e) => errors.push(e),
188 }
189 }
190 Some(FieldHandler::Rich(r)) => {
191 let req = FieldRequest {
192 args: field.args,
193 variables: variables.clone(),
194 selection: field.selection,
195 };
196 match r.resolve(req).await {
197 Ok(data) => {
198 let mut result = serde_json::Map::new();
199 result.insert(field.name.clone(), data);
200 return Ok(serde_json::Value::Object(result));
201 }
202 Err(e) => errors.push(e),
203 }
204 }
205 None => errors.push(format!("unknown field: {field_name}")),
206 }
207
208 Err(errors)
209}
210
211fn merge_args(variables: &Value, args: &Map<String, Value>) -> Result<Value, String> {
214 if args.is_empty() {
215 return Ok(variables.clone());
216 }
217 match variables {
218 Value::Object(m) => {
219 let mut merged = m.clone();
220 for (k, v) in args {
221 merged.insert(k.clone(), v.clone());
222 }
223 Ok(Value::Object(merged))
224 }
225 Value::Null => Ok(Value::Object(args.clone())),
226 _ => Err("variables must be a JSON object when field arguments are present".into()),
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use axum::body::Body;
234 use axum::http::Request;
235 use tower::ServiceExt;
236
237 struct EchoField;
238
239 #[async_trait::async_trait]
240 impl GraphQLField for EchoField {
241 async fn resolve(&self, req: FieldRequest) -> Result<Value, String> {
242 Ok(serde_json::json!({
243 "args": req.args,
244 "variables": req.variables,
245 "has_selection": req.selection.is_some(),
246 }))
247 }
248 }
249
250 #[test]
251 fn legacy_resolver_receives_merged_args() {
252 let schema =
253 GraphQLSchema::new().query_fn("echo", |vars| Box::pin(async move { Ok(vars) }));
254 let rt = tokio::runtime::Runtime::new().unwrap();
255 let vars = serde_json::json!({"id": 1});
256 let data = rt
257 .block_on(execute(&schema, "{ echo(id: 2, name: \"x\") }", &vars))
258 .unwrap();
259 assert_eq!(data["echo"], serde_json::json!({"id": 2, "name": "x"}));
260 }
261
262 #[test]
263 fn legacy_resolver_without_args_is_unchanged() {
264 let schema =
265 GraphQLSchema::new().query_fn("echo", |vars| Box::pin(async move { Ok(vars) }));
266 let rt = tokio::runtime::Runtime::new().unwrap();
267 let vars = serde_json::json!({"a": 1});
268 let data = rt.block_on(execute(&schema, "{ echo }", &vars)).unwrap();
269 assert_eq!(data["echo"], serde_json::json!({"a": 1}));
270 }
271
272 #[test]
273 fn legacy_resolver_args_override_same_named_variables() {
274 let schema =
275 GraphQLSchema::new().query_fn("echo", |vars| Box::pin(async move { Ok(vars) }));
276 let rt = tokio::runtime::Runtime::new().unwrap();
277 let vars = serde_json::json!({"id": 1});
278 let data = rt
279 .block_on(execute(&schema, "{ echo(id: 9) }", &vars))
280 .unwrap();
281 assert_eq!(data["echo"]["id"], 9);
282 }
283
284 #[test]
285 fn legacy_resolver_args_with_null_variables() {
286 let schema =
287 GraphQLSchema::new().query_fn("echo", |vars| Box::pin(async move { Ok(vars) }));
288 let rt = tokio::runtime::Runtime::new().unwrap();
289 let data = rt
290 .block_on(execute(&schema, "{ echo(id: 1) }", &Value::Null))
291 .unwrap();
292 assert_eq!(data["echo"], serde_json::json!({"id": 1}));
293 }
294
295 #[test]
296 fn legacy_resolver_errors_on_non_object_variables_with_args() {
297 let schema =
298 GraphQLSchema::new().query_fn("echo", |vars| Box::pin(async move { Ok(vars) }));
299 let rt = tokio::runtime::Runtime::new().unwrap();
300 let err = rt
301 .block_on(execute(&schema, "{ echo(id: 1) }", &serde_json::json!(5)))
302 .unwrap_err();
303 assert!(
304 err.iter()
305 .any(|e| e.contains("variables must be a JSON object")),
306 "got: {err:?}"
307 );
308 }
309
310 #[test]
311 fn rich_resolver_receives_full_request() {
312 let schema = GraphQLSchema::new().query_field("user", EchoField);
313 let rt = tokio::runtime::Runtime::new().unwrap();
314 let vars = serde_json::json!({"env": "prod"});
315 let data = rt
316 .block_on(execute(&schema, "{ user(id: 7) { name } }", &vars))
317 .unwrap();
318 assert_eq!(data["user"]["args"]["id"], 7);
320 assert_eq!(
321 data["user"]["variables"],
322 serde_json::json!({"env": "prod"})
323 );
324 assert_eq!(data["user"]["has_selection"], true);
325 }
326
327 #[test]
328 fn rich_resolver_without_selection() {
329 let schema = GraphQLSchema::new().query_field("ping", EchoField);
330 let rt = tokio::runtime::Runtime::new().unwrap();
331 let data = rt
332 .block_on(execute(&schema, "{ ping }", &Value::Null))
333 .unwrap();
334 assert_eq!(data["ping"]["has_selection"], false);
335 assert!(data["ping"]["args"].as_object().unwrap().is_empty());
336 }
337
338 #[test]
339 fn unknown_field_and_resolver_error_go_to_errors() {
340 let schema = GraphQLSchema::new().query_field("boom", ResolveErr);
341 let rt = tokio::runtime::Runtime::new().unwrap();
342 let err = rt
343 .block_on(execute(&schema, "{ nope }", &Value::Null))
344 .unwrap_err();
345 assert!(err.iter().any(|e| e.contains("unknown field: nope")));
346
347 let err = rt
348 .block_on(execute(&schema, "{ boom }", &Value::Null))
349 .unwrap_err();
350 assert!(err.iter().any(|e| e == "resolver exploded"));
351 }
352
353 struct ResolveErr;
354
355 #[async_trait::async_trait]
356 impl GraphQLField for ResolveErr {
357 async fn resolve(&self, _req: FieldRequest) -> Result<Value, String> {
358 Err("resolver exploded".into())
359 }
360 }
361
362 #[test]
363 fn mutation_dispatches_to_mutation_resolvers() {
364 let schema = GraphQLSchema::new().query_fn("write", |_v| {
365 Box::pin(async { Ok(serde_json::json!("query")) })
366 });
367 let schema = schema.mutation_field("write", MutWrite);
368 let rt = tokio::runtime::Runtime::new().unwrap();
369 let data = rt
370 .block_on(execute(&schema, "mutation { write(id: 3) }", &Value::Null))
371 .unwrap();
372 assert_eq!(data["write"]["id"], 3);
373 }
374
375 struct MutWrite;
376
377 #[async_trait::async_trait]
378 impl GraphQLField for MutWrite {
379 async fn resolve(&self, req: FieldRequest) -> Result<Value, String> {
380 Ok(serde_json::json!({"id": req.args["id"]}))
381 }
382 }
383
384 #[test]
385 fn subscription_dispatches_to_query_resolvers() {
386 let schema = GraphQLSchema::new().query_fn("sub", |_v| {
387 Box::pin(async { Ok(serde_json::json!("query")) })
388 });
389 let schema = schema.mutation_field("sub", MutWrite);
390 let rt = tokio::runtime::Runtime::new().unwrap();
391 let data = rt
392 .block_on(execute(&schema, "subscription { sub }", &Value::Null))
393 .unwrap();
394 assert_eq!(data["sub"], "query");
395 }
396
397 fn router() -> Router {
398 graphql_router(
399 GraphQLSchema::new()
400 .query_fn("hello", |_v| {
401 Box::pin(async { Ok(serde_json::json!("world")) })
402 })
403 .query_field("user", EchoField),
404 )
405 }
406
407 #[tokio::test]
408 async fn router_serves_simple_query() {
409 let res = router()
410 .oneshot(
411 Request::builder()
412 .method("POST")
413 .uri("/graphql")
414 .header("content-type", "application/json")
415 .body(Body::from(r#"{"query":"{ hello }"}"#))
416 .unwrap(),
417 )
418 .await
419 .unwrap();
420 assert_eq!(res.status(), StatusCode::OK);
421 let body = axum::body::to_bytes(res.into_body(), usize::MAX)
422 .await
423 .unwrap();
424 let v: Value = serde_json::from_slice(&body).unwrap();
425 assert_eq!(v["data"]["hello"], "world");
426 }
427
428 #[tokio::test]
429 async fn router_serves_query_with_args_and_nested_selection() {
430 let res = router()
431 .oneshot(
432 Request::builder()
433 .method("POST")
434 .uri("/graphql")
435 .header("content-type", "application/json")
436 .body(Body::from(
437 r#"{"query":"{ user(id: 7, env: $e) { name } }","variables":{"e":"prod"}}"#,
438 ))
439 .unwrap(),
440 )
441 .await
442 .unwrap();
443 assert_eq!(res.status(), StatusCode::OK);
444 let body = axum::body::to_bytes(res.into_body(), usize::MAX)
445 .await
446 .unwrap();
447 let v: Value = serde_json::from_slice(&body).unwrap();
448 assert_eq!(v["data"]["user"]["args"]["id"], 7);
449 assert_eq!(v["data"]["user"]["args"]["env"], "prod");
450 assert_eq!(
451 v["data"]["user"]["variables"],
452 serde_json::json!({"e": "prod"})
453 );
454 assert_eq!(v["data"]["user"]["has_selection"], true);
455 }
456
457 #[tokio::test]
458 async fn router_returns_400_with_errors() {
459 let res = router()
460 .oneshot(
461 Request::builder()
462 .method("POST")
463 .uri("/graphql")
464 .header("content-type", "application/json")
465 .body(Body::from(r#"{"query":"{ nope }"}"#))
466 .unwrap(),
467 )
468 .await
469 .unwrap();
470 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
471 let body = axum::body::to_bytes(res.into_body(), usize::MAX)
472 .await
473 .unwrap();
474 let v: Value = serde_json::from_slice(&body).unwrap();
475 assert!(v["errors"][0].as_str().unwrap().contains("unknown field"));
476 }
477
478 #[tokio::test]
479 async fn router_returns_400_on_parse_error() {
480 let res = router()
481 .oneshot(
482 Request::builder()
483 .method("POST")
484 .uri("/graphql")
485 .header("content-type", "application/json")
486 .body(Body::from(r#"{"query":"{ a b }"}"#))
487 .unwrap(),
488 )
489 .await
490 .unwrap();
491 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
492 let body = axum::body::to_bytes(res.into_body(), usize::MAX)
493 .await
494 .unwrap();
495 let v: Value = serde_json::from_slice(&body).unwrap();
496 assert!(
497 v["errors"][0]
498 .as_str()
499 .unwrap()
500 .contains("multiple top-level fields")
501 );
502 }
503
504 #[test]
505 fn schema_field_name_conflict_latest_wins() {
506 let schema = GraphQLSchema::new()
507 .query_fn("f", |_v| {
508 Box::pin(async { Ok(serde_json::json!("legacy")) })
509 })
510 .query_field("f", EchoField);
511 let rt = tokio::runtime::Runtime::new().unwrap();
512 let data = rt
513 .block_on(execute(&schema, "{ f(a: 1) }", &Value::Null))
514 .unwrap();
515 assert_eq!(data["f"]["args"]["a"], 1);
516 }
517}