hypers_openapi 0.14.1

Compile time generated OpenAPI documentation for hypers
Documentation
mod config;
mod oauth;

pub use config::Config;
use hypers_core::{
    async_trait,
    prelude::{Request, Response},
    fs::redirect_to_dir_url, Handler,
};
pub use oauth::OauthConfig;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::borrow::Cow;

#[derive(RustEmbed)]
#[folder = "src/swagger_ui/v5.17.14"]
struct SwaggerUiDist;

const INDEX_TMPL: &str = r#"
<!DOCTYPE html>
<html charset="UTF-8">
  <head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
    {{keywords}}
    {{description}}
    <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
    <style>
    html {
        box-sizing: border-box;
        overflow: -moz-scrollbars-vertical;
        overflow-y: scroll;
    }
    *,
    *:before,
    *:after {
        box-sizing: inherit;
    }
    body {
        margin: 0;
        background: #fafafa;
    }
    </style>
  </head>

  <body>
    <div id="swagger-ui"></div>
    <script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
    <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
    <script>
    window.onload = function() {
        let config = {
            dom_id: '#swagger-ui',
            deepLinking: true,
            presets: [
              SwaggerUIBundle.presets.apis,
              SwaggerUIStandalonePreset
            ],
            plugins: [
              SwaggerUIBundle.plugins.DownloadUrl
            ],
            layout: "StandaloneLayout"
          };
        window.ui = SwaggerUIBundle(Object.assign(config, {{config}}));
        //{{oauth}}
    };
    </script>
  </body>
</html>
"#;
/// Represents servable file of Swagger UI. This is used together with [`serve`] function
/// to serve Swagger UI files via web server.
#[non_exhaustive]
pub struct SwaggerFile<'a> {
    /// Content of the file as [`Cow`] [`slice`] of bytes.
    pub bytes: Cow<'a, [u8]>,
    /// Content type of the file e.g `"text/xml"`.
    pub content_type: String,
}
/// Implements [`Handler`] for serving Swagger UI.
#[derive(Clone, Debug)]
pub struct SwaggerUi {
    pub config: Config<'static>,
    /// The title of the html page. The default title is "Swagger UI".
    pub title: Cow<'static, str>,
    /// The keywords of the html page.
    pub keywords: Option<Cow<'static, str>>,
    /// The description of the html page.
    pub description: Option<Cow<'static, str>>,
}
impl SwaggerUi {
    /// Create a new [`SwaggerUi`] for given path.
    /// Path argument will expose the Swagger UI to the user and should be something that
    /// the underlying application framework / library supports.
    /// # Examples
    /// ```rust
    /// # use hypers_openapi::swagger_ui::SwaggerUi;
    /// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}");
    /// ```
    pub fn new(config: impl Into<Config<'static>>) -> Self {
        Self {
            config: config.into(),
            title: "Swagger UI".into(),
            keywords: None,
            description: None,
        }
    }
    /// Set title of the html page. The default title is "Swagger UI".
    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
        self.title = title.into();
        self
    }
    /// Set keywords of the html page.
    pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
        self.keywords = Some(keywords.into());
        self
    }
    /// Set description of the html page.
    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
        self.description = Some(description.into());
        self
    }
    /// Add api doc [`Url`] into [`SwaggerUi`].
    /// Calling this again will add another url to the Swagger UI.
    /// # Examples
    /// ```rust
    /// # use hypers_openapi::swagger_ui::SwaggerUi;
    /// # use hypers_openapi::OpenApi;
    /// let swagger = SwaggerUi::new("/api-doc/openapi.json")
    ///     .url("/api-docs/openapi2.json");
    /// ```
    pub fn url<U: Into<Url<'static>>>(mut self, url: U) -> Self {
        self.config.urls.push(url.into());
        self
    }
    /// Add multiple [`Url`]s to Swagger UI.
    /// Takes one [`Vec`] argument containing tuples of [`Url`] and [OpenApi][crate::OpenApi].
    /// Situations where this comes handy is when there is a need or wish to separate different parts
    /// of the api to separate api docs.
    /// # Examples
    /// Expose multiple api docs via Swagger UI.
    /// ```rust
    /// # use hypers_openapi::swagger_ui::{SwaggerUi, Url};
    /// # use hypers_openapi::OpenApi;
    /// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}")
    ///     .urls(
    ///       vec![
    ///          (Url::with_primary("api doc 1", "/api-docs/openapi.json", true)),
    ///          (Url::new("api doc 2", "/api-docs/openapi2.json"))
    ///     ]
    /// );
    /// ```
    pub fn urls(mut self, urls: Vec<Url<'static>>) -> Self {
        self.config.urls = urls;
        self
    }
    /// Add oauth [`oauth::Config`] into [`SwaggerUi`].
    /// Method takes one argument which exposes the [`oauth::Config`] to the user.
    /// # Examples
    /// Enable pkce with default client_id.
    /// ```rust
    /// # use hypers_openapi::swagger_ui::{SwaggerUi, OauthConfig};
    /// # use hypers_openapi::OpenApi;
    /// let swagger = SwaggerUi::new("/swagger-ui/{_:.*}")
    ///     .url("/api-docs/openapi.json")
    ///     .oauth(OauthConfig::new()
    ///         .client_id("client-id")
    ///         .scopes(vec![String::from("openid")])
    ///         .use_pkce_with_authorization_code_grant(true)
    ///     );
    /// ```
    pub fn oauth(mut self, oauth: oauth::OauthConfig) -> Self {
        self.config.oauth = Some(oauth);
        self
    }
    pub fn serve<'a>(
        path: &str,
        title: &str,
        keywords: &str,
        description: &str,
        config: &Config<'a>,
    ) -> Result<Option<SwaggerFile<'a>>, serde_json::Error> {
        let path = if path.is_empty() || path == "/" {
            "index.html"
        } else {
            path
        };
        let bytes = if path == "index.html" {
            let config_json = serde_json::to_string(&config)?;
            // Replace {{config}} with pretty config json and remove the curly brackets `{ }` from beginning and the end.
            let mut index = INDEX_TMPL
                .replacen("{{config}}", &config_json, 1)
                .replacen("{{description}}", description, 1)
                .replacen("{{keywords}}", keywords, 1)
                .replacen("{{title}}", title, 1);
            if let Some(oauth) = &config.oauth {
                let oauth_json = serde_json::to_string(oauth)?;
                index = index.replace(
                    "//{{oauth}}",
                    &format!("window.ui.initOAuth({});", &oauth_json),
                );
            }
            Some(Cow::Owned(index.as_bytes().to_vec()))
        } else {
            SwaggerUiDist::get(path).map(|f| f.data)
        };
        let file = bytes.map(|bytes| SwaggerFile {
            bytes,
            content_type: mime_guess::from_path(path)
                .first_or_octet_stream()
                .to_string(),
        });
        Ok(file)
    }
}
/// Rust type for Swagger UI url configuration object.
#[non_exhaustive]
#[derive(Default, Serialize, Clone, Debug)]
pub struct Url<'a> {
    name: Cow<'a, str>,
    url: Cow<'a, str>,
    #[serde(skip)]
    primary: bool,
}
impl<'a> Url<'a> {
    /// Create new [`Url`].
    /// Name is shown in the select dropdown when there are multiple docs in Swagger UI.
    /// Url is path which exposes the OpenAPI doc.
    /// # Examples
    /// ```rust
    /// # use hypers_openapi::swagger_ui::Url;
    /// let url = Url::new("My Api", "/api-docs/openapi.json");
    /// ```
    pub fn new(name: &'a str, url: &'a str) -> Self {
        Self {
            name: Cow::Borrowed(name),
            url: Cow::Borrowed(url),
            ..Default::default()
        }
    }
    /// Create new [`Url`] with primary flag.
    /// Primary flag allows users to override the default behavior of the Swagger UI for selecting the primary
    /// doc to display. By default when there are multiple docs in Swagger UI the first one in the list
    /// will be the primary.
    /// Name is shown in the select dropdown when there are multiple docs in Swagger UI.
    /// Url is path which exposes the OpenAPI doc.
    /// # Examples
    /// Set "My Api" as primary.
    /// ```rust
    /// # use hypers_openapi::swagger_ui::Url;
    /// let url = Url::with_primary("My Api", "/api-docs/openapi.json", true);
    /// ```
    pub fn with_primary(name: &'a str, url: &'a str, primary: bool) -> Self {
        Self {
            name: Cow::Borrowed(name),
            url: Cow::Borrowed(url),
            primary,
        }
    }
}
impl<'a> From<&'a str> for Url<'a> {
    fn from(url: &'a str) -> Self {
        Self {
            url: Cow::Borrowed(url),
            ..Default::default()
        }
    }
}
impl From<String> for Url<'_> {
    fn from(url: String) -> Self {
        Self {
            url: Cow::Owned(url),
            ..Default::default()
        }
    }
}
impl<'a> From<Cow<'static, str>> for Url<'a> {
    fn from(url: Cow<'static, str>) -> Self {
        Self {
            url,
            ..Default::default()
        }
    }
}
#[async_trait]
impl Handler for SwaggerUi {
    #[inline]
    async fn handle(&self, req: Request) -> Response {
        let path = req.param::<&str>("*1").map(|s| s.trim_start_matches('/'));
        let mut res = Response::default();
        if path.is_err() && !req.uri().path().ends_with('/') {
            return redirect_to_dir_url(req.uri(), res);
        }
        let keywords = self
            .keywords
            .as_ref()
            .map(|s| {
                format!(
                    "<meta name=\"keywords\" content=\"{}\">",
                    s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
                )
            })
            .unwrap_or_default();
        let description = self
            .description
            .as_ref()
            .map(|s| format!("<meta name=\"description\" content=\"{}\">", s))
            .unwrap_or_default();
        match SwaggerUi::serve(
            path.unwrap(),
            &self.title,
            &keywords,
            &description,
            &self.config,
        ) {
            Ok(Some(file)) => {
                res.content_type(&file.content_type)
                    .body(file.bytes.to_vec());
                res
            }
            Ok(None) => {
                res.status(500);
                res
            }
            Err(e) => {
                res.status(500).body(e.to_string());
                res
            }
        }
    }
}
//v0.67.2 => crates/openapi/src/swagger_ui/v5.13.0