1use std::collections::HashMap;
2
3use handlebars::Handlebars;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Default, Serialize)]
9pub struct AltairSource<'a> {
10 #[serde(default, skip_serializing_if = "Option::is_none")]
11 title: Option<&'a str>,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 options: Option<serde_json::Value>,
14}
15impl<'a> AltairSource<'a> {
16 pub fn build() -> AltairSource<'a> {
18 Default::default()
19 }
20
21 pub fn title(self, title: &'a str) -> AltairSource<'a> {
23 AltairSource {
24 title: Some(title),
25 ..self
26 }
27 }
28
29 pub fn options<T: Serialize>(self, options: T) -> AltairSource<'a> {
64 AltairSource {
65 options: Some(serde_json::to_value(options).expect("Failed to serialize options")),
66 ..self
67 }
68 }
69
70 pub fn finish(self) -> String {
72 let mut handlebars = Handlebars::new();
73
74 handlebars.register_helper("toJson", Box::new(ToJsonHelper));
75
76 handlebars
77 .register_template_string("altair_source", include_str!("./altair_source.hbs"))
78 .expect("Failed to register template");
79
80 handlebars
81 .render("altair_source", &self)
82 .expect("Failed to render template")
83 }
84}
85
86#[derive(Default, Serialize, Deserialize, JsonSchema)]
88#[serde(rename_all = "camelCase")]
89pub struct AltairWindowOptions {
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub initial_name: Option<String>,
93 #[serde(
95 rename = "endpointURL",
96 default,
97 skip_serializing_if = "Option::is_none"
98 )]
99 pub endpoint_url: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub subscriptions_endpoint: Option<String>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub subscriptions_protocol: Option<String>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub initial_query: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub initial_variables: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub initial_pre_request_script: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub initial_post_request_script: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub initial_authorization: Option<AltairAuthorizationProviderInput>,
125 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
132 pub initial_headers: HashMap<String, String>,
133 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
135 pub initial_subscriptions_payload: HashMap<String, String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub initial_http_method: Option<AltairHttpVerb>,
139}
140
141#[derive(Default, Serialize, Deserialize, JsonSchema)]
143#[serde(rename_all = "camelCase")]
144pub struct AltairConfigOptions {
145 #[serde(default, flatten, skip_serializing_if = "Option::is_none")]
147 pub window_options: Option<AltairWindowOptions>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub initial_environments: Option<AltairInitialEnvironments>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub instance_storage_namespace: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub initial_settings: Option<AltairSettingsState>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub preserve_state: Option<bool>,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
182 pub initial_windows: Vec<AltairWindowOptions>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub persisted_settings: Option<AltairSettingsState>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub disable_account: Option<bool>,
190}
191
192#[derive(Serialize, Deserialize, JsonSchema)]
194#[allow(missing_docs)]
195pub enum AltairHttpVerb {
196 POST,
197 GET,
198 PUT,
199 DELETE,
200}
201
202#[derive(Default, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "camelCase")]
205pub struct AltairInitialEnvironments {
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub base: Option<AltairInitialEnvironmentState>,
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub sub_environments: Vec<AltairInitialEnvironmentState>,
212}
213
214#[derive(Default, Serialize, Deserialize, JsonSchema)]
216#[serde(rename_all = "camelCase")]
217pub struct AltairInitialEnvironmentState {
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub id: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub title: Option<String>,
224 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
226 pub variables: HashMap<String, String>,
227}
228
229#[derive(Serialize, Deserialize, JsonSchema)]
231#[serde(tag = "type", content = "data")]
232pub enum AltairAuthorizationProviderInput {
233 #[serde(rename = "api-key")]
235 ApiKey {
236 header_name: String,
238 header_value: String,
240 },
241 #[serde(rename = "basic")]
243 Basic {
244 password: String,
246 username: String,
248 },
249 #[serde(rename = "bearer")]
251 Bearer {
252 token: String,
254 },
255 #[serde(rename = "oauth2")]
257 OAuth2 {
258 access_token_response: String,
260 },
261}
262
263#[derive(Default, Serialize, Deserialize, JsonSchema)]
265#[serde(rename_all = "camelCase")]
266pub struct AltairSettingsState {
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub theme: Option<String>,
270 #[serde(
272 rename = "theme.dark",
273 default,
274 skip_serializing_if = "Option::is_none"
275 )]
276 pub theme_dark: Option<String>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub language: Option<AltairSettingsLanguage>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub add_query_depth_limit: Option<usize>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub tab_size: Option<usize>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub enable_experimental: Option<bool>,
291 #[serde(
295 rename = "theme.fontsize",
296 default,
297 skip_serializing_if = "Option::is_none"
298 )]
299 pub theme_font_size: Option<usize>,
300 #[serde(
302 rename = "theme.editorFontFamily",
303 default,
304 skip_serializing_if = "Option::is_none"
305 )]
306 pub theme_editor_font_family: Option<String>,
307 #[serde(
309 rename = "theme.editorFontSize",
310 default,
311 skip_serializing_if = "Option::is_none"
312 )]
313 pub theme_editor_font_size: Option<usize>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub disable_push_notification: Option<bool>,
317 #[serde(rename = "plugin.list", default, skip_serializing_if = "Vec::is_empty")]
319 pub plugin_list: Vec<String>,
320 #[serde(
322 rename = "request.withCredentials",
323 default,
324 skip_serializing_if = "Option::is_none"
325 )]
326 pub request_with_credentials: Option<bool>,
327 #[serde(
329 rename = "schema.reloadOnStart",
330 default,
331 skip_serializing_if = "Option::is_none"
332 )]
333 pub schema_reload_on_start: Option<bool>,
334 #[serde(
336 rename = "alert.disableUpdateNotification",
337 default,
338 skip_serializing_if = "Option::is_none"
339 )]
340 pub alert_disable_update_notification: Option<bool>,
341 #[serde(
343 rename = "alert.disableWarnings",
344 default,
345 skip_serializing_if = "Option::is_none"
346 )]
347 pub alert_disable_warnings: Option<bool>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub history_depth: Option<usize>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub disable_line_numbers: Option<bool>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub theme_config: Option<serde_json::Value>,
357 #[serde(
359 rename = "themeConfig.dark",
360 default,
361 skip_serializing_if = "Option::is_none"
362 )]
363 pub theme_config_dark: Option<serde_json::Value>,
364 #[serde(
366 rename = "response.hideExtensions",
367 default,
368 skip_serializing_if = "Option::is_none"
369 )]
370 pub response_hide_extensions: Option<bool>,
371 #[serde(
373 rename = "editor.shortcuts",
374 default,
375 skip_serializing_if = "HashMap::is_empty"
376 )]
377 pub editor_shortcuts: HashMap<String, String>,
378 #[serde(
380 rename = "beta.disable.newEditor",
381 default,
382 skip_serializing_if = "Option::is_none"
383 )]
384 pub beta_disable_new_editor: Option<bool>,
385 #[serde(
387 rename = "beta.disable.newScript",
388 default,
389 skip_serializing_if = "Option::is_none"
390 )]
391 pub beta_disable_new_script: Option<bool>,
392 #[serde(
396 rename = "script.allowedCookies",
397 default,
398 skip_serializing_if = "Vec::is_empty"
399 )]
400 pub script_allowed_cookies: Vec<String>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub enable_tablist_scrollbar: Option<bool>,
404}
405
406#[derive(Serialize, Deserialize, JsonSchema)]
408#[allow(missing_docs)]
409pub enum AltairSettingsLanguage {
410 #[serde(rename = "en-US")]
411 English,
412 #[serde(rename = "fr-FR")]
413 French,
414 #[serde(rename = "es-ES")]
415 EspaƱol,
416 #[serde(rename = "cs-CZ")]
417 Czech,
418 #[serde(rename = "de-DE")]
419 German,
420 #[serde(rename = "pt-BR")]
421 Brazilian,
422 #[serde(rename = "ru-RU")]
423 Russian,
424 #[serde(rename = "uk-UA")]
425 Ukrainian,
426 #[serde(rename = "zh-CN")]
427 ChineseSimplified,
428 #[serde(rename = "ja-JP")]
429 Japanese,
430 #[serde(rename = "sr-SP")]
431 Serbian,
432 #[serde(rename = "it-IT")]
433 Italian,
434 #[serde(rename = "pl-PL")]
435 Polish,
436 #[serde(rename = "ko-KR")]
437 Korean,
438 #[serde(rename = "ro-RO")]
439 Romanian,
440 #[serde(rename = "vi-VN")]
441 Vietnamese,
442}
443
444struct ToJsonHelper;
445impl handlebars::HelperDef for ToJsonHelper {
446 #[allow(unused_assignments)]
447 fn call_inner<'reg: 'rc, 'rc>(
448 &self,
449 h: &handlebars::Helper<'rc>,
450 r: &'reg handlebars::Handlebars<'reg>,
451 _: &'rc handlebars::Context,
452 _: &mut handlebars::RenderContext<'reg, 'rc>,
453 ) -> std::result::Result<handlebars::ScopedJson<'rc>, handlebars::RenderError> {
454 let mut param_idx = 0;
455 let obj = h
456 .param(param_idx)
457 .and_then(|x| {
458 if r.strict_mode() && x.is_value_missing() {
459 None
460 } else {
461 Some(x.value())
462 }
463 })
464 .ok_or_else(|| {
465 handlebars::RenderErrorReason::ParamNotFoundForName("toJson", "obj".to_string())
466 })
467 .and_then(|x| {
468 x.as_object().ok_or_else(|| {
469 handlebars::RenderErrorReason::ParamTypeMismatchForName(
470 "toJson",
471 "obj".to_string(),
472 "object".to_string(),
473 )
474 })
475 })?;
476 param_idx += 1;
477 let result = if obj.is_empty() {
478 "{}".to_owned()
479 } else {
480 serde_json::to_string(&obj).expect("Failed to serialize json")
481 };
482 Ok(handlebars::ScopedJson::Derived(
483 handlebars::JsonValue::from(result),
484 ))
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use serde_json::json;
491
492 use super::*;
493
494 #[test]
495 fn test_without_options() {
496 let altair_source = AltairSource::build().title("Custom Title").finish();
497
498 assert_eq!(
499 altair_source,
500 r#"<!DOCTYPE html>
501<html>
502
503 <head>
504 <meta charset="utf-8">
505
506 <title>Custom Title</title>
507
508 <base href="https://unpkg.com/altair-static@latest/build/dist/">
509
510 <meta name="viewport" content="width=device-width, initial-scale=1">
511 <link rel="icon" type="image/x-icon" href="favicon.ico">
512 <link rel="stylesheet" href="styles.css">
513 </head>
514
515 <body>
516 <script>
517 document.addEventListener('DOMContentLoaded', () => {
518 AltairGraphQL.init();
519 });
520 </script>
521 <app-root>
522 <style>
523 .loading-screen {
524 /*Prevents the loading screen from showing until CSS is downloaded*/
525 display: none;
526 }
527 </style>
528 <div class="loading-screen styled">
529 <div class="loading-screen-inner">
530 <div class="loading-screen-logo-container">
531 <img src="assets/img/logo_350.svg" alt="Altair">
532 </div>
533 <div class="loading-screen-loading-indicator">
534 <span class="loading-indicator-dot"></span>
535 <span class="loading-indicator-dot"></span>
536 <span class="loading-indicator-dot"></span>
537 </div>
538 </div>
539 </div>
540 </app-root>
541 <script type="text/javascript" src="runtime.js"></script>
542 <script type="text/javascript" src="polyfills.js"></script>
543 <script type="text/javascript" src="main.js"></script>
544 </body>
545
546</html>"#
547 )
548 }
549
550 #[test]
551 fn test_with_dynamic() {
552 let altair_source = AltairSource::build()
553 .options(json!({
554 "endpointURL": "/",
555 "subscriptionsEndpoint": "/ws",
556 }))
557 .finish();
558
559 assert_eq!(
560 altair_source,
561 r#"<!DOCTYPE html>
562<html>
563
564 <head>
565 <meta charset="utf-8">
566
567 <title>Altair</title>
568
569 <base href="https://unpkg.com/altair-static@latest/build/dist/">
570
571 <meta name="viewport" content="width=device-width, initial-scale=1">
572 <link rel="icon" type="image/x-icon" href="favicon.ico">
573 <link rel="stylesheet" href="styles.css">
574 </head>
575
576 <body>
577 <script>
578 document.addEventListener('DOMContentLoaded', () => {
579 AltairGraphQL.init({"endpointURL":"/","subscriptionsEndpoint":"/ws"});
580 });
581 </script>
582 <app-root>
583 <style>
584 .loading-screen {
585 /*Prevents the loading screen from showing until CSS is downloaded*/
586 display: none;
587 }
588 </style>
589 <div class="loading-screen styled">
590 <div class="loading-screen-inner">
591 <div class="loading-screen-logo-container">
592 <img src="assets/img/logo_350.svg" alt="Altair">
593 </div>
594 <div class="loading-screen-loading-indicator">
595 <span class="loading-indicator-dot"></span>
596 <span class="loading-indicator-dot"></span>
597 <span class="loading-indicator-dot"></span>
598 </div>
599 </div>
600 </div>
601 </app-root>
602 <script type="text/javascript" src="runtime.js"></script>
603 <script type="text/javascript" src="polyfills.js"></script>
604 <script type="text/javascript" src="main.js"></script>
605 </body>
606
607</html>"#
608 )
609 }
610
611 #[test]
612 fn test_with_static() {
613 let altair_source = AltairSource::build()
614 .options(AltairConfigOptions {
615 window_options: Some(AltairWindowOptions {
616 endpoint_url: Some("/".to_owned()),
617 subscriptions_endpoint: Some("/ws".to_owned()),
618 ..Default::default()
619 }),
620 ..Default::default()
621 })
622 .finish();
623
624 assert_eq!(
625 altair_source,
626 r#"<!DOCTYPE html>
627<html>
628
629 <head>
630 <meta charset="utf-8">
631
632 <title>Altair</title>
633
634 <base href="https://unpkg.com/altair-static@latest/build/dist/">
635
636 <meta name="viewport" content="width=device-width, initial-scale=1">
637 <link rel="icon" type="image/x-icon" href="favicon.ico">
638 <link rel="stylesheet" href="styles.css">
639 </head>
640
641 <body>
642 <script>
643 document.addEventListener('DOMContentLoaded', () => {
644 AltairGraphQL.init({"endpointURL":"/","subscriptionsEndpoint":"/ws"});
645 });
646 </script>
647 <app-root>
648 <style>
649 .loading-screen {
650 /*Prevents the loading screen from showing until CSS is downloaded*/
651 display: none;
652 }
653 </style>
654 <div class="loading-screen styled">
655 <div class="loading-screen-inner">
656 <div class="loading-screen-logo-container">
657 <img src="assets/img/logo_350.svg" alt="Altair">
658 </div>
659 <div class="loading-screen-loading-indicator">
660 <span class="loading-indicator-dot"></span>
661 <span class="loading-indicator-dot"></span>
662 <span class="loading-indicator-dot"></span>
663 </div>
664 </div>
665 </div>
666 </app-root>
667 <script type="text/javascript" src="runtime.js"></script>
668 <script type="text/javascript" src="polyfills.js"></script>
669 <script type="text/javascript" src="main.js"></script>
670 </body>
671
672</html>"#
673 )
674 }
675}