use std::collections::HashMap;
use handlebars::Handlebars;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize)]
pub struct AltairSource<'a> {
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<&'a str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
options: Option<serde_json::Value>,
}
impl<'a> AltairSource<'a> {
pub fn build() -> AltairSource<'a> {
Default::default()
}
pub fn title(self, title: &'a str) -> AltairSource<'a> {
AltairSource {
title: Some(title),
..self
}
}
pub fn options<T: Serialize>(self, options: T) -> AltairSource<'a> {
AltairSource {
options: Some(serde_json::to_value(options).expect("Failed to serialize options")),
..self
}
}
pub fn finish(self) -> String {
let mut handlebars = Handlebars::new();
handlebars.register_helper("toJson", Box::new(ToJsonHelper));
handlebars
.register_template_string("altair_source", include_str!("./altair_source.hbs"))
.expect("Failed to register template");
handlebars
.render("altair_source", &self)
.expect("Failed to render template")
}
}
#[derive(Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AltairWindowOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_name: Option<String>,
#[serde(
rename = "endpointURL",
default,
skip_serializing_if = "Option::is_none"
)]
pub endpoint_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscriptions_endpoint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscriptions_protocol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_variables: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_pre_request_script: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_post_request_script: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub initial_headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub initial_subscriptions_payload: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_http_method: Option<AltairHttpVerb>,
}
#[derive(Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AltairConfigOptions {
#[serde(default, flatten, skip_serializing_if = "Option::is_none")]
pub window_options: Option<AltairWindowOptions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_environments: Option<AltairInitialEnvironments>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instance_storage_namespace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_settings: Option<AltairSettingsState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preserve_state: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initial_windows: Vec<AltairWindowOptions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persisted_settings: Option<AltairSettingsState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_account: Option<bool>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(missing_docs)]
pub enum AltairHttpVerb {
POST,
GET,
PUT,
DELETE,
}
#[derive(Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AltairInitialEnvironments {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base: Option<AltairInitialEnvironmentState>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sub_environments: Vec<AltairInitialEnvironmentState>,
}
#[derive(Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AltairInitialEnvironmentState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub variables: HashMap<String, String>,
}
#[derive(Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct AltairSettingsState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
#[serde(
rename = "theme.dark",
default,
skip_serializing_if = "Option::is_none"
)]
pub theme_dark: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<AltairSettingsLanguage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub add_query_depth_limit: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tab_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_experimental: Option<bool>,
#[serde(
rename = "theme.fontsize",
default,
skip_serializing_if = "Option::is_none"
)]
pub theme_font_size: Option<usize>,
#[serde(
rename = "theme.editorFontFamily",
default,
skip_serializing_if = "Option::is_none"
)]
pub theme_editor_font_family: Option<String>,
#[serde(
rename = "theme.editorFontSize",
default,
skip_serializing_if = "Option::is_none"
)]
pub theme_editor_font_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_push_notification: Option<bool>,
#[serde(rename = "plugin.list", default, skip_serializing_if = "Vec::is_empty")]
pub plugin_list: Vec<String>,
#[serde(
rename = "request.withCredentials",
default,
skip_serializing_if = "Option::is_none"
)]
pub request_with_credentials: Option<bool>,
#[serde(
rename = "schema.reloadOnStart",
default,
skip_serializing_if = "Option::is_none"
)]
pub schema_reload_on_start: Option<bool>,
#[serde(
rename = "alert.disableUpdateNotification",
default,
skip_serializing_if = "Option::is_none"
)]
pub alert_disable_update_notification: Option<bool>,
#[serde(
rename = "alert.disableWarnings",
default,
skip_serializing_if = "Option::is_none"
)]
pub alert_disable_warnings: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history_depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_line_numbers: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_config: Option<serde_json::Value>,
#[serde(
rename = "themeConfig.dark",
default,
skip_serializing_if = "Option::is_none"
)]
pub theme_config_dark: Option<serde_json::Value>,
#[serde(
rename = "response.hideExtensions",
default,
skip_serializing_if = "Option::is_none"
)]
pub response_hide_extensions: Option<bool>,
#[serde(
rename = "editor.shortcuts",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub editor_shortcuts: HashMap<String, String>,
#[serde(
rename = "beta.disable.newEditor",
default,
skip_serializing_if = "Option::is_none"
)]
pub beta_disable_new_editor: Option<bool>,
#[serde(
rename = "beta.disable.newScript",
default,
skip_serializing_if = "Option::is_none"
)]
pub beta_disable_new_script: Option<bool>,
#[serde(
rename = "script.allowedCookies",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub script_allowed_cookies: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_tablist_scrollbar: Option<bool>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[allow(missing_docs)]
pub enum AltairSettingsLanguage {
#[serde(rename = "en-US")]
English,
#[serde(rename = "fr-FR")]
French,
#[serde(rename = "es-ES")]
EspaƱol,
#[serde(rename = "cs-CZ")]
Czech,
#[serde(rename = "de-DE")]
German,
#[serde(rename = "pt-BR")]
Brazilian,
#[serde(rename = "ru-RU")]
Russian,
#[serde(rename = "uk-UA")]
Ukrainian,
#[serde(rename = "zh-CN")]
ChineseSimplified,
#[serde(rename = "ja-JP")]
Japanese,
#[serde(rename = "sr-SP")]
Serbian,
#[serde(rename = "it-IT")]
Italian,
#[serde(rename = "pl-PL")]
Polish,
#[serde(rename = "ko-KR")]
Korean,
#[serde(rename = "ro-RO")]
Romanian,
#[serde(rename = "vi-VN")]
Vietnamese,
}
struct ToJsonHelper;
impl handlebars::HelperDef for ToJsonHelper {
#[allow(unused_assignments)]
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &handlebars::Helper<'rc>,
r: &'reg handlebars::Handlebars<'reg>,
_: &'rc handlebars::Context,
_: &mut handlebars::RenderContext<'reg, 'rc>,
) -> std::result::Result<handlebars::ScopedJson<'rc>, handlebars::RenderError> {
let mut param_idx = 0;
let obj = h
.param(param_idx)
.and_then(|x| {
if r.strict_mode() && x.is_value_missing() {
None
} else {
Some(x.value())
}
})
.ok_or_else(|| {
handlebars::RenderErrorReason::ParamNotFoundForName("toJson", "obj".to_string())
})
.and_then(|x| {
x.as_object().ok_or_else(|| {
handlebars::RenderErrorReason::ParamTypeMismatchForName(
"toJson",
"obj".to_string(),
"object".to_string(),
)
})
})?;
param_idx += 1;
let result = if obj.is_empty() {
"{}".to_owned()
} else {
serde_json::to_string(&obj).expect("Failed to serialize json")
};
Ok(handlebars::ScopedJson::Derived(
handlebars::JsonValue::from(result),
))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_without_options() {
let altair_source = AltairSource::build().title("Custom Title").finish();
assert_eq!(
altair_source,
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Custom Title</title>
<base href="https://unpkg.com/altair-static@latest/build/dist/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', () => {
AltairGraphQL.init();
});
</script>
<app-root>
<style>
.loading-screen {
/*Prevents the loading screen from showing until CSS is downloaded*/
display: none;
}
</style>
<div class="loading-screen styled">
<div class="loading-screen-inner">
<div class="loading-screen-logo-container">
<img src="assets/img/logo_350.svg" alt="Altair">
</div>
<div class="loading-screen-loading-indicator">
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
</div>
</div>
</div>
</app-root>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>"#
)
}
#[test]
fn test_with_dynamic() {
let altair_source = AltairSource::build()
.options(json!({
"endpointURL": "/",
"subscriptionsEndpoint": "/ws",
}))
.finish();
assert_eq!(
altair_source,
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Altair</title>
<base href="https://unpkg.com/altair-static@latest/build/dist/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', () => {
AltairGraphQL.init({"endpointURL":"/","subscriptionsEndpoint":"/ws"});
});
</script>
<app-root>
<style>
.loading-screen {
/*Prevents the loading screen from showing until CSS is downloaded*/
display: none;
}
</style>
<div class="loading-screen styled">
<div class="loading-screen-inner">
<div class="loading-screen-logo-container">
<img src="assets/img/logo_350.svg" alt="Altair">
</div>
<div class="loading-screen-loading-indicator">
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
</div>
</div>
</div>
</app-root>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>"#
)
}
#[test]
fn test_with_static() {
let altair_source = AltairSource::build()
.options(AltairConfigOptions {
window_options: Some(AltairWindowOptions {
endpoint_url: Some("/".to_owned()),
subscriptions_endpoint: Some("/ws".to_owned()),
..Default::default()
}),
..Default::default()
})
.finish();
assert_eq!(
altair_source,
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Altair</title>
<base href="https://unpkg.com/altair-static@latest/build/dist/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', () => {
AltairGraphQL.init({"endpointURL":"/","subscriptionsEndpoint":"/ws"});
});
</script>
<app-root>
<style>
.loading-screen {
/*Prevents the loading screen from showing until CSS is downloaded*/
display: none;
}
</style>
<div class="loading-screen styled">
<div class="loading-screen-inner">
<div class="loading-screen-logo-container">
<img src="assets/img/logo_350.svg" alt="Altair">
</div>
<div class="loading-screen-loading-indicator">
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
<span class="loading-indicator-dot"></span>
</div>
</div>
</div>
</app-root>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>"#
)
}
}