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