#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
use core::fmt;
use std::future::Future;
use actix_service::{IntoServiceFactory, ServiceFactory};
use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse};
use actix_web::Error;
use utoipa::openapi::PathItem;
use utoipa::OpenApi;
use self::service_config::ServiceConfig;
pub mod scope;
pub mod service_config;
pub use scope::scope;
pub trait OpenApiFactory {
fn paths(&self) -> utoipa::openapi::path::Paths;
fn schemas(
&self,
schemas: &mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
);
}
impl<'t, T: utoipa::Path + utoipa::__dev::SchemaReferences + utoipa::__dev::Tags<'t>> OpenApiFactory
for T
{
fn paths(&self) -> utoipa::openapi::path::Paths {
let methods = T::methods();
methods
.into_iter()
.fold(
utoipa::openapi::path::Paths::builder(),
|mut builder, method| {
let mut operation = T::operation();
let other_tags = T::tags();
if !other_tags.is_empty() {
let tags = operation.tags.get_or_insert(Vec::new());
tags.extend(other_tags.into_iter().map(ToString::to_string));
};
let path_item = PathItem::new(method, operation);
builder = builder.path(T::path(), path_item);
builder
},
)
.build()
}
fn schemas(
&self,
schemas: &mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
) {
<T as utoipa::__dev::SchemaReferences>::schemas(schemas);
}
}
pub trait AppExt<T> {
fn into_utoipa_app(self) -> UtoipaApp<T>;
}
impl<T> AppExt<T> for actix_web::App<T> {
fn into_utoipa_app(self) -> UtoipaApp<T> {
UtoipaApp::from(self)
}
}
pub struct UtoipaApp<T>(actix_web::App<T>, utoipa::openapi::OpenApi);
impl<T> From<actix_web::App<T>> for UtoipaApp<T> {
fn from(value: actix_web::App<T>) -> Self {
#[derive(OpenApi)]
struct Api;
UtoipaApp(value, Api::openapi())
}
}
impl<T> UtoipaApp<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
{
pub fn openapi(mut self, openapi: utoipa::openapi::OpenApi) -> Self {
self.1 = openapi;
self
}
pub fn app_data<U: 'static>(self, data: U) -> Self {
let app = self.0.app_data(data);
Self(app, self.1)
}
pub fn data_factory<F, Out, D, E>(self, data: F) -> Self
where
F: Fn() -> Out + 'static,
Out: Future<Output = Result<D, E>> + 'static,
D: 'static,
E: std::fmt::Debug,
{
let app = self.0.data_factory(data);
Self(app, self.1)
}
pub fn configure<F>(self, f: F) -> Self
where
F: FnOnce(&mut ServiceConfig),
{
let mut openapi = self.1;
let app = self.0.configure(|config| {
let mut service_config = ServiceConfig::new(config);
f(&mut service_config);
let paths = service_config.1.take();
openapi.paths.merge(paths);
let schemas = service_config.2.take();
let components = openapi
.components
.get_or_insert(utoipa::openapi::Components::new());
components.schemas.extend(schemas);
});
Self(app, openapi)
}
pub fn route(self, path: &str, route: actix_web::Route) -> Self {
let app = self.0.route(path, route);
Self(app, self.1)
}
pub fn service<F>(self, factory: F) -> Self
where
F: HttpServiceFactory + OpenApiFactory + 'static,
{
let mut schemas = Vec::<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>::new();
factory.schemas(&mut schemas);
let paths = factory.paths();
let mut openapi = self.1;
openapi.paths.merge(paths);
let components = openapi
.components
.get_or_insert(utoipa::openapi::Components::new());
components.schemas.extend(schemas);
let app = self.0.service(factory);
Self(app, openapi)
}
pub fn openapi_service<O, F>(self, factory: F) -> Self
where
F: FnOnce(utoipa::openapi::OpenApi) -> O,
O: HttpServiceFactory + 'static,
{
let service = factory(self.1.clone());
let app = self.0.service(service);
Self(app, self.1)
}
pub fn default_service<F, U>(self, svc: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
+ 'static,
U::InitError: fmt::Debug,
{
Self(self.0.default_service(svc), self.1)
}
pub fn external_resource<N, U>(self, name: N, url: U) -> Self
where
N: AsRef<str>,
U: AsRef<str>,
{
Self(self.0.external_resource(name, url), self.1)
}
pub fn map<
F: FnOnce(actix_web::App<T>) -> actix_web::App<NF>,
NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
>(
self,
op: F,
) -> UtoipaApp<NF> {
let app = op(self.0);
UtoipaApp(app, self.1)
}
pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) {
(self.0, self.1)
}
pub fn into_app(self) -> actix_web::App<T> {
self.0
}
}
impl<T> From<UtoipaApp<T>> for actix_web::App<T> {
fn from(value: UtoipaApp<T>) -> Self {
value.0
}
}
#[cfg(test)]
mod tests {
#![allow(unused)]
use actix_service::Service;
use actix_web::guard::{Get, Guard};
use actix_web::http::header::{HeaderValue, CONTENT_TYPE};
use actix_web::web::{self, Data};
use actix_web::{get, App, HttpRequest, HttpResponse};
use utoipa::ToSchema;
use super::*;
#[derive(ToSchema)]
struct Value12 {
v: String,
}
#[derive(ToSchema)]
struct Value2(i32);
#[derive(ToSchema)]
struct Value1 {
bar: Value2,
}
#[derive(ToSchema)]
struct ValueValue {
value: i32,
}
#[utoipa::path(responses(
(status = 200, body = ValueValue)
))]
#[get("/handler2")]
async fn handler2() -> &'static str {
"this is message 2"
}
#[utoipa::path(responses(
(status = 200, body = Value12)
))]
#[get("/handler")]
async fn handler() -> &'static str {
"this is message"
}
#[utoipa::path(responses(
(status = 200, body = Value1)
))]
#[get("/handler3")]
async fn handler3() -> &'static str {
"this is message 3"
}
mod inner {
use actix_web::get;
use actix_web::web::Data;
use utoipa::ToSchema;
#[derive(ToSchema)]
struct Bar(i32);
#[derive(ToSchema)]
struct Foobar {
bar: Bar,
}
#[utoipa::path(responses(
(status = 200, body = Foobar)
))]
#[get("/inner_handler")]
pub async fn inner_handler(_: Data<String>) -> &'static str {
"this is message"
}
#[utoipa::path()]
#[get("/inner_handler3")]
pub async fn inner_handler3(_: Data<String>) -> &'static str {
"this is message 3"
}
}
#[get("/normal_service")]
async fn normal_service() -> &'static str {
"str"
}
#[test]
fn test_app_generate_correct_openapi() {
fn config(cfg: &mut service_config::ServiceConfig) {
cfg.service(handler3)
.map(|config| config.service(normal_service));
}
let (_, mut api) = App::new()
.into_utoipa_app()
.service(handler)
.configure(config)
.service(scope::scope("/path-prefix").service(handler2).map(|scope| {
let s = scope.wrap_fn(|req, srv| {
let fut = srv.call(req);
async {
let mut res = fut.await?;
res.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
Ok(res)
}
});
s
}))
.service(scope::scope("/api/v1/inner").configure(|cfg| {
cfg.service(inner::inner_handler)
.service(inner::inner_handler3)
.app_data(Data::new(String::new()));
}))
.split_for_parts();
api.info = utoipa::openapi::info::Info::new("title", "version");
let json = api.to_pretty_json().expect("OpenAPI is JSON serializable");
println!("{json}");
let expected = include_str!("../testdata/app_generated_openapi");
assert_eq!(json.trim(), expected.trim());
}
}