Skip to content

Commit

Permalink
Finalize actix-web utoipa bindings (#1160)
Browse files Browse the repository at this point in the history
This commit adds missing tags support for utoipa actix-web bindings and
fixes an issue with path merging in the bindings.
  • Loading branch information
juhaku authored Oct 23, 2024
1 parent 31abe78 commit fc45782
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 66 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ being portable and standalone, one of its key aspects is simple integration with

|Flavor|Support|
|--|--|
|[actix-web](https://github.com/actix/actix-web)|Parse path, path parameters and query parameters, recognize request body and response body. See more at [docs](https://docs.rs/utoipa/latest/utoipa/attr.path.html#actix_extras-feature-support-for-actix-web)|
|[actix-web](https://github.com/actix/actix-web)|Parse path, path parameters and query parameters, recognize request body and response body, [`utoipa-actix-web` bindings](./utoipa-actix-web/README.md). See more at [docs](https://docs.rs/utoipa/latest/utoipa/attr.path.html#actix_extras-feature-support-for-actix-web)|
|[axum](https://github.com/tokio-rs/axum)|Parse path and query parameters, recognize request body and response body, [`utoipa-axum` bindings](./utoipa-axum/README.md). See more at [docs](https://docs.rs/utoipa/latest/utoipa/attr.path.html#axum_extras-feature-support-for-axum)|
|[rocket](https://github.com/SergioBenitez/Rocket)| Parse path, path parameters and query parameters, recognize request body and response body. See more at [docs](https://docs.rs/utoipa/latest/utoipa/attr.path.html#rocket_extras-feature-support-for-rocket)|
|Others*| Plain `utoipa` without extra flavor. This gives you all the basic benefits listed below in **[Features](#features)** section but with little less automation.|
Expand Down
4 changes: 4 additions & 0 deletions utoipa-actix-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
### Added

* Add implementation for utoipa-actix-web bindings (https://github.com/juhaku/utoipa/pull/1158)

### Changed

* Finalize actix-web utoipa bindings (https://github.com/juhaku/utoipa/pull/1160)
77 changes: 38 additions & 39 deletions utoipa-actix-web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@

use core::fmt;
use std::future::Future;
use std::rc::Rc;

use actix_service::{IntoServiceFactory, ServiceFactory};
use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse};
Expand Down Expand Up @@ -75,7 +74,9 @@ pub trait OpenApiFactory {
);
}

impl<T: utoipa::Path + utoipa::__dev::SchemaReferences> OpenApiFactory for T {
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();

Expand All @@ -84,7 +85,15 @@ impl<T: utoipa::Path + utoipa::__dev::SchemaReferences> OpenApiFactory for T {
.fold(
utoipa::openapi::path::Paths::builder(),
|mut builder, method| {
builder = builder.path(T::path(), PathItem::new(method, T::operation()));
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
},
Expand Down Expand Up @@ -144,13 +153,13 @@ impl<T> AppExt<T> for actix_web::App<T> {
/// # use actix_web::App;
/// let a: UtoipaApp<_> = actix_web::App::new().into();
/// ```
pub struct UtoipaApp<T>(actix_web::App<T>, Rc<utoipa::openapi::OpenApi>);
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, Rc::new(Api::openapi()))
UtoipaApp(value, Api::openapi())
}
}

Expand All @@ -177,7 +186,7 @@ where
/// let _ = actix_web::App::new().into_utoipa_app().openapi(Api::openapi());
/// ```
pub fn openapi(mut self, openapi: utoipa::openapi::OpenApi) -> Self {
self.1 = Rc::new(openapi);
self.1 = openapi;

self
}
Expand All @@ -203,36 +212,27 @@ where

/// Extended version of [`actix_web::App::configure`] which handles _`schema`_ and _`path`_
/// collection from [`ServiceConfig`] into the wrapped [`utoipa::openapi::OpenApi`] instance.
///
/// # Panics
///
/// If [`UtoipaApp::configure`] is called after [`UtoipaApp::openapi_service`] call. This is because
/// reference count is increase on each call and configuration cannot modify
/// [`utoipa::openapi::OpenApi`] that is already served.
pub fn configure<F>(mut self, f: F) -> Self
pub fn configure<F>(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 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();
api.paths.paths.extend(paths.paths);
openapi.paths.merge(paths);
let schemas = service_config.2.take();
let components = api
let components = openapi
.components
.get_or_insert(utoipa::openapi::Components::new());
components.schemas.extend(schemas);
});

Self(app, self.1)
Self(app, openapi)
}

/// Passthrough implementation for [`actix_web::App::route`].
Expand All @@ -244,13 +244,7 @@ where

/// Extended version of [`actix_web::App::service`] method which handles _`schema`_ and _`path`_
/// collection from [`HttpServiceFactory`].
///
/// # Panics
///
/// If [`UtoipaApp::service`] is called after [`UtoipaApp::openapi_service`] call. This is because
/// reference count is increase on each call and we should have only one instance of
/// [`utoipa::openapi::OpenApi`] that is being built.
pub fn service<F>(mut self, factory: F) -> Self
pub fn service<F>(self, factory: F) -> Self
where
F: HttpServiceFactory + OpenApiFactory + 'static,
{
Expand All @@ -262,20 +256,17 @@ where
factory.schemas(&mut schemas);
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`",
);
let mut openapi = self.1;

api.paths.paths.extend(paths.paths);
let components = api
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, self.1)
Self(app, openapi)
}

/// Helper method to serve wrapped [`utoipa::openapi::OpenApi`] via [`HttpServiceFactory`].
Expand All @@ -284,7 +275,7 @@ where
/// first call [`UtoipaApp::split_for_parts`] and then calling [`actix_web::App::service`].
pub fn openapi_service<O, F>(self, factory: F) -> Self
where
F: FnOnce(Rc<utoipa::openapi::OpenApi>) -> O,
F: FnOnce(utoipa::openapi::OpenApi) -> O,
O: HttpServiceFactory + 'static,
{
let service = factory(self.1.clone());
Expand Down Expand Up @@ -352,10 +343,18 @@ where
/// Split this [`UtoipaApp`] into parts returning tuple of [`actix_web::App`] and
/// [`utoipa::openapi::OpenApi`] of this instance.
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()),
)
(self.0, self.1)
}

/// Converts this [`UtoipaApp`] into the wrapped [`actix_web::App`].
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
}
}

Expand Down
5 changes: 2 additions & 3 deletions utoipa-actix-web/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ where
cfg_fn(&mut service_config);

let other_paths = service_config.1.take();
openapi.paths.paths.extend(other_paths.paths);
openapi.paths.merge(other_paths);
let schemas = service_config.2.take();
let components = openapi
.components
Expand All @@ -139,8 +139,7 @@ where
let mut openapi = self.1.borrow_mut();
let other_paths = factory.paths();
factory.schemas(&mut schemas);

openapi.paths.paths.extend(other_paths.paths);
openapi.paths.merge(other_paths);
let components = openapi
.components
.get_or_insert(utoipa::openapi::Components::new());
Expand Down
3 changes: 2 additions & 1 deletion utoipa-actix-web/src/service_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ impl<'s> ServiceConfig<'s> {
{
let mut paths = self.1.take();
let other_paths = factory.paths();
paths.paths.extend(other_paths.paths);
paths.merge(other_paths);

let mut schemas = self.2.take();
factory.schemas(&mut schemas);
self.2.set(schemas);
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Changed

* Finalize actix-web utoipa bindings (https://github.com/juhaku/utoipa/pull/1160)
* Enhance no_recursion rule to apply also containers (https://github.com/juhaku/utoipa/pull/1144)

## 5.1.0 - Oct 16 2024
Expand Down
5 changes: 5 additions & 0 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
::actix_web::dev::HttpServiceFactory::register(#fn_ident, __config);
}
}
impl<'t> utoipa::__dev::Tags<'t> for #fn_ident {
fn tags() -> Vec<&'t str> {
#path_struct::tags()
}
}
impl utoipa::Path for #fn_ident {
fn path() -> String {
#path_struct::path()
Expand Down
1 change: 1 addition & 0 deletions utoipa/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ to look into changes introduced to **`utoipa-gen`**.

### Changed

* Finalize actix-web utoipa bindings (https://github.com/juhaku/utoipa/pull/1160)
* Update utoipa-gen version

## 5.1.0 - Oct 16 2024
Expand Down
2 changes: 1 addition & 1 deletion utoipa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
//!
//! |Flavor|Support|
//! |--|--|
//! |[actix-web](https://github.com/actix/actix-web)|Parse path, path parameters and query parameters, recognize request body and response body. See more at [docs][actix_path]|
//! |[actix-web](https://github.com/actix/actix-web)|Parse path, path parameters and query parameters, recognize request body and response body, [`utoipa-actix-web` bindings](https://docs.rs/utoipa-actix-web). See more at [docs][actix_path]|
//! |[axum](https://github.com/tokio-rs/axum)|Parse path and query parameters, recognize request body and response body, [`utoipa-axum` bindings](https://docs.rs/utoipa-axum). See more at [docs][axum_path]|
//! |[rocket](https://github.com/SergioBenitez/Rocket)| Parse path, path parameters and query parameters, recognize request body and response body. See more at [docs][rocket_path]|
//! |Others*| Plain `utoipa` without extra flavor. This gives you all the basic benefits listed below in **[Features](#features)** section but with little less automation.|
Expand Down
14 changes: 2 additions & 12 deletions utoipa/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::{
de::{Error, Expected, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use std::{fmt::Formatter, mem};
use std::fmt::Formatter;

use self::path::PathsMap;
pub use self::{
Expand Down Expand Up @@ -201,17 +201,7 @@ impl OpenApi {
}

if !other.paths.paths.is_empty() {
for (path, that) in &mut other.paths.paths {
if let Some(this) = self.paths.paths.get_mut(path) {
that.merge_operations(mem::take(this));
}
}
self.paths.paths.extend(other.paths.paths);

if let Some(other_paths_extensions) = other.paths.extensions {
let paths_extensions = self.paths.extensions.get_or_insert(Extensions::default());
paths_extensions.merge(other_paths_extensions);
}
self.paths.merge(other.paths);
};

if let Some(other_components) = &mut other.components {
Expand Down
37 changes: 28 additions & 9 deletions utoipa/src/openapi/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ impl Paths {
);
}
}

/// Merge _`other_paths`_ into `self`. On conflicting path the path item operations will be
/// merged into existing [`PathItem`]. Otherwise path with [`PathItem`] will be appended to
/// `self`. All [`Extensions`] will be merged from _`other_paths`_ into `self`.
pub fn merge(&mut self, other_paths: Paths) {
for (path, that) in other_paths.paths {
if let Some(this) = self.paths.get_mut(&path) {
this.merge_operations(that);
} else {
self.paths.insert(path, that);
}
}

if let Some(other_paths_extensions) = other_paths.extensions {
let paths_extensions = self.extensions.get_or_insert(Extensions::default());
paths_extensions.merge(other_paths_extensions);
}
}
}

impl PathsBuilder {
Expand Down Expand Up @@ -311,30 +329,31 @@ impl PathItem {
path_item
}

/// Merge all defined [`Operation`]s from given [`PathItem`] to `self`.
/// Merge all defined [`Operation`]s from given [`PathItem`] to `self` if `self` does not have
/// existing operation.
pub fn merge_operations(&mut self, path_item: PathItem) {
if path_item.get.is_some() {
if path_item.get.is_some() && self.get.is_none() {
self.get = path_item.get;
}
if path_item.put.is_some() {
if path_item.put.is_some() && self.put.is_none() {
self.put = path_item.put;
}
if path_item.post.is_some() {
if path_item.post.is_some() && self.post.is_none() {
self.post = path_item.post;
}
if path_item.delete.is_some() {
if path_item.delete.is_some() && self.delete.is_none() {
self.delete = path_item.delete;
}
if path_item.options.is_some() {
if path_item.options.is_some() && self.options.is_none() {
self.options = path_item.options;
}
if path_item.head.is_some() {
if path_item.head.is_some() && self.head.is_none() {
self.head = path_item.head;
}
if path_item.patch.is_some() {
if path_item.patch.is_some() && self.patch.is_none() {
self.patch = path_item.patch;
}
if path_item.trace.is_some() {
if path_item.trace.is_some() && self.trace.is_none() {
self.trace = path_item.trace;
}
}
Expand Down

0 comments on commit fc45782

Please sign in to comment.