Skip to main content

oxirs_gql/
playground_integration.rs

1//! GraphQL Playground Integration
2//!
3//! This module provides advanced GraphQL Playground IDE with features:
4//! - **Multiple Tabs**: Work with multiple queries simultaneously
5//! - **Query History**: Persistent history with search and filtering
6//! - **Schema Documentation**: Interactive schema explorer
7//! - **Response Tracing**: Detailed performance analysis
8//! - **Themes**: Multiple color schemes and customization
9//! - **Collaboration**: Share queries with teammates
10//! - **Code Generation**: Generate client code from queries
11//! - **Request Headers**: Manage authentication and custom headers
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// GraphQL Playground configuration
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PlaygroundConfig {
19    /// Playground endpoint path
20    pub endpoint: String,
21
22    /// GraphQL endpoint URL
23    pub graphql_endpoint: String,
24
25    /// Subscription endpoint (WebSocket)
26    pub subscription_endpoint: Option<String>,
27
28    /// Enable multiple tabs
29    pub enable_tabs: bool,
30
31    /// Enable query history
32    pub enable_history: bool,
33
34    /// Enable schema polling (auto-refresh schema)
35    pub enable_schema_polling: bool,
36
37    /// Schema polling interval (in seconds)
38    pub schema_polling_interval: u64,
39
40    /// Enable request tracing
41    pub enable_tracing: bool,
42
43    /// Enable code generation
44    pub enable_code_gen: bool,
45
46    /// Supported code generation targets
47    pub code_gen_targets: Vec<CodeGenTarget>,
48
49    /// Default theme
50    pub default_theme: PlaygroundTheme,
51
52    /// Enable settings persistence
53    pub enable_settings_persistence: bool,
54
55    /// Custom title
56    pub title: String,
57
58    /// Custom favicon URL
59    pub favicon: Option<String>,
60
61    /// Default request headers
62    pub default_headers: HashMap<String, String>,
63
64    /// Enable query sharing
65    pub enable_sharing: bool,
66
67    /// Custom CSS
68    pub custom_css: Option<String>,
69
70    /// Enable prettier formatting
71    pub enable_prettier: bool,
72
73    /// Enable linting
74    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/// Playground color themes
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub enum PlaygroundTheme {
111    /// Dark theme (default)
112    Dark,
113    /// Light theme
114    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/// Code generation targets
127#[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/// Query tab configuration
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct QueryTab {
170    /// Tab ID
171    pub id: String,
172
173    /// Tab name
174    pub name: String,
175
176    /// GraphQL query
177    pub query: String,
178
179    /// Query variables
180    pub variables: HashMap<String, serde_json::Value>,
181
182    /// Request headers
183    pub headers: HashMap<String, String>,
184
185    /// Last modified timestamp
186    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/// Playground settings
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct PlaygroundSettings {
205    /// Active tab ID
206    pub active_tab: Option<String>,
207
208    /// All query tabs
209    pub tabs: Vec<QueryTab>,
210
211    /// Current theme
212    pub theme: PlaygroundTheme,
213
214    /// Font size
215    pub font_size: u32,
216
217    /// Editor settings
218    pub editor: EditorSettings,
219
220    /// Request headers
221    pub headers: HashMap<String, String>,
222
223    /// Tracing enabled
224    pub tracing_enabled: bool,
225
226    /// Auto-completion enabled
227    pub auto_completion_enabled: bool,
228
229    /// Prettier enabled
230    pub prettier_enabled: bool,
231
232    /// Linting enabled
233    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/// Editor settings
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct EditorSettings {
259    /// Tab size
260    pub tab_size: u32,
261
262    /// Use spaces instead of tabs
263    pub use_spaces: bool,
264
265    /// Show line numbers
266    pub line_numbers: bool,
267
268    /// Word wrap
269    pub word_wrap: bool,
270
271    /// Vim mode
272    pub vim_mode: bool,
273
274    /// Auto-close brackets
275    pub auto_close_brackets: bool,
276
277    /// Highlight matching brackets
278    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
295/// Generate Playground HTML
296pub fn generate_playground_html(config: &PlaygroundConfig) -> String {
297    // Build HTML incrementally to avoid complex string literal issues
298    let mut html = String::new();
299
300    // HTML header
301    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
354/// Default query templates for RDF operations
355pub 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
435/// Code generator for different languages
436pub struct CodeGenerator;
437
438impl CodeGenerator {
439    /// Generate client code from GraphQL query
440    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}