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