102 lines
3.4 KiB
Rust
102 lines
3.4 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|