#![doc = include_str!("../README.md")]
use std::collections::{BTreeMap, HashMap};
use auto_di::{BoxFuture, Container, DiError};
use axum::{Json, Router, response::Html, routing::get};
use poem_openapi::{
__private::poem::{endpoint::BoxEndpoint, http::Method},
OpenApi, OpenApiService, ParameterStyle,
registry::{
MetaApi, MetaMediaType, MetaOperation, MetaOperationParam, MetaParamIn, MetaPath,
MetaRequest, MetaResponse, MetaResponses, MetaSchemaRef, MetaTag, Registry,
},
types::Type,
};
use scalar_api_reference::axum as scalar;
use serde_json::Value;
pub use auto_route_macros::{controller, delete, get, head, options, patch, post, put};
#[doc(hidden)]
pub struct RouteDescriptor {
factory: for<'a> fn(&'a Container) -> BoxFuture<'a, Result<Router<()>, DiError>>,
}
impl RouteDescriptor {
#[doc(hidden)]
pub const fn new(
factory: for<'a> fn(&'a Container) -> BoxFuture<'a, Result<Router<()>, DiError>>,
) -> Self {
Self { factory }
}
}
inventory::collect!(RouteDescriptor);
#[doc(hidden)]
pub struct OpenApiRouteDescriptor {
method: &'static str,
path: &'static str,
operation_id: &'static str,
tag: &'static str,
params: &'static [OpenApiParamDescriptor],
request: Option<OpenApiSchemaDescriptor>,
response: Option<OpenApiSchemaDescriptor>,
}
impl OpenApiRouteDescriptor {
#[doc(hidden)]
pub const fn new(
method: &'static str,
path: &'static str,
operation_id: &'static str,
tag: &'static str,
params: &'static [OpenApiParamDescriptor],
request: Option<OpenApiSchemaDescriptor>,
response: Option<OpenApiSchemaDescriptor>,
) -> Self {
Self {
method,
path,
operation_id,
tag,
params,
request,
response,
}
}
}
inventory::collect!(OpenApiRouteDescriptor);
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct OpenApiSchemaDescriptor {
schema_ref: fn() -> MetaSchemaRef,
register: fn(&mut Registry),
content_type: &'static str,
}
impl OpenApiSchemaDescriptor {
#[doc(hidden)]
pub const fn json<T: Type>() -> Self {
Self {
schema_ref: schema_ref::<T>,
register: register_type::<T>,
content_type: "application/json",
}
}
#[doc(hidden)]
pub const fn form<T: Type>() -> Self {
Self {
schema_ref: schema_ref::<T>,
register: register_type::<T>,
content_type: "application/x-www-form-urlencoded",
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct OpenApiParamDescriptor {
name: &'static str,
in_type: MetaParamIn,
schema: OpenApiSchemaDescriptor,
required: bool,
deprecated: bool,
explode: bool,
style: Option<ParameterStyle>,
}
impl OpenApiParamDescriptor {
#[doc(hidden)]
pub const fn path<T: Type>(name: &'static str) -> Self {
Self {
name,
in_type: MetaParamIn::Path,
schema: OpenApiSchemaDescriptor::json::<T>(),
required: true,
deprecated: false,
explode: false,
style: None,
}
}
#[doc(hidden)]
pub const fn query<T: Type>(name: &'static str) -> Self {
Self {
name,
in_type: MetaParamIn::Query,
schema: OpenApiSchemaDescriptor::json::<T>(),
required: true,
deprecated: false,
explode: true,
style: Some(ParameterStyle::Form),
}
}
}
fn schema_ref<T: Type>() -> MetaSchemaRef {
T::schema_ref()
}
fn register_type<T: Type>(registry: &mut Registry) {
T::register(registry);
}
pub async fn build_routes(container: &Container) -> Result<Router<()>, DiError> {
let mut router = Router::new();
for descriptor in inventory::iter::<RouteDescriptor> {
router = router.merge((descriptor.factory)(container).await?);
}
Ok(router)
}
pub async fn routes() -> Result<Router<()>, DiError> {
build_routes(auto_di::global_container()?).await
}
#[doc(hidden)]
pub struct AutoRouteOpenApi;
impl OpenApi for AutoRouteOpenApi {
fn meta() -> Vec<MetaApi> {
let mut paths = BTreeMap::<String, Vec<MetaOperation>>::new();
for descriptor in inventory::iter::<OpenApiRouteDescriptor> {
paths
.entry(descriptor.path.to_owned())
.or_default()
.push(openapi_operation(descriptor));
}
vec![MetaApi {
paths: paths
.into_iter()
.map(|(path, operations)| MetaPath { path, operations })
.collect(),
}]
}
fn register(registry: &mut Registry) {
for descriptor in inventory::iter::<OpenApiRouteDescriptor> {
registry.tags.insert(MetaTag {
name: descriptor.tag,
description: None,
external_docs: None,
});
if let Some(schema) = descriptor.request {
(schema.register)(registry);
}
if let Some(schema) = descriptor.response {
(schema.register)(registry);
}
for param in descriptor.params {
(param.schema.register)(registry);
}
}
}
fn add_routes(self, _route_table: &mut HashMap<String, HashMap<Method, BoxEndpoint<'static>>>) {
}
}
pub fn openapi_json() -> Value {
serde_json::from_str(&openapi_spec()).expect("poem-openapi spec must be valid JSON")
}
pub fn openapi_spec() -> String {
OpenApiService::new(
AutoRouteOpenApi,
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
)
.spec()
}
fn openapi_operation(descriptor: &OpenApiRouteDescriptor) -> MetaOperation {
MetaOperation {
method: method_from_str(descriptor.method),
tags: vec![descriptor.tag],
summary: None,
description: None,
external_docs: None,
params: descriptor
.params
.iter()
.map(|param| param.to_meta())
.collect(),
request: descriptor.request.map(|schema| MetaRequest {
description: None,
content: vec![schema.media_type()],
required: true,
}),
responses: MetaResponses {
responses: vec![MetaResponse {
description: "Successful response",
status: Some(200),
status_range: None,
content: descriptor
.response
.map(|schema| vec![schema.media_type()])
.unwrap_or_default(),
headers: Vec::new(),
}],
},
deprecated: false,
security: Vec::new(),
operation_id: Some(descriptor.operation_id),
code_samples: Vec::new(),
}
}
impl OpenApiSchemaDescriptor {
fn media_type(self) -> MetaMediaType {
MetaMediaType {
content_type: self.content_type,
schema: (self.schema_ref)(),
}
}
}
impl OpenApiParamDescriptor {
fn to_meta(self) -> MetaOperationParam {
MetaOperationParam {
name: self.name.trim_start_matches('*').to_owned(),
schema: (self.schema.schema_ref)(),
in_type: self.in_type,
description: None,
required: self.required,
deprecated: self.deprecated,
explode: self.explode,
style: self.style,
}
}
}
fn method_from_str(method: &str) -> Method {
match method {
"GET" => Method::GET,
"POST" => Method::POST,
"PUT" => Method::PUT,
"DELETE" => Method::DELETE,
"PATCH" => Method::PATCH,
"OPTIONS" => Method::OPTIONS,
"HEAD" => Method::HEAD,
_ => Method::GET,
}
}
pub fn openapi_routes(json_path: &'static str, ui_path: &'static str) -> Router<()> {
Router::new()
.route(json_path, get(|| async { Json(openapi_json()) }))
.route(ui_path, get(|| async { Html(swagger_ui_html()) }))
}
pub fn scalar_routes(scalar_path: &'static str, openapi_json_path: &'static str) -> Router<()> {
let configuration = serde_json::json!({
"url": openapi_json_path,
"theme": "kepler",
"layout": "modern",
"showSidebar": true,
"hideDownloadButton": false,
"agent": { "disabled": true },
});
scalar::router(scalar_path, &configuration)
}
fn swagger_ui_html() -> String {
OpenApiService::new(
AutoRouteOpenApi,
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
)
.swagger_ui_html()
}
#[doc(hidden)]
pub mod __private {
pub use auto_di;
pub use axum;
pub use inventory;
pub use poem_openapi;
}