use std::collections::BTreeMap;
use utoipa::PartialSchema;
use utoipa::ToSchema;
use utoipa::openapi::{
HttpMethod, InfoBuilder, OpenApi, OpenApiBuilder, PathItem, Paths, PathsBuilder, RefOr, Schema,
path::{Operation, PathItemBuilder},
schema::{Components, ComponentsBuilder},
security::{HttpAuthScheme, HttpBuilder, SecurityRequirement, SecurityScheme},
};
pub(crate) const ESQUEMA_BEARER: &str = "bearerAuth";
#[derive(Default)]
pub(crate) struct OpenApiState {
title: Option<String>,
version: Option<String>,
paths: Vec<(String, HttpMethod, Operation, SegurancaDaRota)>,
schemas: BTreeMap<String, RefOr<Schema>>,
}
impl OpenApiState {
pub(crate) fn set_info(&mut self, title: impl Into<String>, version: impl Into<String>) {
self.title = Some(title.into());
self.version = Some(version.into());
}
pub(crate) fn push_operation(
&mut self,
path: impl Into<String>,
method: HttpMethod,
op: Operation,
seguranca: SegurancaDaRota,
) {
self.paths.push((path.into(), method, op, seguranca));
}
pub(crate) fn register_schema<T: ToSchema + PartialSchema>(&mut self) {
let name = T::name().into_owned();
let schema = <T as PartialSchema>::schema();
self.schemas.insert(name, schema);
}
pub(crate) fn build(&self, auth_instalada: bool) -> OpenApi {
let info = InfoBuilder::new()
.title(self.title.clone().unwrap_or_else(|| "serverust".into()))
.version(self.version.clone().unwrap_or_else(|| "0.1.0".into()))
.build();
let mut grouped: BTreeMap<String, Vec<(HttpMethod, Operation)>> = BTreeMap::new();
for (path, method, op, seguranca) in &self.paths {
let mut op = op.clone();
if auth_instalada {
op.security = Some(seguranca.requisitos());
}
grouped
.entry(path.clone())
.or_default()
.push((method.clone(), op));
}
let mut paths_builder = PathsBuilder::new();
for (path, ops) in grouped {
let mut item_builder = PathItemBuilder::new();
for (method, op) in ops {
item_builder = item_builder.operation(method, op);
}
let item: PathItem = item_builder.build();
paths_builder = paths_builder.path(path, item);
}
let paths: Paths = paths_builder.build();
let components: Option<Components> = if self.schemas.is_empty() && !auth_instalada {
None
} else {
let mut cb = ComponentsBuilder::new();
for (name, schema) in &self.schemas {
cb = cb.schema(name, schema.clone());
}
if auth_instalada {
cb = cb.security_scheme(ESQUEMA_BEARER, esquema_bearer());
}
Some(cb.build())
};
let mut builder = OpenApiBuilder::new()
.info(info)
.paths(paths)
.components(components);
if auth_instalada {
builder = builder.security(Some([SecurityRequirement::new::<_, [&str; 0], _>(
ESQUEMA_BEARER,
[],
)]));
}
builder.build()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SegurancaDaRota {
Publica,
ExigeIdentidade,
ExigeEscopos(&'static [&'static str]),
}
impl SegurancaDaRota {
fn requisitos(self) -> Vec<SecurityRequirement> {
match self {
Self::Publica => Vec::new(),
Self::ExigeIdentidade => vec![SecurityRequirement::new::<_, [&str; 0], _>(
ESQUEMA_BEARER,
[],
)],
Self::ExigeEscopos(escopos) => {
vec![SecurityRequirement::new(
ESQUEMA_BEARER,
escopos.iter().copied(),
)]
}
}
}
}
fn esquema_bearer() -> SecurityScheme {
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.description(Some(
"JWT emitido pelo provedor de identidade configurado. \
Envie em `Authorization: Bearer <token>`.",
))
.build(),
)
}
pub(crate) fn swagger_ui_html(spec_url: &str) -> String {
format!(
r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API Reference</title>
</head>
<body>
<script
id="api-reference"
data-url="{spec_url}"
data-configuration='{{"theme":"default","layout":"modern"}}'
></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>"##
)
}
pub(crate) fn redoc_html(spec_url: &str) -> String {
format!(
r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>API ReDoc</title>
</head>
<body>
<redoc spec-url="{spec_url}"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
</body>
</html>"##
)
}