#![cfg(all(feature = "utoipa", feature = "json", feature = "msgpack"))]
#![allow(missing_docs, unreachable_pub)]
#![allow(
clippy::doc_markdown,
clippy::expect_used,
clippy::panic,
clippy::redundant_closure_for_method_calls,
clippy::single_match_else,
clippy::uninlined_format_args,
clippy::unwrap_used
)]
use std::sync::Arc;
use http::HeaderValue;
use mediatype::MediaType;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tower_conneg::{
ErasedFormat, Format, JsonFormat, MsgPackFormat, MsgPackNamedFormat, Negotiate,
NegotiateResponse, OpenApiFormats, ServerConfig,
};
use utoipa::openapi::example::ExampleBuilder;
use utoipa::openapi::extensions::ExtensionsBuilder;
use utoipa::openapi::{ContentBuilder, Ref, RefOr, Schema};
use utoipa::{PartialSchema, ToSchema};
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
fn widget_schema_ref() -> RefOr<Schema> {
RefOr::<Schema>::Ref(Ref::from_schema_name("Widget"))
}
fn formats() -> Vec<Arc<dyn ErasedFormat>> {
vec![Arc::new(JsonFormat), Arc::new(MsgPackNamedFormat)]
}
fn openapi_formats() -> OpenApiFormats {
let formats = formats();
OpenApiFormats::from_formats(formats.iter().map(AsRef::as_ref))
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
struct Widget {
id: u64,
}
#[derive(Debug, Clone, Copy)]
struct VendorJsonFormat;
impl Format for VendorJsonFormat {
fn media_types(&self) -> &'static [MediaType<'static>] {
static TYPES: &[MediaType<'_>] = &[mediatype::media_type!(
APPLICATION / vnd::OPENSTREETMAP_DATA + JSON
)];
TYPES
}
fn content_type_header(&self) -> HeaderValue {
HeaderValue::from_static("application/vnd.openstreetmap.data+json")
}
fn serializer<'a>(
&'a self,
bytes: &'a mut Vec<u8>,
) -> erased_serde::Result<impl tower_conneg::OwnedSerializer + 'a> {
Ok(serde_json::Serializer::new(bytes))
}
fn deserializer<'a>(
&'a self,
bytes: &'a [u8],
) -> erased_serde::Result<impl tower_conneg::OwnedDeserializer<'a> + 'a> {
Ok(tower_conneg::Borrowable(
serde_json::Deserializer::from_slice(bytes),
))
}
}
#[test]
fn request_body_with_content_preserves_metadata() -> TestResult {
let content = ContentBuilder::new()
.schema(Some(widget_schema_ref()))
.example(Some(json!({ "id": 1 })))
.examples_from_iter([(
"named",
ExampleBuilder::new()
.summary("Named example")
.value(Some(json!({ "id": 2 }))),
)])
.extensions(Some(
ExtensionsBuilder::new().add("x-negotiated", true).build(),
))
.build();
let body = openapi_formats()
.request_body_with_content(&content, Some(utoipa::openapi::Required::True));
let rendered = serde_json::to_value(body)?;
assert_eq!(
rendered,
json!({
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Widget" },
"example": { "id": 1 },
"examples": {
"named": {
"summary": "Named example",
"value": { "id": 2 }
}
},
"x-negotiated": true
},
"application/msgpack": {
"schema": { "$ref": "#/components/schemas/Widget" },
"example": { "id": 1 },
"examples": {
"named": {
"summary": "Named example",
"value": { "id": 2 }
}
},
"x-negotiated": true
},
"application/x-msgpack": {
"schema": { "$ref": "#/components/schemas/Widget" },
"example": { "id": 1 },
"examples": {
"named": {
"summary": "Named example",
"value": { "id": 2 }
}
},
"x-negotiated": true
}
},
"required": true
})
);
Ok(())
}
#[test]
fn request_body_uses_typed_schema_ref() -> TestResult {
let body = openapi_formats().request_body::<Widget>();
let rendered = serde_json::to_value(body)?;
assert_eq!(
rendered,
json!({
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn required_request_body_marks_body_required() -> TestResult {
let body =
OpenApiFormats::from_media_types(["application/json"]).required_request_body::<Widget>();
let rendered = serde_json::to_value(body)?;
assert_eq!(
rendered,
json!({
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } }
},
"required": true
})
);
Ok(())
}
#[test]
fn response_uses_typed_schema_ref() -> TestResult {
let response = openapi_formats().response::<Widget, _>("Created widget.");
let rendered = serde_json::to_value(response)?;
assert_eq!(
rendered,
json!({
"description": "Created widget.",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn response_with_content_preserves_metadata() -> TestResult {
let content = ContentBuilder::new()
.schema(Some(widget_schema_ref()))
.example(Some(json!({ "id": 1 })))
.extensions(Some(
ExtensionsBuilder::new().add("x-negotiated", true).build(),
))
.build();
let response = OpenApiFormats::from_media_types(["application/json"])
.response_with_content("Fetched widget.", &content);
let rendered = serde_json::to_value(response)?;
assert_eq!(
rendered,
json!({
"description": "Fetched widget.",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Widget" },
"example": { "id": 1 },
"x-negotiated": true
}
}
})
);
Ok(())
}
#[test]
fn duplicate_format_media_types_are_deterministic() -> TestResult {
let formats: Vec<Arc<dyn ErasedFormat>> =
vec![Arc::new(MsgPackFormat), Arc::new(MsgPackNamedFormat)];
let body =
OpenApiFormats::from_formats(formats.iter().map(AsRef::as_ref)).request_body::<Widget>();
let rendered = serde_json::to_value(body)?;
assert_eq!(
rendered,
json!({
"content": {
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn empty_formats_produce_empty_content() -> TestResult {
let formats = OpenApiFormats::default();
let body = formats.request_body::<Widget>();
let response = formats.response::<Widget, _>("No content.");
assert_eq!(serde_json::to_value(body)?, json!({ "content": {} }));
assert_eq!(
serde_json::to_value(response)?,
json!({ "description": "No content." })
);
Ok(())
}
#[test]
fn negotiation_error_responses_are_documented() -> TestResult {
let formats = openapi_formats();
assert_eq!(
serde_json::to_value(formats.not_acceptable_response())?,
json!({
"description": "The Accept header does not match any supported response format."
})
);
assert_eq!(
serde_json::to_value(formats.unsupported_media_type_response())?,
json!({
"description": "The request Content-Type is not supported."
})
);
assert_eq!(
serde_json::to_value(formats.unsupported_media_type_post_response())?,
json!({
"description": "The request Content-Type is not supported.",
"headers": {
"Accept-Post": {
"schema": { "type": "string" },
"description": "Supported request media types: application/json, application/msgpack, application/x-msgpack."
}
}
})
);
assert_eq!(
serde_json::to_value(formats.unsupported_media_type_patch_response())?,
json!({
"description": "The request Content-Type is not supported.",
"headers": {
"Accept-Patch": {
"schema": { "type": "string" },
"description": "Supported request media types: application/json, application/msgpack, application/x-msgpack."
}
}
})
);
Ok(())
}
#[test]
fn openapi_formats_can_come_from_server_config() -> TestResult {
let json: Arc<dyn ErasedFormat> = Arc::new(JsonFormat);
let msgpack: Arc<dyn ErasedFormat> = Arc::new(MsgPackNamedFormat);
let config = ServerConfig::builder()
.formats([json.clone(), msgpack])
.fallback_format(json)
.build();
let rendered = serde_json::to_value(
config
.openapi_formats()
.response::<Widget, _>("Fetched widget."),
)?;
assert_eq!(
rendered,
json!({
"description": "Fetched widget.",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn openapi_formats_can_be_narrowed() {
let formats = OpenApiFormats::from_media_types([
"application/json",
"application/msgpack",
"application/x-msgpack",
]);
assert_eq!(
formats.only(["application/json"]).media_types(),
["application/json"]
);
assert_eq!(
formats.without(["application/x-msgpack"]).media_types(),
["application/json", "application/msgpack"]
);
}
#[test]
fn error_responses_can_have_negotiated_bodies() -> TestResult {
let formats = openapi_formats();
assert_eq!(
serde_json::to_value(formats.not_acceptable_response_with_body::<Widget>())?,
json!({
"description": "The Accept header does not match any supported response format.",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
assert_eq!(
serde_json::to_value(formats.unsupported_media_type_post_response_with_body::<Widget>())?,
json!({
"description": "The request Content-Type is not supported.",
"headers": {
"Accept-Post": {
"schema": { "type": "string" },
"description": "Supported request media types: application/json, application/msgpack, application/x-msgpack."
}
},
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } },
"application/x-msgpack": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn custom_vendor_media_types_are_documented() -> TestResult {
let formats: Vec<Arc<dyn ErasedFormat>> = vec![Arc::new(VendorJsonFormat)];
let rendered = serde_json::to_value(
OpenApiFormats::from_formats(formats.iter().map(AsRef::as_ref))
.response::<Widget, _>("Fetched widget."),
)?;
assert_eq!(
rendered,
json!({
"description": "Fetched widget.",
"content": {
"application/vnd.openstreetmap.data+json": {
"schema": { "$ref": "#/components/schemas/Widget" }
}
}
})
);
Ok(())
}
#[test]
fn schema_name_escape_hatches_emit_refs() -> TestResult {
let formats = OpenApiFormats::from_media_types(["application/json"]);
assert_eq!(
serde_json::to_value(formats.required_request_body_ref("Widget"))?,
json!({
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } }
},
"required": true
})
);
assert_eq!(
serde_json::to_value(formats.response_ref("Fetched widget.", "Widget"))?,
json!({
"description": "Fetched widget.",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Widget" } }
}
})
);
Ok(())
}
#[test]
fn negotiate_wrappers_forward_utoipa_schema() {
assert_eq!(Negotiate::<Widget>::name(), Widget::name());
assert_eq!(NegotiateResponse::<Widget>::name(), Widget::name());
assert!(Negotiate::<Widget>::schema() == Widget::schema());
assert!(NegotiateResponse::<Widget>::schema() == Widget::schema());
}