allframe_core/router/
graphiql.rs1use std::collections::HashMap;
33
34use serde::{Deserialize, Serialize};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
38#[serde(rename_all = "lowercase")]
39pub enum GraphiQLTheme {
40 Light,
42 #[default]
44 Dark,
45}
46
47#[derive(Debug, Clone)]
52pub struct GraphiQLConfig {
53 pub endpoint_url: String,
55
56 pub subscription_url: Option<String>,
58
59 pub theme: GraphiQLTheme,
61
62 pub enable_explorer: bool,
64
65 pub enable_history: bool,
67
68 pub headers: HashMap<String, String>,
70
71 pub cdn_url: String,
73
74 pub custom_css: Option<String>,
76}
77
78impl Default for GraphiQLConfig {
79 fn default() -> Self {
80 Self {
81 endpoint_url: "/graphql".to_string(),
82 subscription_url: None,
83 theme: GraphiQLTheme::Dark,
84 enable_explorer: true,
85 enable_history: true,
86 headers: HashMap::new(),
87 cdn_url: "https://unpkg.com/graphiql@3.0.0/graphiql.min.css".to_string(),
88 custom_css: None,
89 }
90 }
91}
92
93impl GraphiQLConfig {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn endpoint_url(mut self, url: impl Into<String>) -> Self {
101 self.endpoint_url = url.into();
102 self
103 }
104
105 pub fn subscription_url(mut self, url: impl Into<String>) -> Self {
107 self.subscription_url = Some(url.into());
108 self
109 }
110
111 pub fn theme(mut self, theme: GraphiQLTheme) -> Self {
113 self.theme = theme;
114 self
115 }
116
117 pub fn enable_explorer(mut self, enable: bool) -> Self {
119 self.enable_explorer = enable;
120 self
121 }
122
123 pub fn enable_history(mut self, enable: bool) -> Self {
125 self.enable_history = enable;
126 self
127 }
128
129 pub fn add_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131 self.headers.insert(key.into(), value.into());
132 self
133 }
134
135 pub fn cdn_url(mut self, url: impl Into<String>) -> Self {
137 self.cdn_url = url.into();
138 self
139 }
140
141 pub fn custom_css(mut self, css: impl Into<String>) -> Self {
143 self.custom_css = Some(css.into());
144 self
145 }
146
147 pub fn to_json(&self) -> serde_json::Value {
149 let mut config = serde_json::json!({
150 "endpoint": self.endpoint_url,
151 "theme": self.theme,
152 "explorer": self.enable_explorer,
153 "history": self.enable_history,
154 });
155
156 if let Some(ref sub_url) = self.subscription_url {
157 config["subscriptionUrl"] = serde_json::Value::String(sub_url.clone());
158 }
159
160 if !self.headers.is_empty() {
161 config["headers"] = serde_json::to_value(&self.headers).unwrap();
162 }
163
164 config
165 }
166}
167
168pub fn graphiql_html(config: &GraphiQLConfig, title: &str) -> String {
195 let config_json = serde_json::to_string(&config.to_json()).unwrap();
196 let theme_class = match config.theme {
197 GraphiQLTheme::Light => "graphiql-light",
198 GraphiQLTheme::Dark => "graphiql-dark",
199 };
200
201 let custom_css = config.custom_css.as_deref().unwrap_or("");
202
203 format!(
204 r#"<!DOCTYPE html>
205<html>
206<head>
207 <meta charset="utf-8">
208 <meta name="viewport" content="width=device-width, initial-scale=1">
209 <title>{title}</title>
210 <link rel="stylesheet" href="{cdn_url}" />
211 <style>
212 body {{
213 height: 100vh;
214 margin: 0;
215 overflow: hidden;
216 }}
217 #graphiql {{
218 height: 100vh;
219 }}
220 {custom_css}
221 </style>
222</head>
223<body class="{theme_class}">
224 <div id="graphiql">Loading GraphiQL...</div>
225 <script
226 crossorigin
227 src="https://unpkg.com/react@18/umd/react.production.min.js"
228 ></script>
229 <script
230 crossorigin
231 src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
232 ></script>
233 <script
234 crossorigin
235 src="https://unpkg.com/graphiql@3.0.0/graphiql.min.js"
236 ></script>
237 <script>
238 const config = {config_json};
239 const root = ReactDOM.createRoot(document.getElementById('graphiql'));
240 const fetcher = GraphiQL.createFetcher({{
241 url: config.endpoint,
242 subscriptionUrl: config.subscriptionUrl,
243 headers: config.headers || {{}}
244 }});
245 root.render(
246 React.createElement(GraphiQL, {{
247 fetcher: fetcher,
248 defaultEditorToolsVisibility: config.explorer,
249 storage: config.history ? window.localStorage : null
250 }})
251 );
252 </script>
253</body>
254</html>"#,
255 title = title,
256 cdn_url = config.cdn_url,
257 theme_class = theme_class,
258 custom_css = custom_css,
259 config_json = config_json,
260 )
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_graphiql_config_defaults() {
269 let config = GraphiQLConfig::new();
270 assert_eq!(config.endpoint_url, "/graphql");
271 assert_eq!(config.theme, GraphiQLTheme::Dark);
272 assert!(config.enable_explorer);
273 assert!(config.enable_history);
274 assert!(config.subscription_url.is_none());
275 }
276
277 #[test]
278 fn test_graphiql_config_builder() {
279 let config = GraphiQLConfig::new()
280 .endpoint_url("/api/graphql")
281 .subscription_url("ws://localhost:4000/graphql")
282 .theme(GraphiQLTheme::Light)
283 .enable_explorer(false)
284 .enable_history(false)
285 .add_header("Authorization", "Bearer token123");
286
287 assert_eq!(config.endpoint_url, "/api/graphql");
288 assert_eq!(
289 config.subscription_url,
290 Some("ws://localhost:4000/graphql".to_string())
291 );
292 assert_eq!(config.theme, GraphiQLTheme::Light);
293 assert!(!config.enable_explorer);
294 assert!(!config.enable_history);
295 assert_eq!(
296 config.headers.get("Authorization"),
297 Some(&"Bearer token123".to_string())
298 );
299 }
300
301 #[test]
302 fn test_graphiql_html_generation() {
303 let config = GraphiQLConfig::new()
304 .endpoint_url("/graphql")
305 .theme(GraphiQLTheme::Dark);
306
307 let html = graphiql_html(&config, "Test API");
308
309 assert!(html.contains("<!DOCTYPE html>"));
310 assert!(html.contains("Test API"));
311 assert!(html.contains("/graphql"));
312 assert!(html.contains("graphiql-dark"));
313 assert!(html.contains("GraphiQL"));
314 }
315
316 #[test]
317 fn test_graphiql_html_with_subscription() {
318 let config = GraphiQLConfig::new()
319 .endpoint_url("/graphql")
320 .subscription_url("ws://localhost:3000/graphql");
321
322 let html = graphiql_html(&config, "Test API");
323
324 assert!(html.contains("ws://localhost:3000/graphql"));
325 assert!(html.contains("subscriptionUrl"));
326 }
327
328 #[test]
329 fn test_graphiql_theme_serialization() {
330 let light = GraphiQLTheme::Light;
331 let dark = GraphiQLTheme::Dark;
332
333 assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\"");
334 assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\"");
335 }
336
337 #[test]
338 fn test_graphiql_config_json_generation() {
339 let config = GraphiQLConfig::new()
340 .endpoint_url("/api/graphql")
341 .theme(GraphiQLTheme::Light)
342 .enable_explorer(true)
343 .add_header("X-API-Key", "secret");
344
345 let json = config.to_json();
346
347 assert_eq!(json["endpoint"], "/api/graphql");
348 assert_eq!(json["theme"], "light");
349 assert_eq!(json["explorer"], true);
350 assert_eq!(json["headers"]["X-API-Key"], "secret");
351 }
352
353 #[test]
354 fn test_graphiql_custom_css() {
355 let config = GraphiQLConfig::new().custom_css("body { background: #1a1a1a; }");
356
357 let html = graphiql_html(&config, "Test API");
358
359 assert!(html.contains("body { background: #1a1a1a; }"));
360 }
361}