1use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PlaygroundConfig {
19 pub endpoint: String,
21
22 pub graphql_endpoint: String,
24
25 pub subscription_endpoint: Option<String>,
27
28 pub enable_tabs: bool,
30
31 pub enable_history: bool,
33
34 pub enable_schema_polling: bool,
36
37 pub schema_polling_interval: u64,
39
40 pub enable_tracing: bool,
42
43 pub enable_code_gen: bool,
45
46 pub code_gen_targets: Vec<CodeGenTarget>,
48
49 pub default_theme: PlaygroundTheme,
51
52 pub enable_settings_persistence: bool,
54
55 pub title: String,
57
58 pub favicon: Option<String>,
60
61 pub default_headers: HashMap<String, String>,
63
64 pub enable_sharing: bool,
66
67 pub custom_css: Option<String>,
69
70 pub enable_prettier: bool,
72
73 pub enable_linting: bool,
75}
76
77impl Default for PlaygroundConfig {
78 fn default() -> Self {
79 Self {
80 endpoint: "/playground".to_string(),
81 graphql_endpoint: "/graphql".to_string(),
82 subscription_endpoint: Some("/graphql/subscriptions".to_string()),
83 enable_tabs: true,
84 enable_history: true,
85 enable_schema_polling: true,
86 schema_polling_interval: 5,
87 enable_tracing: true,
88 enable_code_gen: true,
89 code_gen_targets: vec![
90 CodeGenTarget::TypeScript,
91 CodeGenTarget::JavaScript,
92 CodeGenTarget::Rust,
93 CodeGenTarget::Python,
94 ],
95 default_theme: PlaygroundTheme::Dark,
96 enable_settings_persistence: true,
97 title: "OxiRS GraphQL Playground".to_string(),
98 favicon: None,
99 default_headers: HashMap::new(),
100 enable_sharing: true,
101 custom_css: None,
102 enable_prettier: true,
103 enable_linting: true,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub enum PlaygroundTheme {
111 Dark,
113 Light,
115}
116
117impl PlaygroundTheme {
118 pub fn as_str(&self) -> &'static str {
119 match self {
120 PlaygroundTheme::Dark => "dark",
121 PlaygroundTheme::Light => "light",
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128pub enum CodeGenTarget {
129 TypeScript,
130 JavaScript,
131 Rust,
132 Python,
133 Go,
134 Java,
135 CSharp,
136 Swift,
137}
138
139impl CodeGenTarget {
140 pub fn as_str(&self) -> &'static str {
141 match self {
142 CodeGenTarget::TypeScript => "typescript",
143 CodeGenTarget::JavaScript => "javascript",
144 CodeGenTarget::Rust => "rust",
145 CodeGenTarget::Python => "python",
146 CodeGenTarget::Go => "go",
147 CodeGenTarget::Java => "java",
148 CodeGenTarget::CSharp => "csharp",
149 CodeGenTarget::Swift => "swift",
150 }
151 }
152
153 pub fn all() -> Vec<Self> {
154 vec![
155 Self::TypeScript,
156 Self::JavaScript,
157 Self::Rust,
158 Self::Python,
159 Self::Go,
160 Self::Java,
161 Self::CSharp,
162 Self::Swift,
163 ]
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct QueryTab {
170 pub id: String,
172
173 pub name: String,
175
176 pub query: String,
178
179 pub variables: HashMap<String, serde_json::Value>,
181
182 pub headers: HashMap<String, String>,
184
185 pub last_modified: String,
187}
188
189impl QueryTab {
190 pub fn new(name: String, query: String) -> Self {
191 Self {
192 id: uuid::Uuid::new_v4().to_string(),
193 name,
194 query,
195 variables: HashMap::new(),
196 headers: HashMap::new(),
197 last_modified: chrono::Utc::now().to_rfc3339(),
198 }
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct PlaygroundSettings {
205 pub active_tab: Option<String>,
207
208 pub tabs: Vec<QueryTab>,
210
211 pub theme: PlaygroundTheme,
213
214 pub font_size: u32,
216
217 pub editor: EditorSettings,
219
220 pub headers: HashMap<String, String>,
222
223 pub tracing_enabled: bool,
225
226 pub auto_completion_enabled: bool,
228
229 pub prettier_enabled: bool,
231
232 pub linting_enabled: bool,
234}
235
236impl Default for PlaygroundSettings {
237 fn default() -> Self {
238 Self {
239 active_tab: None,
240 tabs: vec![QueryTab::new(
241 "Default Query".to_string(),
242 "query { __typename }".to_string(),
243 )],
244 theme: PlaygroundTheme::Dark,
245 font_size: 14,
246 editor: EditorSettings::default(),
247 headers: HashMap::new(),
248 tracing_enabled: true,
249 auto_completion_enabled: true,
250 prettier_enabled: true,
251 linting_enabled: true,
252 }
253 }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct EditorSettings {
259 pub tab_size: u32,
261
262 pub use_spaces: bool,
264
265 pub line_numbers: bool,
267
268 pub word_wrap: bool,
270
271 pub vim_mode: bool,
273
274 pub auto_close_brackets: bool,
276
277 pub highlight_brackets: bool,
279}
280
281impl Default for EditorSettings {
282 fn default() -> Self {
283 Self {
284 tab_size: 2,
285 use_spaces: true,
286 line_numbers: true,
287 word_wrap: false,
288 vim_mode: false,
289 auto_close_brackets: true,
290 highlight_brackets: true,
291 }
292 }
293}
294
295pub fn generate_playground_html(config: &PlaygroundConfig) -> String {
297 let mut html = String::new();
299
300 html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
302 html.push_str(" <meta charset=\"utf-8\">\n");
303 html.push_str(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n");
304 html.push_str(&format!(" <title>{}</title>\n", config.title));
305
306 if let Some(favicon) = &config.favicon {
307 html.push_str(&format!(" <link rel=\"icon\" href=\"{}\" />\n", favicon));
308 }
309
310 html.push_str(" <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/graphql-playground-react@1.7.28/build/static/css/index.css\" />\n");
311
312 if let Some(css) = &config.custom_css {
313 html.push_str(&format!(" <style>{}</style>\n", css));
314 }
315
316 html.push_str(" <style>\n body { margin: 0; padding: 0; overflow: hidden; }\n");
317 html.push_str(" #root { height: 100vh; }\n </style>\n");
318 html.push_str("</head>\n<body>\n <div id=\"root\"></div>\n");
319 html.push_str(" <script src=\"https://cdn.jsdelivr.net/npm/graphql-playground-react@1.7.28/build/static/js/middleware.js\"></script>\n");
320 html.push_str(" <script>\n window.addEventListener(\"load\", function(event) {\n");
321 html.push_str(" GraphQLPlayground.init(document.getElementById(\"root\"), {\n");
322 html.push_str(&format!(
323 " endpoint: \"{}\",\n",
324 config.graphql_endpoint
325 ));
326
327 if let Some(sub_endpoint) = &config.subscription_endpoint {
328 html.push_str(&format!(
329 " subscriptionEndpoint: \"{}\",\n",
330 sub_endpoint
331 ));
332 } else {
333 html.push_str(" subscriptionEndpoint: null,\n");
334 }
335
336 html.push_str(" settings: {},\n");
337 html.push_str(" tabs: [],\n");
338 html.push_str(" config: {\n");
339 html.push_str(" \"general.betaUpdates\": false,\n");
340 html.push_str(&format!(
341 " \"editor.theme\": \"{}\",\n",
342 config.default_theme.as_str()
343 ));
344 html.push_str(" \"editor.cursorShape\": \"line\",\n");
345 html.push_str(" \"editor.reuseHeaders\": true\n");
346 html.push_str(" },\n");
347 html.push_str(" headers: {}\n");
348 html.push_str(" });\n });\n </script>\n");
349 html.push_str("</body>\n</html>");
350
351 html
352}
353
354pub fn get_default_playground_tabs() -> Vec<QueryTab> {
356 vec![
357 QueryTab {
358 id: uuid::Uuid::new_v4().to_string(),
359 name: "Store Information".to_string(),
360 query: r#"# Get basic store information
361query StoreInfo {
362 info {
363 version
364 tripleCount
365 description
366 }
367}"#
368 .to_string(),
369 variables: HashMap::new(),
370 headers: HashMap::new(),
371 last_modified: chrono::Utc::now().to_rfc3339(),
372 },
373 QueryTab {
374 id: uuid::Uuid::new_v4().to_string(),
375 name: "List Subjects".to_string(),
376 query: r#"# List all subjects with pagination
377query ListSubjects($limit: Int = 10, $offset: Int = 0) {
378 subjects(limit: $limit, offset: $offset) {
379 iri
380 label
381 type
382 }
383}"#
384 .to_string(),
385 variables: {
386 let mut vars = HashMap::new();
387 vars.insert("limit".to_string(), serde_json::json!(10));
388 vars.insert("offset".to_string(), serde_json::json!(0));
389 vars
390 },
391 headers: HashMap::new(),
392 last_modified: chrono::Utc::now().to_rfc3339(),
393 },
394 QueryTab {
395 id: uuid::Uuid::new_v4().to_string(),
396 name: "SPARQL Query".to_string(),
397 query: r#"# Execute raw SPARQL query
398query ExecuteSPARQL($sparql: String!) {
399 sparql(query: $sparql) {
400 results
401 }
402}"#
403 .to_string(),
404 variables: {
405 let mut vars = HashMap::new();
406 vars.insert(
407 "sparql".to_string(),
408 serde_json::json!("SELECT * WHERE { ?s ?p ?o } LIMIT 10"),
409 );
410 vars
411 },
412 headers: HashMap::new(),
413 last_modified: chrono::Utc::now().to_rfc3339(),
414 },
415 QueryTab {
416 id: uuid::Uuid::new_v4().to_string(),
417 name: "Subscription Example".to_string(),
418 query: r#"# Subscribe to RDF changes
419subscription OnTripleChanged {
420 tripleChanged {
421 subject
422 predicate
423 object
424 changeType
425 }
426}"#
427 .to_string(),
428 variables: HashMap::new(),
429 headers: HashMap::new(),
430 last_modified: chrono::Utc::now().to_rfc3339(),
431 },
432 ]
433}
434
435pub struct CodeGenerator;
437
438impl CodeGenerator {
439 pub fn generate(
441 target: CodeGenTarget,
442 query: &str,
443 operation_name: Option<&str>,
444 ) -> anyhow::Result<String> {
445 match target {
446 CodeGenTarget::TypeScript => Self::generate_typescript(query, operation_name),
447 CodeGenTarget::JavaScript => Self::generate_javascript(query, operation_name),
448 CodeGenTarget::Rust => Self::generate_rust(query, operation_name),
449 CodeGenTarget::Python => Self::generate_python(query, operation_name),
450 CodeGenTarget::Go => Self::generate_go(query, operation_name),
451 CodeGenTarget::Java => Self::generate_java(query, operation_name),
452 CodeGenTarget::CSharp => Self::generate_csharp(query, operation_name),
453 CodeGenTarget::Swift => Self::generate_swift(query, operation_name),
454 }
455 }
456
457 fn generate_typescript(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
458 let name = operation_name.unwrap_or("Query");
459 Ok(format!(
460 r#"// TypeScript client code
461import {{ gql }} from '@apollo/client';
462
463const {}_QUERY = gql`
464{}
465`;
466
467// Usage:
468// const {{ data, loading, error }} = useQuery({}_QUERY);
469"#,
470 name.to_uppercase(),
471 query,
472 name.to_uppercase()
473 ))
474 }
475
476 fn generate_javascript(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
477 let name = operation_name.unwrap_or("Query");
478 Ok(format!(
479 r#"// JavaScript client code
480const {{ gql }} = require('@apollo/client');
481
482const {}_QUERY = gql`
483{}
484`;
485
486// Usage:
487// client.query({{ query: {}_QUERY }})
488// .then(result => console.log(result));
489"#,
490 name.to_uppercase(),
491 query,
492 name.to_uppercase()
493 ))
494 }
495
496 fn generate_rust(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
497 let name = operation_name.unwrap_or("Query");
498 let mut code = String::new();
499 code.push_str("// Rust client code using graphql_client\n");
500 code.push_str("use graphql_client::{GraphQLQuery, Response};\n\n");
501 code.push_str("#[derive(GraphQLQuery)]\n");
502 code.push_str("#[graphql(\n");
503 code.push_str(" schema_path = \"schema.graphql\",\n");
504 code.push_str(" query_path = \"query.graphql\",\n");
505 code.push_str(" response_derives = \"Debug\"\n");
506 code.push_str(")]\n");
507 code.push_str(&format!("pub struct {};\n\n", name));
508 code.push_str("const QUERY: &str = r#\"\n");
509 code.push_str(query);
510 code.push_str("\n\"#;\n\n");
511 code.push_str("// Usage:\n");
512 code.push_str(&format!("// let variables = {}::Variables {{}};\n", name));
513 code.push_str(&format!(
514 "// let response_body = post_graphql::<{}>(client, url, variables).await?;\n",
515 name
516 ));
517 Ok(code)
518 }
519
520 fn generate_python(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
521 let name = operation_name.unwrap_or("query");
522 let mut code = String::new();
523 code.push_str("# Python client code using gql\n");
524 code.push_str("from gql import gql, Client\n");
525 code.push_str("from gql.transport.requests import RequestsHTTPTransport\n\n");
526 code.push_str(&format!("{}_QUERY = gql(\"\"\"\n", name.to_uppercase()));
527 code.push_str(query);
528 code.push_str("\n\"\"\")\n\n");
529 code.push_str("# Usage:\n");
530 code.push_str("# transport = RequestsHTTPTransport(url='http://localhost:8000/graphql')\n");
531 code.push_str("# client = Client(transport=transport)\n");
532 code.push_str(&format!(
533 "# result = client.execute({}_QUERY)\n",
534 name.to_uppercase()
535 ));
536 Ok(code)
537 }
538
539 fn generate_go(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
540 let name = operation_name.unwrap_or("Query");
541 let mut code = String::new();
542 code.push_str("// Go client code using graphql package\n");
543 code.push_str("package main\n\n");
544 code.push_str("import (\n");
545 code.push_str(" \"context\"\n");
546 code.push_str(" \"github.com/machinebox/graphql\"\n");
547 code.push_str(")\n\n");
548 code.push_str(&format!("const {}Query = `\n", name));
549 code.push_str(query);
550 code.push_str("\n`\n\n");
551 code.push_str("// Usage:\n");
552 code.push_str("// client := graphql.NewClient(\"http://localhost:8000/graphql\")\n");
553 code.push_str(&format!("// req := graphql.NewRequest({}Query)\n", name));
554 code.push_str("// var response map[string]interface{}\n");
555 code.push_str(
556 "// if err := client.Run(context.Background(), req, &response); err != nil {\n",
557 );
558 code.push_str("// log.Fatal(err)\n");
559 code.push_str("// }\n");
560 Ok(code)
561 }
562
563 fn generate_java(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
564 let name = operation_name.unwrap_or("Query");
565 let mut code = String::new();
566 code.push_str("// Java client code using Apollo Android\n");
567 code.push_str("import com.apollographql.apollo.ApolloClient;\n\n");
568 code.push_str(&format!("public class {}Query {{\n", name));
569 code.push_str(" private static final String QUERY = \"\"\"\n");
570 code.push_str(query);
571 code.push_str("\n \"\"\";\n\n");
572 code.push_str(" // Usage with Apollo Android client\n");
573 code.push_str(" // ApolloClient apolloClient = ApolloClient.builder()\n");
574 code.push_str(" // .serverUrl(\"http://localhost:8000/graphql\")\n");
575 code.push_str(" // .build();\n");
576 code.push_str("}\n");
577 Ok(code)
578 }
579
580 fn generate_csharp(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
581 let name = operation_name.unwrap_or("Query");
582 let mut code = String::new();
583 code.push_str("// C# client code using GraphQL.Client\n");
584 code.push_str("using GraphQL;\n");
585 code.push_str("using GraphQL.Client.Http;\n\n");
586 code.push_str(&format!("public class {}Client\n", name));
587 code.push_str("{\n");
588 code.push_str(" private const string Query = @\"\n");
589 code.push_str(query);
590 code.push_str("\n \";\n\n");
591 code.push_str(" // Usage:\n");
592 code.push_str(" // var graphQLClient = new GraphQLHttpClient(\"http://localhost:8000/graphql\");\n");
593 code.push_str(" // var request = new GraphQLRequest { Query = Query };\n");
594 code.push_str(
595 " // var response = await graphQLClient.SendQueryAsync<ResponseType>(request);\n",
596 );
597 code.push_str("}\n");
598 Ok(code)
599 }
600
601 fn generate_swift(query: &str, operation_name: Option<&str>) -> anyhow::Result<String> {
602 let name = operation_name.unwrap_or("Query");
603 let mut code = String::new();
604 code.push_str("// Swift client code using Apollo iOS\n");
605 code.push_str("import Apollo\n\n");
606 code.push_str(&format!("let {}Query = \"\"\"\n", name));
607 code.push_str(query);
608 code.push_str("\n\"\"\"\n\n");
609 code.push_str("// Usage with Apollo iOS client\n");
610 code.push_str(
611 "// let apollo = ApolloClient(url: URL(string: \"http://localhost:8000/graphql\")!)\n",
612 );
613 code.push_str(&format!(
614 "// apollo.fetch(query: {}Query) {{ result in\n",
615 name
616 ));
617 code.push_str("// switch result {\n");
618 code.push_str("// case .success(let graphQLResult):\n");
619 code.push_str("// print(graphQLResult.data)\n");
620 code.push_str("// case .failure(let error):\n");
621 code.push_str("// print(error)\n");
622 code.push_str("// }\n");
623 code.push_str("// }\n");
624 Ok(code)
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn test_playground_config_default() {
634 let config = PlaygroundConfig::default();
635 assert_eq!(config.endpoint, "/playground");
636 assert!(config.enable_tabs);
637 assert!(config.enable_history);
638 }
639
640 #[test]
641 fn test_playground_theme() {
642 assert_eq!(PlaygroundTheme::Dark.as_str(), "dark");
643 assert_eq!(PlaygroundTheme::Light.as_str(), "light");
644 }
645
646 #[test]
647 fn test_code_gen_target() {
648 assert_eq!(CodeGenTarget::TypeScript.as_str(), "typescript");
649 assert_eq!(CodeGenTarget::Rust.as_str(), "rust");
650 assert_eq!(CodeGenTarget::Python.as_str(), "python");
651
652 let all = CodeGenTarget::all();
653 assert_eq!(all.len(), 8);
654 }
655
656 #[test]
657 fn test_query_tab_creation() {
658 let tab = QueryTab::new("Test Query".to_string(), "{ __typename }".to_string());
659 assert_eq!(tab.name, "Test Query");
660 assert_eq!(tab.query, "{ __typename }");
661 assert!(!tab.id.is_empty());
662 }
663
664 #[test]
665 fn test_playground_settings_default() {
666 let settings = PlaygroundSettings::default();
667 assert_eq!(settings.theme, PlaygroundTheme::Dark);
668 assert_eq!(settings.font_size, 14);
669 assert!(!settings.tabs.is_empty());
670 }
671
672 #[test]
673 fn test_editor_settings_default() {
674 let settings = EditorSettings::default();
675 assert_eq!(settings.tab_size, 2);
676 assert!(settings.use_spaces);
677 assert!(settings.line_numbers);
678 }
679
680 #[test]
681 fn test_generate_playground_html() {
682 let config = PlaygroundConfig::default();
683 let html = generate_playground_html(&config);
684
685 assert!(html.contains("<!DOCTYPE html>"));
686 assert!(html.contains("OxiRS GraphQL Playground"));
687 assert!(html.contains("/graphql"));
688 assert!(html.contains("GraphQLPlayground.init"));
689 }
690
691 #[test]
692 fn test_default_playground_tabs() {
693 let tabs = get_default_playground_tabs();
694 assert_eq!(tabs.len(), 4);
695
696 assert_eq!(&tabs[0].name, &String::from("Store Information"));
697 assert_eq!(&tabs[1].name, &String::from("List Subjects"));
698 assert_eq!(&tabs[2].name, &String::from("SPARQL Query"));
699 assert_eq!(&tabs[3].name, &String::from("Subscription Example"));
700 }
701
702 #[test]
703 fn test_code_generator_typescript() {
704 let query = "query TestQuery { __typename }";
705 let code =
706 CodeGenerator::generate_typescript(query, Some("TestQuery")).expect("should succeed");
707
708 assert!(code.contains("TypeScript"));
709 assert!(code.contains("TESTQUERY_QUERY"));
710 assert!(code.contains(query));
711 }
712
713 #[test]
714 fn test_code_generator_rust() {
715 let query = "query TestQuery { __typename }";
716 let code = CodeGenerator::generate_rust(query, Some("TestQuery")).expect("should succeed");
717
718 assert!(code.contains("Rust"));
719 assert!(code.contains("GraphQLQuery"));
720 assert!(code.contains(query));
721 }
722
723 #[test]
724 fn test_code_generator_python() {
725 let query = "query TestQuery { __typename }";
726 let code =
727 CodeGenerator::generate_python(query, Some("TestQuery")).expect("should succeed");
728
729 assert!(code.contains("Python"));
730 assert!(code.contains("gql"));
731 assert!(code.contains(query));
732 }
733
734 #[test]
735 fn test_code_generator_all_targets() {
736 let query = "query Test { __typename }";
737
738 for target in CodeGenTarget::all() {
739 let result = CodeGenerator::generate(target, query, Some("Test"));
740 assert!(result.is_ok(), "Failed for target: {:?}", target);
741 }
742 }
743
744 #[test]
745 fn test_playground_config_custom() {
746 let config = PlaygroundConfig {
747 title: String::from("Custom Playground"),
748 default_theme: PlaygroundTheme::Light,
749 enable_tracing: false,
750 ..Default::default()
751 };
752
753 let html = generate_playground_html(&config);
754 let title_check = String::from("Custom Playground");
755 assert!(html.contains(&title_check));
756 assert!(html.contains("editor.theme"));
757 }
758}