Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalize actix-web utoipa bindings #1160

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading