#![warn(clippy::all)]
#![warn(clippy::nursery)]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![deny(unsafe_code)]
#![allow(clippy::return_self_not_must_use)]
#![allow(clippy::similar_names)]
pub use rovo_macros::rovo;
pub use aide;
pub use schemars;
use ::axum::{response::IntoResponse, Extension};
use aide::axum::ApiRouter as AideApiRouter;
use aide::openapi::OpenApi;
pub struct Router<S = ()> {
inner: AideApiRouter<S>,
oas_spec: Option<OpenApi>,
oas_route: String,
}
impl<S> Router<S>
where
S: Clone + Send + Sync + 'static,
{
#[must_use]
pub fn new() -> Self {
Self {
inner: AideApiRouter::new(),
oas_spec: None,
oas_route: "/api.json".to_string(),
}
}
pub fn route<M>(mut self, path: &str, method_router: M) -> Self
where
M: Into<aide::axum::routing::ApiMethodRouter<S>>,
{
self.inner = self.inner.api_route(path, method_router.into());
self
}
#[must_use]
pub fn nest(mut self, path: &str, router: Self) -> Self {
self.inner = self.inner.nest(path, router.inner);
if self.oas_spec.is_none() && router.oas_spec.is_some() {
self.oas_spec = router.oas_spec;
self.oas_route = router.oas_route;
}
self
}
#[must_use]
pub fn with_oas(mut self, api: OpenApi) -> Self {
self.oas_spec = Some(api);
self.oas_route = "/api.json".to_string();
self
}
pub fn with_oas_route(mut self, api: OpenApi, route: impl Into<String>) -> Self {
self.oas_spec = Some(api);
let route_str = route.into();
let base_route = route_str
.strip_suffix(".json")
.or_else(|| route_str.strip_suffix(".yaml"))
.or_else(|| route_str.strip_suffix(".yml"))
.unwrap_or(&route_str);
self.oas_route = format!("{base_route}.json");
self
}
#[cfg(feature = "swagger")]
#[must_use]
pub fn with_swagger(mut self, swagger_path: &str) -> Self
where
S: Clone + Send + Sync + 'static,
{
let api_route = self.oas_route.clone();
self.inner = self.inner.route(
swagger_path,
aide::swagger::Swagger::new(&api_route).axum_route(),
);
self
}
#[cfg(feature = "redoc")]
#[must_use]
pub fn with_redoc(mut self, redoc_path: &str) -> Self
where
S: Clone + Send + Sync + 'static,
{
let api_route = self.oas_route.clone();
self.inner = self
.inner
.route(redoc_path, aide::redoc::Redoc::new(&api_route).axum_route());
self
}
#[cfg(feature = "scalar")]
#[must_use]
pub fn with_scalar(mut self, scalar_path: &str) -> Self
where
S: Clone + Send + Sync + 'static,
{
let api_route = self.oas_route.clone();
self.inner = self.inner.route(
scalar_path,
aide::scalar::Scalar::new(&api_route).axum_route(),
);
self
}
fn wire_openapi_routes(self) -> (Option<::axum::Router<S>>, Option<AideApiRouter<S>>)
where
S: Clone + Send + Sync + 'static,
{
if let Some(api) = self.oas_spec {
let oas_route = self.oas_route.clone();
let mut api_mut = api;
let axum_router = self.inner.finish_api(&mut api_mut);
let api_for_json = api_mut.clone();
let api_for_yaml = api_mut.clone();
let api_for_yml = api_mut.clone();
let base_route = oas_route.strip_suffix(".json").unwrap_or(&oas_route);
let router_with_json = axum_router.route(
&oas_route,
::axum::routing::get(move || {
let api = api_for_json.clone();
async move { ::axum::Json(api) }
}),
);
let yaml_route = format!("{base_route}.yaml");
let router_with_yaml = router_with_json.route(
&yaml_route,
::axum::routing::get(move || {
let api = api_for_yaml.clone();
async move {
match serde_yaml::to_string(&api) {
Ok(yaml) => (
[(::axum::http::header::CONTENT_TYPE, "application/x-yaml")],
yaml,
)
.into_response(),
Err(e) => (
::axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to serialize OpenAPI spec to YAML: {e}"),
)
.into_response(),
}
}
}),
);
let yml_route = format!("{base_route}.yml");
let router_with_yml = router_with_yaml.route(
&yml_route,
::axum::routing::get(move || {
let api = api_for_yml.clone();
async move {
match serde_yaml::to_string(&api) {
Ok(yaml) => (
[(::axum::http::header::CONTENT_TYPE, "application/x-yaml")],
yaml,
)
.into_response(),
Err(e) => (
::axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to serialize OpenAPI spec to YAML: {e}"),
)
.into_response(),
}
}
}),
);
(Some(router_with_yml.layer(Extension(api_mut))), None)
} else {
(None, Some(self.inner))
}
}
pub fn with_state(self, state: S) -> ::axum::Router
where
S: Clone + Send + Sync + 'static,
{
let (with_oas, without_oas) = self.wire_openapi_routes();
if let Some(router) = with_oas {
router.with_state(state)
} else if let Some(inner) = without_oas {
inner.with_state(state).into()
} else {
unreachable!("Either with_oas or without_oas must be Some")
}
}
pub fn finish(self) -> ::axum::Router<S>
where
S: Clone + Send + Sync + 'static,
{
let (with_oas, without_oas) = self.wire_openapi_routes();
with_oas.unwrap_or_else(|| {
without_oas.map_or_else(
|| unreachable!("Either with_oas or without_oas must be Some"),
std::convert::Into::into,
)
})
}
pub fn finish_api(self, api: &mut aide::openapi::OpenApi) -> ::axum::Router<S> {
self.inner.finish_api(api)
}
pub fn finish_api_with_extension(self, api: aide::openapi::OpenApi) -> ::axum::Router<S>
where
S: Clone + Send + Sync + 'static,
{
let mut api_mut = api;
self.inner
.finish_api(&mut api_mut)
.layer(Extension(api_mut))
}
pub fn into_inner(self) -> AideApiRouter<S> {
self.inner
}
}
impl<S> Default for Router<S>
where
S: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
pub trait IntoApiMethodRouter<S = ()> {
fn into_get_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
fn into_post_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
fn into_patch_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
fn into_delete_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
fn into_put_route(self) -> aide::axum::routing::ApiMethodRouter<S>;
}
pub struct ApiMethodRouter<S = ()> {
inner: aide::axum::routing::ApiMethodRouter<S>,
}
impl<S> ApiMethodRouter<S>
where
S: Clone + Send + Sync + 'static,
{
#[must_use]
pub const fn new(inner: aide::axum::routing::ApiMethodRouter<S>) -> Self {
Self { inner }
}
pub fn post<H>(self, handler: H) -> Self
where
H: IntoApiMethodRouter<S>,
{
Self {
inner: self.inner.merge(handler.into_post_route()),
}
}
pub fn get<H>(self, handler: H) -> Self
where
H: IntoApiMethodRouter<S>,
{
Self {
inner: self.inner.merge(handler.into_get_route()),
}
}
pub fn patch<H>(self, handler: H) -> Self
where
H: IntoApiMethodRouter<S>,
{
Self {
inner: self.inner.merge(handler.into_patch_route()),
}
}
pub fn delete<H>(self, handler: H) -> Self
where
H: IntoApiMethodRouter<S>,
{
Self {
inner: self.inner.merge(handler.into_delete_route()),
}
}
pub fn put<H>(self, handler: H) -> Self
where
H: IntoApiMethodRouter<S>,
{
Self {
inner: self.inner.merge(handler.into_put_route()),
}
}
}
impl<S> From<ApiMethodRouter<S>> for aide::axum::routing::ApiMethodRouter<S> {
fn from(router: ApiMethodRouter<S>) -> Self {
router.inner
}
}
pub mod routing {
use super::{ApiMethodRouter, IntoApiMethodRouter};
pub fn get<S, H>(handler: H) -> ApiMethodRouter<S>
where
H: IntoApiMethodRouter<S>,
S: Clone + Send + Sync + 'static,
{
ApiMethodRouter::new(handler.into_get_route())
}
pub fn post<S, H>(handler: H) -> ApiMethodRouter<S>
where
H: IntoApiMethodRouter<S>,
S: Clone + Send + Sync + 'static,
{
ApiMethodRouter::new(handler.into_post_route())
}
pub fn patch<S, H>(handler: H) -> ApiMethodRouter<S>
where
H: IntoApiMethodRouter<S>,
S: Clone + Send + Sync + 'static,
{
ApiMethodRouter::new(handler.into_patch_route())
}
pub fn delete<S, H>(handler: H) -> ApiMethodRouter<S>
where
H: IntoApiMethodRouter<S>,
S: Clone + Send + Sync + 'static,
{
ApiMethodRouter::new(handler.into_delete_route())
}
pub fn put<S, H>(handler: H) -> ApiMethodRouter<S>
where
H: IntoApiMethodRouter<S>,
S: Clone + Send + Sync + 'static,
{
ApiMethodRouter::new(handler.into_put_route())
}
}
pub mod axum {
pub use aide::axum::{ApiRouter, IntoApiResponse};
}