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