vespera_macro 0.4.0

Procedural macros for Vespera - automatic route discovery and OpenAPI generation
Documentation
use quote::quote;
use vespera_core::route::HttpMethod;

use crate::method::http_method_to_token_stream;

/// Swagger UI HTML template. Contains `{}` format placeholder for the OpenAPI spec JSON.
pub(super) const SWAGGER_UI_HTML: &str = r##"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Swagger UI</title><link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" /></head><body style="margin: 0; padding: 0;"><div id="swagger-ui"></div><script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script><script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script><script>const openapiSpec = {};window.onload = () => {{ SwaggerUIBundle({{ spec: openapiSpec, dom_id: "#swagger-ui", presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], layout: "StandaloneLayout" }}); }};</script></body></html>"##;

/// ReDoc HTML template. Contains `{}` format placeholder for the OpenAPI spec JSON.
pub(super) const REDOC_HTML: &str = r#"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>ReDoc</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body {{ margin: 0; padding: 0; }}</style><link rel="stylesheet" href="https://unpkg.com/redoc/bundles/redoc.standalone.css" /></head><body><div id="redoc-container"></div><script src="https://unpkg.com/redoc/bundles/redoc.standalone.js"></script><script>const openapiSpec = {};Redoc.init(openapiSpec, {{}}, document.getElementById("redoc-container"));</script></body></html>"#;

/// Generate a documentation route handler (Swagger UI or ReDoc).
///
/// When `has_merge` is true, the handler merges specs from child apps at runtime.
/// When false, it serves the spec directly from the compile-time constant.
pub(super) fn generate_docs_route_tokens(
    url: &str,
    html_template: &str,
    spec_expr: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
    let method_path = http_method_to_token_stream(HttpMethod::Get);

    quote!(
        .route(#url, #method_path(|| async {
            static HTML: std::sync::OnceLock<String> = std::sync::OnceLock::new();
            let html = HTML.get_or_init(|| {
                format!(#html_template, #spec_expr)
            });
            vespera::axum::response::Html(html.as_str())
        }))
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_swagger_html_template_renders_valid_quotes() {
        assert!(
            !SWAGGER_UI_HTML.contains(r#"\""#),
            "Swagger template should not contain literal backslash-quotes: {SWAGGER_UI_HTML}"
        );
        assert!(
            SWAGGER_UI_HTML.contains(r#"href="https://unpkg.com/swagger-ui-dist/swagger-ui.css""#)
        );
        assert!(
            SWAGGER_UI_HTML
                .contains(r#"src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js""#)
        );
        assert!(SWAGGER_UI_HTML.contains(r##"dom_id: "#swagger-ui""##));
    }

    #[test]
    fn test_redoc_html_template_renders_valid_quotes() {
        assert!(
            !REDOC_HTML.contains(r#"\""#),
            "ReDoc template should not contain literal backslash-quotes: {REDOC_HTML}"
        );
        assert!(
            REDOC_HTML.contains(r#"href="https://unpkg.com/redoc/bundles/redoc.standalone.css""#)
        );
        assert!(
            REDOC_HTML.contains(r#"src="https://unpkg.com/redoc/bundles/redoc.standalone.js""#)
        );
        assert!(REDOC_HTML.contains(r#"document.getElementById("redoc-container")"#));
    }
}