use crate::handlers::{ContentHandler, RedirectHandler};
use crate::settings::UrlObject;
use rocket::http::ContentType;
use rocket::Route;
use std::collections::HashMap;
macro_rules! static_file {
($name: literal, $type: ident) => {
ContentHandler::bytes(
ContentType::$type,
include_bytes!(concat!("../rapidoc/", $name)),
)
.into_route(concat!("/", $name))
};
}
#[macro_export]
macro_rules! hash_map {
($($key:expr => $val:expr),* $(,)*) => ({
#[allow(unused_mut)]
let mut map = ::std::collections::HashMap::new();
$( map.insert($key, $val); )*
map
});
}
#[derive(Debug, Clone, Default)]
pub struct RapiDocConfig {
pub title: Option<String>,
pub general: GeneralConfig,
pub ui: UiConfig,
pub nav: NavConfig,
pub layout: LayoutConfig,
pub hide_show: HideShowConfig,
pub schema: SchemaConfig,
pub api: ApiConfig,
pub slots: SlotsConfig,
pub custom_html: Option<String>,
pub custom_template_tags: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct GeneralConfig {
pub spec_urls: Vec<UrlObject>,
pub update_route: bool,
pub route_prefix: String,
pub sort_tags: bool,
pub sort_endpoints_by: SortEndpointsBy,
pub heading_text: String,
pub goto_path: String,
pub fill_request_fields_with_example: bool,
pub persist_auth: bool,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
spec_urls: vec![],
update_route: true,
route_prefix: "#".to_owned(),
sort_tags: false,
sort_endpoints_by: SortEndpointsBy::Path,
heading_text: "".to_owned(),
goto_path: "".to_owned(),
fill_request_fields_with_example: true,
persist_auth: false,
}
}
}
#[derive(Debug, Clone)]
pub struct UiConfig {
pub theme: Theme,
pub bg_color: String,
pub text_color: String,
pub header_color: String,
pub primary_color: String,
pub load_fonts: bool,
pub regular_font: String,
pub mono_font: String,
pub font_size: FontSize,
pub css_file: Option<String>,
pub css_classes: Vec<String>,
}
impl Default for UiConfig {
fn default() -> Self {
Self {
theme: Theme::Light,
bg_color: "".to_owned(),
text_color: "".to_owned(),
header_color: "".to_owned(),
primary_color: "".to_owned(),
load_fonts: true,
regular_font: "".to_owned(),
mono_font: "".to_owned(),
font_size: FontSize::Default,
css_file: None,
css_classes: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct NavConfig {
pub show_method_in_nav_bar: ShowMethodInNavBar,
pub use_path_in_nav_bar: bool,
pub nav_bg_color: String,
pub nav_text_color: String,
pub nav_hover_bg_color: String,
pub nav_hover_text_color: String,
pub nav_accent_color: String,
pub nav_accent_text_color: String,
pub nav_active_item_marker: NavActiveItemMarker,
pub nav_item_spacing: NavItemSpacing,
pub on_nav_tag_click: NavTagClick,
}
impl Default for NavConfig {
fn default() -> Self {
Self {
show_method_in_nav_bar: ShowMethodInNavBar::None,
use_path_in_nav_bar: false,
nav_bg_color: "".to_owned(),
nav_text_color: "".to_owned(),
nav_hover_bg_color: "".to_owned(),
nav_hover_text_color: "".to_owned(),
nav_accent_color: "".to_owned(),
nav_accent_text_color: "".to_owned(),
nav_active_item_marker: NavActiveItemMarker::LeftBar,
nav_item_spacing: NavItemSpacing::Default,
on_nav_tag_click: NavTagClick::ExpandCollapse,
}
}
}
#[derive(Debug, Clone)]
pub struct LayoutConfig {
pub layout: Layout,
pub render_style: RenderStyle,
pub response_area_height: String,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self {
layout: Layout::Row,
render_style: RenderStyle::Read,
response_area_height: "300px".to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct HideShowConfig {
pub show_info: bool,
pub info_description_headings_in_navbar: bool,
pub show_components: bool,
pub show_header: bool,
pub allow_authentication: bool,
pub allow_spec_url_load: bool,
pub allow_spec_file_load: bool,
pub allow_spec_file_download: bool,
pub allow_search: bool,
pub allow_advanced_search: bool,
pub allow_try: bool,
pub show_curl_before_try: bool,
pub allow_server_selection: bool,
pub allow_schema_description_expand_toggle: bool,
}
impl Default for HideShowConfig {
fn default() -> Self {
Self {
show_info: true,
info_description_headings_in_navbar: false,
show_components: false,
show_header: true,
allow_authentication: true,
allow_spec_url_load: true,
allow_spec_file_load: true,
allow_spec_file_download: false,
allow_search: true,
allow_advanced_search: true,
allow_try: true,
show_curl_before_try: false,
allow_server_selection: true,
allow_schema_description_expand_toggle: true,
}
}
}
#[derive(Debug, Clone)]
pub struct SchemaConfig {
pub schema_style: SchemaStyle,
pub schema_expand_level: u16,
pub schema_description_expanded: bool,
pub schema_hide_read_only: SchemaHideReadOnly,
pub schema_hide_write_only: SchemaHideWriteOnly,
pub default_schema_tab: DefaultSchemaTab,
}
impl Default for SchemaConfig {
fn default() -> Self {
Self {
schema_style: SchemaStyle::Tree,
schema_expand_level: 999,
schema_description_expanded: false,
schema_hide_read_only: SchemaHideReadOnly::Always,
schema_hide_write_only: SchemaHideWriteOnly::Always,
default_schema_tab: DefaultSchemaTab::Model,
}
}
}
#[derive(Debug, Clone)]
pub struct ApiConfig {
pub server_url: String,
pub default_api_server: String,
pub api_key_name: String,
pub api_key_location: Option<ApiKeyLocation>,
pub api_key_value: String,
pub fetch_credentials: Option<FetchCredentials>,
}
impl Default for ApiConfig {
fn default() -> Self {
Self {
server_url: "".to_owned(),
default_api_server: "".to_owned(),
api_key_name: "".to_owned(),
api_key_location: None,
api_key_value: "".to_owned(),
fetch_credentials: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SlotsConfig {
pub default: Vec<String>,
pub logo: Option<String>,
pub header: Option<String>,
pub footer: Option<String>,
pub nav_logo: Option<String>,
pub overview: Option<String>,
pub servers: Option<String>,
pub auth: Option<String>,
pub operations_top: Option<String>,
pub tags: HashMap<String, String>,
pub endpoints: HashMap<String, String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SortEndpointsBy {
Path,
Method,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Theme {
Light,
Dark,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FontSize {
Default,
Large,
Largest,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ShowMethodInNavBar {
None,
AsPlainText,
AsColoredText,
AsColoredBlock,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NavActiveItemMarker {
LeftBar,
ColoredBlock,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NavItemSpacing {
Default,
Compact,
Relaxed,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Layout {
Row,
Column,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RenderStyle {
View,
Read,
Focused,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NavTagClick {
ExpandCollapse,
ShowDescription,
}
impl std::fmt::Display for NavTagClick {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use NavTagClick::*;
write!(
fmt,
"{}",
match self {
ExpandCollapse => "expand-collapse",
ShowDescription => "show-description",
}
)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SchemaStyle {
Tree,
Table,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SchemaHideReadOnly {
Always,
Never,
Post,
Put,
Patch,
PostPut,
PostPatch,
PutPatch,
PostPutPatch,
}
impl std::fmt::Display for SchemaHideReadOnly {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use SchemaHideReadOnly::*;
write!(
fmt,
"{}",
match self {
Always => "always",
Never => "never",
Post => "post",
Put => "put",
Patch => "patch",
PostPut => "post put",
PostPatch => "post patch",
PutPatch => "put patch",
PostPutPatch => "post put patch",
}
)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SchemaHideWriteOnly {
Always,
Never,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DefaultSchemaTab {
Model,
Example,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ApiKeyLocation {
Header,
Query,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FetchCredentials {
Omit,
SameOrigin,
Include,
}
impl std::fmt::Display for FetchCredentials {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use FetchCredentials::*;
write!(
fmt,
"{}",
match self {
Omit => "omit",
SameOrigin => "same-origin",
Include => "include",
}
)
}
}
macro_rules! impl_display {
($to_impl:ident) => {
impl std::fmt::Display for $to_impl {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
let dbg_repr = format!("{:?}", self);
write!(fmt, "{}", dbg_repr.to_lowercase())
}
}
};
}
impl_display!(SortEndpointsBy);
impl_display!(Theme);
impl_display!(FontSize);
impl_display!(ShowMethodInNavBar);
impl_display!(NavActiveItemMarker);
impl_display!(NavItemSpacing);
impl_display!(Layout);
impl_display!(RenderStyle);
impl_display!(SchemaStyle);
impl_display!(SchemaHideWriteOnly);
impl_display!(DefaultSchemaTab);
impl_display!(ApiKeyLocation);
fn slot_list(slots: &[String]) -> String {
let mut result = "".to_owned();
for html in slots {
result = format!(r#"{}<slot>{}</slot>"#, result, html);
}
result
}
fn slot_opt(slot: &Option<String>, name: &str) -> String {
match slot {
Some(html) => format!(r#"<slot name="{}">{}</slot>"#, name, html),
None => "".to_owned(),
}
}
fn slot_logo(slot: &Option<String>) -> String {
match slot {
Some(html) => format!(
r#"<img slot="logo" src="{}" alt="logo" style="max-width: 150px; max-height: 50px"/>"#,
html
),
None => "".to_owned(),
}
}
fn slot_tags(slots: &HashMap<String, String>) -> String {
let mut result = "".to_owned();
for (key, html) in slots {
result = format!(r#"{}<slot name="tag--{}">{}</slot>"#, result, key, html);
}
result
}
fn slot_endpoints(slots: &HashMap<String, String>) -> String {
let mut result = "".to_owned();
for (key, html) in slots {
if key.contains('{') || key.contains('}') || key.contains('#') || key.contains(' ') {
if cfg!(debug_assertions) {
panic!(
"Slot endpoint `{}` contains invalid characters `{{`, `}}`, `#` or ` ` (space).",
key
);
} else {
eprintln!(
"Slot endpoint `{}` contains invalid characters `{{`, `}}`, `#` or ` ` (space).",
key
);
}
}
result = format!(r#"{}<slot name="{}">{}</slot>"#, result, key, html);
}
result
}
pub fn make_rapidoc(config: &RapiDocConfig) -> impl Into<Vec<Route>> {
let title = match &config.title {
Some(title) => title.clone(),
None => "API Documentation | RapiDoc".to_owned(),
};
let template_map = hash_map! {
"TITLE" => title,
"SPEC_URL" => config.general.spec_urls[0].url.clone(),
"SPEC_URLS" => serde_json::to_string(&config.general.spec_urls).unwrap_or_default(),
"UPDATE_ROUTE" => config.general.update_route.to_string(),
"ROUTE_PREFIX" => config.general.route_prefix.clone(),
"SORT_TAGS" => config.general.sort_tags.to_string(),
"SORT_ENDPOINTS_BY" => config.general.sort_endpoints_by.to_string(),
"HEADING_TEXT" => config.general.heading_text.clone(),
"GOTO_PATH" => config.general.goto_path.clone(),
"REQUEST_EXAMPLE_FIELDS" => config.general.fill_request_fields_with_example.to_string(),
"PERSIST_AUTH" => config.general.persist_auth.to_string(),
"THEME" => config.ui.theme.to_string(),
"BG_COLOR" => config.ui.bg_color.clone(),
"TEXT_COLOR" => config.ui.text_color.clone(),
"HEADER_COLOR" => config.ui.header_color.clone(),
"PRIMARY_COLOR" => config.ui.primary_color.clone(),
"LOAD_FONTS" => config.ui.load_fonts.to_string(),
"REGULAR_FONT" => config.ui.regular_font.clone(),
"MONO_FONT" => config.ui.mono_font.clone(),
"FONT_SIZE" => config.ui.font_size.to_string(),
"CSS_FILE" => config.ui.css_file.clone().unwrap_or_default(),
"CSS_CLASSES" => config.ui.css_classes.join(" ").to_string(),
"SHOW_METHOD_IN_NAV_BAR" => config.nav.show_method_in_nav_bar.to_string(),
"USE_PATH_IN_NAV_BAR" => config.nav.use_path_in_nav_bar.to_string(),
"NAV_BG_COLOR" => config.nav.nav_bg_color.clone(),
"NAV_TEXT_COLOR" => config.nav.nav_text_color.clone(),
"NAV_HOVER_BG_COLOR" => config.nav.nav_hover_bg_color.clone(),
"NAV_HOVER_TEXT_COLOR" => config.nav.nav_hover_text_color.clone(),
"NAV_ACCENT_COLOR" => config.nav.nav_accent_color.clone(),
"NAV_ACCENT_TEXT_COLOR" => config.nav.nav_accent_text_color.clone(),
"NAV_ACCENT_ITEM_MARKER" => config.nav.nav_active_item_marker.to_string(),
"NAV_ITEM_SPACING" => config.nav.nav_item_spacing.to_string(),
"ON_NAV_TAG_CLICK" => config.nav.on_nav_tag_click.to_string(),
"LAYOUT" => config.layout.layout.to_string(),
"RENDER_STYLE" => config.layout.render_style.to_string(),
"RESPONSE_AREA_HEIGHT" => config.layout.response_area_height.clone(),
"SHOW_INFO" => config.hide_show.show_info.to_string(),
"INFO_DESCRIPTIONS_IN_NAVBAR" => config.hide_show.info_description_headings_in_navbar.to_string(),
"SHOW_COMPONENTS" => config.hide_show.show_components.to_string(),
"SHOW_HEADER" => config.hide_show.show_header.to_string(),
"ALLOW_AUTHENTICATION" => config.hide_show.allow_authentication.to_string(),
"ALLOW_SPEC_URL_LOAD" => config.hide_show.allow_spec_url_load.to_string(),
"ALLOW_SPEC_FILE_LOAD" => config.hide_show.allow_spec_file_load.to_string(),
"ALLOW_SPEC_FILE_DOWNLOAD" => config.hide_show.allow_spec_file_download.to_string(),
"ALLOW_SEARCH" => config.hide_show.allow_search.to_string(),
"ALLOW_ADVANCED_SEARCH" => config.hide_show.allow_advanced_search.to_string(),
"ALLOW_TRY" => config.hide_show.allow_try.to_string(),
"SHOW_CURL_BEFORE_TRY" => config.hide_show.show_curl_before_try.to_string(),
"ALLOW_SERVER_SELECTION" => config.hide_show.allow_server_selection.to_string(),
"ALLOW_SCHEMA_DESC_EXPAND_TOGGLE" => config.hide_show.allow_schema_description_expand_toggle.to_string(),
"SCHEMA_STYLE" => config.schema.schema_style.to_string(),
"SCHEMA_EXPAND_LEVEL" => config.schema.schema_expand_level.to_string(),
"SCHEMA_DESCRIPTION_EXPANDED" => config.schema.schema_description_expanded.to_string(),
"SCHEMA_HIDE_READ_ONLY" => config.schema.schema_hide_read_only.to_string(),
"SCHEMA_HIDE_WRITE_ONLY" => config.schema.schema_hide_write_only.to_string(),
"DEFAULT_SCHEMA_TAB" => config.schema.default_schema_tab.to_string(),
"SERVER_URL" => config.api.server_url.clone(),
"DEFAULT_API_SERVER" => config.api.default_api_server.clone(),
"API_KEY_NAME" => config.api.api_key_name.clone(),
"API_KEY_LOCATION" => config.api.api_key_location.as_ref().map_or_else(|| "".to_owned(), |v| v.to_string()),
"API_KEY_VALUE" => config.api.api_key_value.clone(),
"FETCH_CREDENTIALS" => config.api.fetch_credentials.as_ref().map_or_else(|| "".to_owned(), |v| v.to_string()),
"DEFAULT" => slot_list(&config.slots.default),
"LOGO" => slot_logo(&config.slots.logo),
"HEADER" => slot_opt(&config.slots.header, "header"),
"FOOTER" => slot_opt(&config.slots.footer, "footer"),
"NAV_LOGO" => slot_opt(&config.slots.nav_logo, "nav-logo"),
"OVERVIEW" => slot_opt(&config.slots.overview, "overview"),
"SERVERS" => slot_opt(&config.slots.servers, "servers"),
"AUTH" => slot_opt(&config.slots.auth, "auth"),
"OPERATIONS_TOP" => slot_opt(&config.slots.operations_top, "operations-top"),
"TAGS" => slot_tags(&config.slots.tags),
"ENDPOINTS" => slot_endpoints(&config.slots.tags),
};
let mut index_page = match &config.custom_html {
Some(custom_file) => custom_file.clone(),
None => include_str!("../rapidoc/index.html").to_owned(),
};
for (key, value) in &config.custom_template_tags {
index_page = index_page.replace(&format!("{{{{{}}}}}", key), value);
}
for (key, value) in template_map {
index_page = index_page.replace(&format!("{{{{{}}}}}", key), &value);
}
vec![
RedirectHandler::to("index.html").into_route("/"),
ContentHandler::bytes_owned(ContentType::HTML, index_page.as_bytes().to_vec())
.into_route("/index.html"),
static_file!("rapidoc-min.js", JavaScript),
static_file!("oauth-receiver.html", HTML),
]
}