use crate::{OpenApiError, Result};
use serde_json::Value;
use utoipa::openapi::{InfoBuilder, OpenApi, OpenApiBuilder, PathItem, PathsBuilder};
#[derive(Clone)]
pub struct OpenApiDoc {
openapi: OpenApi,
}
impl OpenApiDoc {
pub fn new(title: &str, version: &str) -> Self {
let openapi = OpenApiBuilder::new()
.info(InfoBuilder::new().title(title).version(version).build())
.build();
Self { openapi }
}
pub fn from_openapi(openapi: OpenApi) -> Self {
Self { openapi }
}
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
if let Some(ref mut info) = self.openapi.info.description {
*info = description.into();
} else {
self.openapi.info.description = Some(description.into());
}
self
}
pub fn add_server(mut self, url: &str, description: Option<&str>) -> Self {
use utoipa::openapi::ServerBuilder;
let mut server_builder = ServerBuilder::new().url(url);
if let Some(desc) = description {
server_builder = server_builder.description(Some(desc));
}
let server = server_builder.build();
if self.openapi.servers.is_none() {
self.openapi.servers = Some(Vec::new());
}
if let Some(ref mut servers) = self.openapi.servers {
servers.push(server);
}
self
}
pub fn add_path(mut self, path: &str, path_item: PathItem) -> Self {
let mut paths_builder = PathsBuilder::new();
paths_builder = paths_builder.path(path, path_item);
self.openapi.paths = paths_builder.build();
self
}
pub fn add_paths(mut self, paths: Vec<(String, PathItem)>) -> Self {
let mut paths_builder = PathsBuilder::new();
for (path, path_item) in paths {
paths_builder = paths_builder.path(&path, path_item);
}
self.openapi.paths = paths_builder.build();
self
}
pub fn add_placeholder_schemas(mut self, type_names: &[&str]) -> Self {
use utoipa::openapi::ComponentsBuilder;
use utoipa::openapi::schema::{ObjectBuilder, Schema};
let mut components = self
.openapi
.components
.unwrap_or_else(|| ComponentsBuilder::new().build());
for name in type_names {
components
.schemas
.entry((*name).to_string())
.or_insert_with(|| {
utoipa::openapi::RefOr::T(Schema::Object(ObjectBuilder::new().build()))
});
}
self.openapi.components = Some(components);
self
}
pub fn apply_registered_schemas(mut self) -> Self {
crate::doc::apply_registered_schemas(&mut self.openapi);
self
}
pub fn add_bearer_auth(mut self, scheme_name: &str, description: Option<&str>) -> Self {
use utoipa::openapi::ComponentsBuilder;
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
let http = HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT");
if let Some(_desc) = description {
}
let scheme = SecurityScheme::Http(http.build());
let mut components = self
.openapi
.components
.unwrap_or_else(|| ComponentsBuilder::new().build());
components
.security_schemes
.insert(scheme_name.to_string(), scheme);
self.openapi.components = Some(components);
self
}
pub fn set_global_security(mut self, scheme_name: &str, scopes: &[&str]) -> Self {
use utoipa::openapi::security::SecurityRequirement;
let scopes_vec: Vec<String> = scopes.iter().map(|s| s.to_string()).collect();
let requirement = SecurityRequirement::new(scheme_name.to_string(), scopes_vec);
match self.openapi.security {
Some(ref mut list) => list.push(requirement),
None => self.openapi.security = Some(vec![requirement]),
}
self
}
pub fn openapi(&self) -> &OpenApi {
&self.openapi
}
pub fn into_openapi(self) -> OpenApi {
self.openapi
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(&self.openapi).map_err(OpenApiError::Json)
}
pub fn to_pretty_json(&self) -> Result<String> {
serde_json::to_string_pretty(&self.openapi).map_err(OpenApiError::Json)
}
pub fn to_json_value(&self) -> Result<Value> {
serde_json::to_value(&self.openapi).map_err(OpenApiError::Json)
}
}
#[derive(Debug, Clone)]
pub struct PathInfo {
pub method: http::Method,
pub path: String,
pub operation_id: Option<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub tags: Vec<String>,
}
impl PathInfo {
pub fn new(method: http::Method, path: &str) -> Self {
Self {
method,
path: path.to_string(),
operation_id: None,
summary: None,
description: None,
tags: Vec::new(),
}
}
pub fn operation_id<S: Into<String>>(mut self, id: S) -> Self {
self.operation_id = Some(id.into());
self
}
pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
self.summary = Some(summary.into());
self
}
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
self.tags.push(tag.into());
self
}
pub fn tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = tags.into_iter().map(|s| s.into()).collect();
self
}
}
pub fn create_success_response(description: &str) -> utoipa::openapi::Response {
use utoipa::openapi::ResponseBuilder;
ResponseBuilder::new().description(description).build()
}
pub fn create_json_response(
description: &str,
_schema_ref: Option<&str>,
) -> utoipa::openapi::Response {
use utoipa::openapi::ResponseBuilder;
ResponseBuilder::new().description(description).build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openapi_doc_creation() {
let doc = OpenApiDoc::new("Test API", "1.0.0")
.description("A test API")
.add_server("http://localhost:8080", Some("Development server"));
let openapi = doc.openapi();
assert_eq!(openapi.info.title, "Test API");
assert_eq!(openapi.info.version, "1.0.0");
assert_eq!(openapi.info.description, Some("A test API".to_string()));
assert!(openapi.servers.is_some());
}
#[test]
fn test_path_info() {
let path_info = PathInfo::new(http::Method::GET, "/users/{id}")
.operation_id("get_user")
.summary("Get user by ID")
.description("Retrieve a user by their unique identifier")
.tag("users");
assert_eq!(path_info.method, http::Method::GET);
assert_eq!(path_info.path, "/users/{id}");
assert_eq!(path_info.operation_id, Some("get_user".to_string()));
assert_eq!(path_info.tags, vec!["users"]);
}
#[test]
fn test_json_serialization() {
let doc = OpenApiDoc::new("Test API", "1.0.0");
let json = doc.to_json().unwrap();
assert!(json.contains("Test API"));
assert!(json.contains("1.0.0"));
}
#[test]
fn test_add_server_and_security() {
let doc = OpenApiDoc::new("T", "1")
.add_server("https://api.example.com", Some("prod"))
.add_bearer_auth("bearerAuth", Some("jwt"))
.set_global_security("bearerAuth", &[]);
let json_value = doc.to_json_value().unwrap();
assert!(json_value["servers"].is_array());
assert!(json_value["components"]["securitySchemes"]["bearerAuth"].is_object());
assert!(json_value["security"].is_array());
}
#[test]
fn test_add_paths_multiple_and_pretty_json() {
let pi = PathItem::default();
let doc = OpenApiDoc::new("T", "1").add_paths(vec![("/ping".into(), pi)]);
let pretty = doc.to_pretty_json().unwrap();
assert!(pretty.contains("/ping"));
}
}