1pub mod extensions;
19pub mod resolver;
20
21#[cfg(feature = "graphql-complexity")]
22pub mod complexity;
23#[cfg(feature = "graphql-n1")]
24pub mod dataloader;
25#[cfg(any(feature = "graphql-n1", feature = "graphql-complexity"))]
26pub mod query_ir;
27#[cfg(feature = "graphql-schema-gen")]
28pub mod schema_gen;
29
30#[cfg(feature = "async-graphql-integration")]
31pub mod async_graphql_integration;
32
33use serde::{Deserialize, Serialize};
34
35#[cfg(feature = "real")]
36mod real_graphql;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct GraphQLSchema {
40 pub types: Vec<GraphQLType>,
41 pub queries: Vec<GraphQLField>,
42 pub mutations: Vec<GraphQLField>,
43}
44
45impl GraphQLSchema {
46 pub fn new() -> Self {
47 Self {
48 types: vec![],
49 queries: vec![],
50 mutations: vec![],
51 }
52 }
53
54 pub fn add_type(mut self, t: GraphQLType) -> Self {
55 self.types.push(t);
56 self
57 }
58
59 pub fn add_query(mut self, f: GraphQLField) -> Self {
60 self.queries.push(f);
61 self
62 }
63
64 pub fn add_mutation(mut self, f: GraphQLField) -> Self {
65 self.mutations.push(f);
66 self
67 }
68
69 pub fn to_sdl(&self) -> String {
71 let mut out = String::new();
72 for t in &self.types {
73 out.push_str(&format!("type {} {{\n", t.name));
74 for f in &t.fields {
75 out.push_str(&format!(" {}: {}\n", f.name, f.type_name));
76 }
77 out.push_str("}\n\n");
78 }
79 out.push_str("type Query {\n");
80 for q in &self.queries {
81 out.push_str(&format!(" {}: {}\n", q.name, q.type_name));
82 }
83 out.push_str("}\n");
84 if !self.mutations.is_empty() {
85 out.push_str("\ntype Mutation {\n");
86 for m in &self.mutations {
87 out.push_str(&format!(" {}: {}\n", m.name, m.type_name));
88 }
89 out.push_str("}\n");
90 }
91 out
92 }
93}
94
95impl Default for GraphQLSchema {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct GraphQLType {
103 pub name: String,
104 pub fields: Vec<GraphQLField>,
105}
106
107impl GraphQLType {
108 pub fn new(name: &str) -> Self {
109 Self {
110 name: name.to_string(),
111 fields: vec![],
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GraphQLField {
118 pub name: String,
119 pub type_name: String,
120}
121
122fn to_pascal_singular(input: &str) -> String {
125 let mut out = String::new();
126 let mut cap_next = true;
127 for ch in input.chars() {
128 if ch == '_' || ch == '-' || ch == ' ' {
129 cap_next = true;
130 } else if cap_next {
131 out.extend(ch.to_uppercase());
132 cap_next = false;
133 } else {
134 out.push(ch);
135 }
136 }
137 let len = out.len();
138 if len > 1 && out.ends_with('s') && !out.ends_with("ss") {
139 out.truncate(len - 1);
140 }
141 out
142}
143
144pub struct GraphQLSchemaGenerator;
145
146impl GraphQLSchemaGenerator {
147 pub fn generate_schema(models: &[&str]) -> GraphQLSchema {
151 let mut schema = GraphQLSchema::new();
152 for m in models {
153 let type_name = to_pascal_singular(m);
154 let mut t = GraphQLType::new(&type_name);
155 t.fields.push(GraphQLField {
156 name: "id".to_string(),
157 type_name: "ID!".to_string(),
158 });
159 t.fields.push(GraphQLField {
160 name: "name".to_string(),
161 type_name: "String!".to_string(),
162 });
163 t.fields.push(GraphQLField {
164 name: "createdAt".to_string(),
165 type_name: "String!".to_string(),
166 });
167 t.fields.push(GraphQLField {
168 name: "updatedAt".to_string(),
169 type_name: "String!".to_string(),
170 });
171 schema = schema.add_type(t);
172 schema = schema.add_query(GraphQLField {
173 name: format!("get{}", type_name),
174 type_name: type_name.clone(),
175 });
176 schema = schema.add_query(GraphQLField {
177 name: format!("list{}s", type_name),
178 type_name: format!("[{}!]!", type_name),
179 });
180 }
181 schema
182 }
183}
184
185pub struct GraphQLServer {
186 port: u16,
187 schema: Option<GraphQLSchema>,
188 #[cfg(feature = "real")]
189 dynamic_schema: std::sync::OnceLock<Result<async_graphql::dynamic::Schema, String>>,
190 #[cfg(feature = "real")]
191 resolver: Option<resolver::SharedDbResolver>,
192}
193
194impl GraphQLServer {
195 pub fn new(port: u16) -> Self {
196 Self {
197 port,
198 schema: None,
199 #[cfg(feature = "real")]
200 dynamic_schema: std::sync::OnceLock::new(),
201 #[cfg(feature = "real")]
202 resolver: None,
203 }
204 }
205
206 pub fn with_schema(mut self, s: GraphQLSchema) -> Self {
207 self.schema = Some(s);
208 self
209 }
210
211 #[cfg(feature = "real")]
216 pub fn with_db_resolver(mut self, resolver: resolver::SharedDbResolver) -> Self {
217 self.resolver = Some(resolver);
218 self
219 }
220
221 #[cfg(not(feature = "real"))]
225 pub fn start(&self) -> Result<String, String> {
226 if self.schema.is_none() {
227 return Err("No schema".to_string());
228 }
229 let port = self.port;
230 let url = format!("http://localhost:{}", port);
231 tokio::spawn(async move {
233 let addr = format!("127.0.0.1:{}", port);
234 match tokio::net::TcpListener::bind(&addr).await {
235 Ok(listener) => {
236 while listener.accept().await.is_ok() {
237 }
239 }
240 Err(_) => {
241 }
243 }
244 });
245 Ok(url)
246 }
247
248 #[cfg(feature = "real")]
250 fn executable_schema(&self) -> Result<&async_graphql::dynamic::Schema, String> {
251 let schema = self.schema.as_ref().ok_or("No schema")?;
252 self.dynamic_schema
253 .get_or_init(|| real_graphql::build_dynamic_schema(schema, self.resolver.as_ref()))
254 .as_ref()
255 .map_err(Clone::clone)
256 }
257
258 #[cfg(feature = "real")]
262 pub fn start(&self) -> Result<String, String> {
263 let schema = self.executable_schema()?.clone();
264 let port = self.port;
265 let url = format!("http://localhost:{}", port);
266 tokio::spawn(async move {
267 let addr = format!("127.0.0.1:{}", port);
268 match tokio::net::TcpListener::bind(&addr).await {
269 Ok(listener) => {
270 let _ = axum::serve(listener, real_graphql::router(schema)).await;
271 }
272 Err(_) => {
273 }
275 }
276 });
277 Ok(url)
278 }
279
280 #[cfg(not(feature = "real"))]
283 pub fn execute_query(&self, query: &str) -> Result<serde_json::Value, String> {
284 let schema = self.schema.as_ref().ok_or("No schema")?;
285 let trimmed = query.trim();
286 let brace_start = trimmed.find('{').ok_or("Missing '{' in query")?;
287 let after_brace = trimmed[brace_start + 1..].trim_start();
288 let end = after_brace
290 .find(|c: char| c.is_whitespace() || c == '(' || c == '{')
291 .unwrap_or(after_brace.len());
292 let query_name = after_brace[..end].trim();
293 if query_name.is_empty() {
294 return Err("Empty query name".to_string());
295 }
296 let field = schema
297 .queries
298 .iter()
299 .find(|q| q.name == query_name)
300 .ok_or_else(|| format!("Query '{}' not found in schema", query_name))?;
301 if field.type_name.starts_with('[') {
302 Ok(serde_json::json!([
304 {
305 "id": "1",
306 "name": format!("{}_1", field.name),
307 "createdAt": "2024-01-01T00:00:00Z",
308 "updatedAt": "2024-01-01T00:00:00Z"
309 },
310 {
311 "id": "2",
312 "name": format!("{}_2", field.name),
313 "createdAt": "2024-01-01T00:00:00Z",
314 "updatedAt": "2024-01-01T00:00:00Z"
315 }
316 ]))
317 } else {
318 Ok(serde_json::json!({
320 "id": "1",
321 "name": format!("{}_1", field.name),
322 "createdAt": "2024-01-01T00:00:00Z",
323 "updatedAt": "2024-01-01T00:00:00Z"
324 }))
325 }
326 }
327
328 #[cfg(feature = "real")]
331 pub fn execute_query(&self, query: &str) -> Result<serde_json::Value, String> {
332 let schema = self.executable_schema()?;
333 real_graphql::execute(schema, query)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_schema_new() {
343 let s = GraphQLSchema::new();
344 assert!(s.types.is_empty());
345 }
346
347 #[test]
348 fn test_schema_add_type() {
349 let s = GraphQLSchema::new().add_type(GraphQLType::new("User"));
350 assert_eq!(s.types.len(), 1);
351 }
352
353 #[test]
354 fn test_generator_creates_types_and_queries() {
355 let s = GraphQLSchemaGenerator::generate_schema(&["users", "orders"]);
356 assert_eq!(s.types.len(), 2);
358 assert_eq!(s.queries.len(), 4);
360 for t in &s.types {
362 assert!(t
363 .fields
364 .iter()
365 .any(|f| f.name == "id" && f.type_name == "ID!"));
366 assert!(t.fields.iter().any(|f| f.name == "name"));
367 assert!(t.fields.iter().any(|f| f.name == "createdAt"));
368 assert!(t.fields.iter().any(|f| f.name == "updatedAt"));
369 }
370 assert!(s
372 .queries
373 .iter()
374 .any(|q| q.name == "getUser" && q.type_name == "User"));
375 assert!(s
376 .queries
377 .iter()
378 .any(|q| q.name == "listUsers" && q.type_name == "[User!]!"));
379 assert!(s
380 .queries
381 .iter()
382 .any(|q| q.name == "getOrder" && q.type_name == "Order"));
383 assert!(s
384 .queries
385 .iter()
386 .any(|q| q.name == "listOrders" && q.type_name == "[Order!]!"));
387 }
388
389 #[test]
390 fn test_schema_sdl_contains_all_models() {
391 let s = GraphQLSchemaGenerator::generate_schema(&["users", "orders"]);
392 let sdl = s.to_sdl();
393 assert!(sdl.contains("type User {"));
394 assert!(sdl.contains("type Order {"));
395 assert!(sdl.contains("type Query {"));
396 assert!(sdl.contains("getUser: User"));
397 assert!(sdl.contains("listUsers: [User!]!"));
398 }
399
400 #[test]
401 fn test_server_new() {
402 let srv = GraphQLServer::new(4000);
403 assert_eq!(srv.port, 4000);
404 }
405
406 #[test]
407 fn test_server_start_without_schema_fails() {
408 let srv = GraphQLServer::new(4000);
409 assert!(srv.start().is_err());
410 }
411
412 #[tokio::test]
413 async fn test_server_start_returns_url_and_binds_port() {
414 let srv = GraphQLServer::new(4123)
415 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
416 let url = srv.start().expect("start should succeed");
417 assert!(url.contains("4123"));
418 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
420 let second = tokio::net::TcpListener::bind("127.0.0.1:4123").await;
422 assert!(
423 second.is_err(),
424 "Port 4123 should already be bound by the spawned server task"
425 );
426 }
427
428 #[test]
429 fn test_execute_query_single() {
430 let srv = GraphQLServer::new(4001)
431 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
432 let result = srv.execute_query("{ getUser(id: 1) { id name } }");
433 assert!(result.is_ok(), "expected ok, got {:?}", result);
434 let v = result.unwrap();
435 assert_eq!(v["id"], "1");
436 assert!(v["name"].as_str().unwrap().contains("getUser"));
437 }
438
439 #[test]
440 fn test_execute_query_list() {
441 let srv = GraphQLServer::new(4002)
442 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
443 let result = srv.execute_query("{ listUsers { id name } }");
444 assert!(result.is_ok(), "expected ok, got {:?}", result);
445 let v = result.unwrap();
446 assert!(v.is_array());
447 assert_eq!(v.as_array().unwrap().len(), 2);
448 }
449
450 #[test]
451 fn test_execute_query_unknown_returns_error() {
452 let srv = GraphQLServer::new(4003)
453 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
454 let result = srv.execute_query("{ unknownQuery { id } }");
455 assert!(result.is_err());
456 assert!(result.unwrap_err().contains("unknownQuery"));
457 }
458
459 #[test]
460 fn test_execute_query_without_schema_fails() {
461 let srv = GraphQLServer::new(4004);
462 let result = srv.execute_query("{ getUser { id } }");
463 assert!(result.is_err());
464 }
465
466 #[test]
467 fn test_execute_query_malformed_no_brace() {
468 let srv = GraphQLServer::new(4005)
469 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
470 let result = srv.execute_query("getUser");
471 assert!(result.is_err());
472 }
473
474 #[cfg(feature = "real")]
479 async fn post_graphql(url: &str, body: &str) -> (u16, serde_json::Value) {
480 use tokio::io::{AsyncReadExt, AsyncWriteExt};
481 let without_scheme = url
482 .strip_prefix("http://")
483 .expect("url must start with http://");
484 let (addr, path) = without_scheme
485 .split_once('/')
486 .unwrap_or((without_scheme, ""));
487 let mut stream = tokio::net::TcpStream::connect(addr)
488 .await
489 .expect("connect should succeed");
490 let request = format!(
491 "POST /{path} HTTP/1.0\r\nHost: {addr}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
492 body.len()
493 );
494 stream
495 .write_all(request.as_bytes())
496 .await
497 .expect("write should succeed");
498 let mut raw = Vec::new();
499 stream
500 .read_to_end(&mut raw)
501 .await
502 .expect("read should succeed");
503 let text = String::from_utf8(raw).expect("response must be valid UTF-8");
504 let (head, body) = text
505 .split_once("\r\n\r\n")
506 .expect("response must contain a header/body separator");
507 let status = head
508 .split_whitespace()
509 .nth(1)
510 .and_then(|code| code.parse::<u16>().ok())
511 .expect("status line must contain a numeric status code");
512 let json = serde_json::from_str(body).expect("response body must be valid JSON");
513 (status, json)
514 }
515
516 #[cfg(feature = "real")]
517 #[tokio::test]
518 #[ignore = "requires the real GraphQL server (feature `real`)"]
519 async fn test_real_http_post_single_query() {
520 let srv = GraphQLServer::new(4331)
521 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
522 let url = srv.start().expect("start should succeed");
523 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
524 let (status, body) = post_graphql(
525 &format!("{url}/graphql"),
526 &serde_json::json!({"query": "{ getUser(id: 1) { id name } }"}).to_string(),
527 )
528 .await;
529 assert_eq!(status, 200);
530 assert_eq!(body["data"]["getUser"]["id"], "1");
531 assert!(body["data"]["getUser"]["name"]
532 .as_str()
533 .expect("name must be a string")
534 .contains("getUser"));
535 }
536
537 #[cfg(feature = "real")]
538 #[tokio::test]
539 #[ignore = "requires the real GraphQL server (feature `real`)"]
540 async fn test_real_http_post_list_query() {
541 let srv = GraphQLServer::new(4332)
542 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
543 let url = srv.start().expect("start should succeed");
544 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
545 let (status, body) = post_graphql(
546 &format!("{url}/graphql"),
547 &serde_json::json!({"query": "{ listUsers { id name } }"}).to_string(),
548 )
549 .await;
550 assert_eq!(status, 200);
551 let users = body["data"]["listUsers"]
552 .as_array()
553 .expect("listUsers must be an array");
554 assert_eq!(users.len(), 2);
555 assert_eq!(users[0]["id"], "1");
556 assert_eq!(users[1]["id"], "2");
557 }
558
559 #[cfg(feature = "real")]
560 #[tokio::test]
561 #[ignore = "requires the real GraphQL server (feature `real`)"]
562 async fn test_real_http_post_unknown_query_returns_errors() {
563 let srv = GraphQLServer::new(4333)
564 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
565 let url = srv.start().expect("start should succeed");
566 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
567 let (status, body) = post_graphql(
568 &format!("{url}/graphql"),
569 &serde_json::json!({"query": "{ unknownQuery { id } }"}).to_string(),
570 )
571 .await;
572 assert_eq!(status, 200);
573 let errors = body["errors"].as_array().expect("errors must be an array");
574 assert!(!errors.is_empty());
575 assert!(errors[0]["message"]
576 .as_str()
577 .expect("message must be a string")
578 .contains("unknownQuery"));
579 }
580
581 #[cfg(feature = "real")]
582 #[test]
583 #[ignore = "requires the real GraphQL engine (feature `real`)"]
584 fn test_real_execute_query_matches_mock_shape() {
585 let srv = GraphQLServer::new(4334)
586 .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
587 let single = srv
588 .execute_query("{ getUser(id: 1) { id name } }")
589 .expect("single query should succeed");
590 assert_eq!(single["id"], "1");
591 assert!(single["name"]
592 .as_str()
593 .expect("name must be a string")
594 .contains("getUser"));
595 let list = srv
596 .execute_query("{ listUsers { id name } }")
597 .expect("list query should succeed");
598 assert_eq!(list.as_array().expect("result must be an array").len(), 2);
599 let err = srv
600 .execute_query("{ unknownQuery { id } }")
601 .expect_err("unknown query must fail");
602 assert!(err.contains("unknownQuery"));
603 }
604}