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