use crate::transport::http::route::{Builder, Route};
use crate::transport::http::{Request, Response};
use crate::transport::Context;
use once_cell::sync::Lazy;
use std::collections::btree_map::Entry;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use utoipa::openapi::path::Parameter;
use utoipa::openapi::request_body::{RequestBody, RequestBodyBuilder};
use utoipa::openapi::security::SecurityScheme;
use utoipa::openapi::{
Components, Content, Deprecated, PathItem, PathItemType, Ref, RefOr, Required, ResponseBuilder,
Responses, Schema, SecurityRequirement, Server,
};
use utoipa::OpenApi;
use utoipa::ToSchema;
use utoipa_swagger_ui::{serve, Config};
fn append_to_components(name: impl Into<String>, schema: RefOr<Schema>) {
let mut open_api = OPENAPI_DOC.lock().unwrap();
if open_api.components.is_none() {
open_api.components = Some(Components::new());
}
if let Some(c) = open_api.components.as_mut() {
c.schemas.insert(name.into(), schema);
}
}
pub struct SchemaDefenition {
main_schema_name: String,
schemas: Vec<(String, RefOr<Schema>)>,
}
impl SchemaDefenition {
fn schema_from_type<'a, T: ToSchema<'a>>(schema_name: &str) -> (&'a str, RefOr<Schema>) {
let aliases = T::aliases();
if !aliases.is_empty() {
let type_name = schema_name.split("::").last().unwrap();
aliases
.into_iter()
.find_map(|(name, schema)| {
if name == type_name {
return Some((name, RefOr::T(schema)));
}
None
})
.unwrap_or_else(|| panic!("expect one of alias names, got {type_name}"))
} else {
T::schema()
}
}
pub fn new<'a, S: ToSchema<'a>>(schema_name: &str) -> Self {
let (schema_name, schema) = Self::schema_from_type::<S>(schema_name);
Self {
schemas: vec![(schema_name.to_string(), schema)],
main_schema_name: schema_name.to_string(),
}
}
pub fn component<'a, C: ToSchema<'a>>(self, schema_name: &str) -> Self {
let mut schemas = self.schemas;
let (schema_name, schema) = Self::schema_from_type::<C>(schema_name);
schemas.push((schema_name.to_string(), schema));
Self { schemas, ..self }
}
fn build(self) {
self.schemas
.into_iter()
.for_each(|(name, schema)| append_to_components(name, schema))
}
}
#[macro_export]
macro_rules! define_schema {
($main_schema: ty, $($component:ty),*) => {
{
use $crate::transport::http::openapi::SchemaDefenition;
SchemaDefenition::new::<$main_schema>(stringify!($main_schema))
$( .component::<$component>(stringify!($component)) )*
}
};
($main_schema: ty) => {
define_schema!($main_schema,)
};
}
#[derive(Default, Clone)]
pub struct RouteOperation {
responses: Responses,
request: Option<RequestBody>,
params: Option<Vec<Parameter>>,
tags: Option<Vec<String>>,
summary: Option<String>,
description: Option<String>,
operation_id: Option<String>,
deprecated: Option<Deprecated>,
sec_requirements: Option<Vec<SecurityRequirement>>,
servers: Option<Vec<Server>>,
}
impl RouteOperation {
pub fn new() -> Self {
Self::default()
}
pub fn with_response(
self,
schema: SchemaDefenition,
code: impl Into<String>,
content_type: impl Into<String>,
description: impl Into<String>,
) -> Self {
let main_schema_name = schema.main_schema_name.clone();
schema.build();
let response_builder = ResponseBuilder::new().description(description).content(
content_type,
Content::new(RefOr::Ref(Ref::from_schema_name(main_schema_name))),
);
let mut responses = self.responses;
responses
.responses
.insert(code.into(), RefOr::T(response_builder.build()));
Self { responses, ..self }
}
pub fn with_request(
self,
schema: SchemaDefenition,
required: bool,
content_type: impl Into<String>,
description: impl Into<String>,
) -> Self {
let main_schema_name = schema.main_schema_name.clone();
schema.build();
let mut req_builder = RequestBodyBuilder::new()
.description(Some(description))
.content(
content_type,
Content::new(RefOr::Ref(Ref::from_schema_name(main_schema_name))),
);
if required {
req_builder = req_builder.required(Some(Required::True))
}
Self {
request: Some(req_builder.build()),
..self
}
}
pub fn with_param(self, param: Parameter) -> Self {
let mut params = self.params.unwrap_or_default();
params.push(param);
Self {
params: Some(params),
..self
}
}
pub fn with_params(self, params: impl Iterator<Item = Parameter>) -> Self {
let mut current_params = self.params.unwrap_or_default();
current_params.extend(params);
Self {
params: Some(current_params),
..self
}
}
pub fn with_tag(self, tag: impl Into<String>) -> Self {
let mut tags = self.tags.unwrap_or_default();
tags.push(tag.into());
Self {
tags: Some(tags),
..self
}
}
pub fn with_summary(self, summary: impl Into<String>) -> Self {
Self {
summary: Some(summary.into()),
..self
}
}
pub fn with_description(self, descr: impl Into<String>) -> Self {
Self {
description: Some(descr.into()),
..self
}
}
pub fn with_operation_id(self, op_id: impl Into<String>) -> Self {
Self {
operation_id: Some(op_id.into()),
..self
}
}
pub fn set_deprecated(self) -> Self {
Self {
deprecated: Some(Deprecated::True),
..self
}
}
pub fn with_security(
self,
name: impl Into<String>,
schema: SecurityScheme,
scopes: &[String],
) -> Self {
let mut open_api = OPENAPI_DOC.lock().unwrap();
if open_api.components.is_none() {
open_api.components = Some(Components::new());
}
let name = name.into();
if let Some(c) = open_api.components.as_mut() {
c.add_security_scheme(name.clone(), schema)
}
let req = SecurityRequirement::new(name, scopes);
let mut sec_requirements = self.sec_requirements.unwrap_or_default();
sec_requirements.push(req);
Self {
sec_requirements: Some(sec_requirements),
..self
}
}
pub fn with_server(self, server: Server) -> Self {
let mut servers = self.servers.unwrap_or_default();
servers.push(server);
Self {
servers: Some(servers),
..self
}
}
pub(crate) fn update_global_doc(self, path: &str, method: &str) {
let it_type = match method.to_lowercase().as_str() {
"get" => PathItemType::Get,
"post" => PathItemType::Post,
"put" => PathItemType::Put,
"patch" => PathItemType::Patch,
"delete" => PathItemType::Delete,
_ => PathItemType::Get,
};
let op_builder = utoipa::openapi::path::OperationBuilder::new()
.tags(self.tags)
.summary(self.summary)
.description(self.description)
.operation_id(self.operation_id)
.deprecated(self.deprecated)
.securities(self.sec_requirements)
.servers(self.servers)
.responses(self.responses)
.request_body(self.request)
.parameters(self.params);
let mut open_api = OPENAPI_DOC.lock().unwrap();
match open_api.paths.paths.entry(path.to_string()) {
Entry::Vacant(e) => {
e.insert(PathItem::new(it_type, op_builder));
}
Entry::Occupied(mut item) => {
item.get_mut()
.operations
.insert(it_type, op_builder.build());
}
}
}
}
#[derive(OpenApi)]
#[openapi()]
pub struct ApiDoc;
static OPENAPI_DOC: Lazy<Mutex<utoipa::openapi::OpenApi>> =
Lazy::new(|| Mutex::new(ApiDoc::openapi()));
pub fn with_open_api<T>(f: fn(&mut utoipa::openapi::OpenApi) -> T) -> T {
let mut lock = OPENAPI_DOC.lock().unwrap();
f(&mut lock)
}
pub fn swagger_ui_route<E>(base_path: &'static str, config: Arc<Config<'static>>) -> Route<E> {
Builder::new()
.with_method("GET")
.with_path(base_path)
.with_path("/:tail")
.build(move |_ctx: &mut Context, req: Request| -> Result<_, E> {
let tail = req.stash.get("tail").map(|s| s.as_str()).unwrap_or("/");
match serve(tail, config.clone()) {
Ok(swagger_file) => swagger_file
.map(|file| {
Ok(Response {
status: 200,
body: file.bytes.to_vec(),
headers: HashMap::from([(
"content-type".to_string(),
file.content_type,
)]),
})
})
.unwrap_or_else(|| {
Ok(Response {
status: 404,
body: "not found".as_bytes().to_vec(),
headers: HashMap::from([(
"content-type".to_string(),
"text/html; charset=utf-8".to_string(),
)]),
})
}),
Err(error) => Ok(Response {
status: 500,
body: error.to_string().as_bytes().to_vec(),
headers: HashMap::from([(
"content-type".to_string(),
"text/html; charset=utf-8".to_string(),
)]),
}),
}
})
}