Skip to content

Commit

Permalink
Wip initial implementation of service config
Browse files Browse the repository at this point in the history
Implements `ServiceConfig` for actix-web with scope support. This allows
users to register `services` directly to OpenApi via `utiopa` App and
custom service config without the need to register paths via
`#[openapi(paths(...))]`.

Closes #121
  • Loading branch information
juhaku committed Oct 21, 2024
1 parent 155657f commit 57ccb9f
Show file tree
Hide file tree
Showing 6 changed files with 461 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"utoipa-scalar",
"utoipa-axum",
"utoipa-config",
"utoipa-actix-web",
]

[workspace.metadata.publish]
Expand All @@ -26,4 +27,5 @@ order = [
"utoipa-rapidoc",
"utoipa-scalar",
"utoipa-axum",
"utoipa-actix-web",
]
19 changes: 19 additions & 0 deletions utoipa-actix-web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "utoipa-actix-web"
version = "0.1.0"
edition = "2021"
rust-version.workspace = true

[dependencies]
utoipa = { path = "../utoipa", version = "5" }
actix-web = { version = "4", default-features = false }
actix-service = "2"
once_cell = "1"

[dev-dependencies]
utoipa = { path = "../utoipa", version = "5", features = [
"actix_extras",
"macros",
"debug",
] }
actix-web = { version = "4", default-features = false, features = ["macros"] }
233 changes: 233 additions & 0 deletions utoipa-actix-web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
use core::fmt;
use std::future::Future;
use std::rc::Rc;

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 trait PathsFactory {
fn paths(&self) -> utoipa::openapi::path::Paths;
}

impl<T: utoipa::Path> PathsFactory 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| {
builder = builder.path(T::path(), PathItem::new(method, T::operation()));

builder
},
)
.build()
}
}

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>, Rc<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, Rc::new(Api::openapi()))
}
}

impl<T> UtoipaApp<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
{
/// Passthrough implementation for [`actix_web::App::app_data`].
pub fn app_data<U: 'static>(self, data: U) -> Self {
let app = self.0.app_data(data);
Self(app, self.1)
}

/// Passthrough implementation for [`actix_web::App::data_factory`].
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)
}

/// Passthrough implementation for [`actix_web::App::configure`].
pub fn configure<F>(mut self, f: F) -> Self
where
F: FnOnce(&mut ServiceConfig),
{
// TODO get OpenAPI paths????
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect(
"OpenApi should not have more than one reference when building App with `configure`",
);

let app = self.0.configure(|config| {
let mut service_config = ServiceConfig::new(config);

f(&mut service_config);

let ServiceConfig(_, paths) = service_config;
api.paths.paths.extend(paths.take().paths);
});

Self(app, self.1)
}

/// Passthrough implementation for [`actix_web::App::route`].
pub fn route(self, path: &str, route: actix_web::Route) -> Self {
let app = self.0.route(path, route);

Self(app, self.1)
}

/// Passthrough implementation for [`actix_web::App::service`].
pub fn service<F>(mut self, factory: F) -> Self
where
F: HttpServiceFactory + PathsFactory + 'static,
{
let paths = factory.paths();

// TODO should this be `make_mut`?
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect(
"OpenApi should not have more than one reference when building App with `service`",
);

api.paths.paths.extend(paths.paths);
let app = self.0.service(factory);

Self(app, self.1)
}

pub fn openapi_service<O, F>(self, factory: F) -> Self
where
F: FnOnce(Rc<utoipa::openapi::OpenApi>) -> O,
O: HttpServiceFactory + 'static,
{
let service = factory(self.1.clone());
let app = self.0.service(service);
Self(app, self.1)
}

/// Passthrough implementation for [`actix_web::App::default_service`].
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)
}

/// Passthrough implementation for [`actix_web::App::external_resource`].
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,
Rc::try_unwrap(self.1).unwrap_or_else(|rc| (*rc).clone()),
)
}
}

#[cfg(test)]
mod tests {
use actix_service::Service;
use actix_web::http::header::{HeaderValue, CONTENT_TYPE};
use actix_web::{get, App};

use super::*;

#[utoipa::path(impl_for = handler2)]
#[get("/handler2")]
async fn handler2() -> &'static str {
"this is message 2"
}

#[utoipa::path(impl_for = handler)]
#[get("/handler")]
async fn handler() -> &'static str {
"this is message"
}

#[utoipa::path(impl_for = handler3)]
#[get("/handler3")]
async fn handler3() -> &'static str {
"this is message 3"
}

#[test]
fn test_app() {
fn config(cfg: &mut service_config::ServiceConfig) {
cfg.service(handler3);
}

let (_, 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
}))
.split_for_parts();

dbg!(api);
// let app = app.service(scope::scope("prefix").service(handler));

// app
}
}
Loading

0 comments on commit 57ccb9f

Please sign in to comment.