v0.11.6
This commit is contained in:
@@ -28,6 +28,8 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
mod openapi;
|
||||
|
||||
const INDEX_HTML: &str = include_str!("../web/index.html");
|
||||
const NOT_FOUND_HTML: &str = include_str!("../web/404.html");
|
||||
const APP_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/app.bundle.js"));
|
||||
@@ -139,6 +141,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/lang/:file", get(language_file))
|
||||
.route("/presets/index.json", get(preset_index))
|
||||
.route("/presets/:file", get(preset_file))
|
||||
.merge(openapi::swagger_ui(&state.config.base_path))
|
||||
.merge(protected)
|
||||
.merge(home_assistant_api);
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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])
|
||||
.filter(true)
|
||||
.try_it_out_enabled(true)
|
||||
.display_request_duration(true)
|
||||
.persist_authorization(true),
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user