use utoipa::openapi::OpenApi;
pub fn merge_health_paths(doc: &mut OpenApi, health_path: &str) {
use utoipa::openapi::path::{HttpMethod, OperationBuilder, PathItem};
use utoipa::openapi::response::ResponseBuilder;
use utoipa::openapi::tag::Tag;
let liveness_op = OperationBuilder::new()
.operation_id(Some("liveness"))
.tag("health")
.summary(Some("Liveness probe"))
.description(Some(
"Returns `200 OK` when the process is alive. \
Always succeeds as long as the server is running.",
))
.response(
"200",
ResponseBuilder::new()
.description("Service is alive")
.build(),
)
.build();
let readiness_op = OperationBuilder::new()
.operation_id(Some("readiness"))
.tag("health")
.summary(Some("Readiness probe"))
.description(Some(
"Returns `200 OK` when all registered readiness checks pass, \
`503 Service Unavailable` when any check fails.",
))
.response(
"200",
ResponseBuilder::new()
.description("Service is ready")
.build(),
)
.response(
"503",
ResponseBuilder::new()
.description("Service is not ready — one or more checks failed")
.build(),
)
.build();
let live_path = format!("{health_path}/live");
let ready_path = format!("{health_path}/ready");
doc.paths
.paths
.entry(live_path)
.and_modify(|item| {
item.merge_operations(PathItem::new(HttpMethod::Get, liveness_op.clone()))
})
.or_insert_with(|| PathItem::new(HttpMethod::Get, liveness_op));
doc.paths
.paths
.entry(ready_path)
.and_modify(|item| {
item.merge_operations(PathItem::new(HttpMethod::Get, readiness_op.clone()))
})
.or_insert_with(|| PathItem::new(HttpMethod::Get, readiness_op));
if !doc
.tags
.as_deref()
.unwrap_or(&[])
.iter()
.any(|t| t.name == "health")
{
let tags = doc.tags.get_or_insert_with(Vec::new);
tags.push(Tag::new("health"));
}
}
pub fn rewrite_nullable_for_progenitor(value: &mut serde_json::Value) {
rewrite_nullable_recursive(value);
}
pub fn to_3_0_pretty_json(doc: &OpenApi) -> serde_json::Result<String> {
let json = serde_json::to_string_pretty(doc)?;
let mut val: serde_json::Value = serde_json::from_str(&json)?;
coerce_boolean_and_2020_schemas(&mut val);
rewrite_nullable_recursive(&mut val);
if let Some(obj) = val.as_object_mut() {
obj.insert(
"openapi".to_string(),
serde_json::Value::String("3.0.3".to_string()),
);
if let Some(info) = obj.get_mut("info")
&& let Some(license) = info.as_object_mut().and_then(|o| o.get_mut("license"))
&& let Some(lic_obj) = license.as_object_mut()
{
lic_obj.remove("identifier");
}
}
serde_json::to_string_pretty(&val)
}
pub struct BearerAuthAddon;
impl utoipa::Modify for BearerAuthAddon {
fn modify(&self, openapi: &mut OpenApi) {
use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme};
let components = openapi.components.get_or_insert_with(Default::default);
components.add_security_scheme(
"BearerAuth",
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
);
}
}
fn rewrite_nullable_recursive(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
let rewrite_type = if let Some(type_val) = map.get("type") {
if let Some(arr) = type_val.as_array() {
let non_null: Vec<&str> = arr
.iter()
.filter_map(|v| v.as_str())
.filter(|s| *s != "null")
.collect();
let has_null = arr.iter().any(|v| v.as_str() == Some("null"));
if non_null.len() == 1 {
Some((non_null[0].to_string(), has_null))
} else {
None
}
} else {
None
}
} else {
None
};
if let Some((t, has_null)) = rewrite_type {
map.insert("type".to_string(), serde_json::Value::String(t));
if has_null {
map.insert("nullable".to_string(), serde_json::Value::Bool(true));
}
}
for of_key in &["oneOf", "anyOf"] {
let rewrite = if let Some(variants) = map.get(*of_key) {
if let Some(arr) = variants.as_array() {
let has_null_arm = arr.iter().any(|v| {
v.as_object()
.and_then(|o| o.get("type"))
.and_then(|t| t.as_str())
== Some("null")
});
if has_null_arm {
let remaining: Vec<serde_json::Value> = arr
.iter()
.filter(|v| {
v.as_object()
.and_then(|o| o.get("type"))
.and_then(|t| t.as_str())
!= Some("null")
})
.cloned()
.collect();
Some(remaining)
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(remaining) = rewrite {
map.insert("nullable".to_string(), serde_json::Value::Bool(true));
if remaining.len() == 1 {
let arm = remaining.into_iter().next().unwrap();
map.remove(*of_key);
if let serde_json::Value::Object(arm_map) = arm {
for (k, v) in arm_map {
map.entry(k).or_insert(v);
}
}
} else {
map.insert(of_key.to_string(), serde_json::Value::Array(remaining));
}
break;
}
}
map.remove("propertyNames");
if let Some(examples) = map.remove("examples")
&& let Some(arr) = examples.as_array()
&& let Some(first) = arr.first()
{
map.entry("example".to_string())
.or_insert_with(|| first.clone());
}
for v in map.values_mut() {
rewrite_nullable_recursive(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr.iter_mut() {
rewrite_nullable_recursive(v);
}
}
_ => {}
}
}
fn coerce_boolean_and_2020_schemas(val: &mut serde_json::Value) {
match val {
serde_json::Value::Object(map) => {
let schema_keys = [
"additionalProperties",
"additionalItems",
"items",
"not",
"if",
"then",
"else",
"contains",
"propertyNames",
];
for key in &schema_keys {
if let Some(&serde_json::Value::Bool(b)) = map.get(*key) {
if b {
map.insert(
key.to_string(),
serde_json::Value::Object(Default::default()),
);
} else {
map.remove(*key);
}
}
}
map.remove("unevaluatedProperties");
map.remove("unevaluatedItems");
map.remove("$schema");
for v in map.values_mut() {
coerce_boolean_and_2020_schemas(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr.iter_mut() {
coerce_boolean_and_2020_schemas(v);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use utoipa::openapi::OpenApiBuilder;
fn empty_api() -> OpenApi {
OpenApiBuilder::new()
.info(
utoipa::openapi::InfoBuilder::new()
.title("test")
.version("0.1.0")
.build(),
)
.build()
}
#[test]
fn to_3_0_pretty_json_version_field() {
let api = empty_api();
let result = to_3_0_pretty_json(&api).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["openapi"], "3.0.3");
}
#[test]
fn rewrite_nullable_for_progenitor_round_trip() {
let mut val = json!({ "type": ["string", "null"] });
rewrite_nullable_for_progenitor(&mut val);
assert_eq!(val["type"], "string");
assert_eq!(val["nullable"], true);
}
#[test]
fn merge_health_paths_idempotency() {
let mut api = empty_api();
merge_health_paths(&mut api, "/health");
merge_health_paths(&mut api, "/health");
let count = api
.tags
.as_deref()
.unwrap_or(&[])
.iter()
.filter(|t| t.name == "health")
.count();
assert_eq!(count, 1);
assert!(api.paths.paths.contains_key("/health/live"));
assert!(api.paths.paths.contains_key("/health/ready"));
}
#[test]
fn bearer_auth_addon_modify_presence() {
use utoipa::Modify as _;
let mut api = empty_api();
BearerAuthAddon.modify(&mut api);
let components = api.components.unwrap();
assert!(components.security_schemes.contains_key("BearerAuth"));
}
}