use serde_json::{json, Value}; use utoipa_swagger_ui::{Config as SwaggerConfig, SwaggerUi}; const OPENAPI_JSON: &str = include_str!("../../docs/openapi.json"); pub(super) fn swagger_ui(base_path: &str) -> SwaggerUi { let docs_url = format!("{}/api-docs/openapi.json", normalized_base_path(base_path)); SwaggerUi::new("/api-docs") // Keep the route itself relative to the application router so Axum's // outer base-path nesting prefixes it exactly once. Swagger UI may use // the externally visible, base-path-aware URL when fetching the spec. .external_url_unchecked("/api-docs/openapi.json", document(base_path)) .config( SwaggerConfig::new([docs_url]) .doc_expansion("none") .default_models_expand_depth(-1), ) } fn document(base_path: &str) -> Value { let mut document: Value = serde_json::from_str(OPENAPI_JSON) .expect("embedded OpenAPI document must contain valid JSON"); document["info"]["version"] = Value::String(env!("CARGO_PKG_VERSION").to_string()); document["servers"] = json!([{ "url": server_base_path(base_path), "description": "This GREE Controller instance" }]); document } fn normalized_base_path(base_path: &str) -> String { let base = base_path.trim().trim_end_matches('/'); if base.is_empty() || base == "/" { String::new() } else if base.starts_with('/') { base.to_string() } else { format!("/{base}") } } fn server_base_path(base_path: &str) -> String { let base = normalized_base_path(base_path); if base.is_empty() { "/".into() } else { base } } #[cfg(test)] mod tests { use super::*; #[test] fn openapi_document_is_valid_json_and_version_is_runtime_version() { let document = document(""); assert_eq!(document["openapi"], "3.1.0"); assert_eq!(document["info"]["version"], env!("CARGO_PKG_VERSION")); assert_eq!(document["servers"][0]["url"], "/"); } #[test] fn openapi_respects_configured_base_path() { let document = document("/gree/"); assert_eq!(document["servers"][0]["url"], "/gree"); assert_eq!(normalized_base_path("/gree/"), "/gree"); } #[test] fn every_documented_operation_has_summary_description_and_responses() { let document = document(""); let paths = document["paths"].as_object().expect("OpenAPI paths object"); for (path, item) in paths { let methods = item.as_object().expect("OpenAPI path item"); for (method, operation) in methods { if !matches!(method.as_str(), "get" | "post" | "put" | "patch" | "delete") { continue; } assert!( operation.get("summary").and_then(Value::as_str).is_some(), "{method} {path} is missing summary" ); assert!( operation .get("description") .and_then(Value::as_str) .is_some(), "{method} {path} is missing description" ); assert!( operation .get("responses") .and_then(Value::as_object) .is_some(), "{method} {path} is missing responses" ); } } } }