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

Axum bindings for utoipa - better path registration for axum #991

Closed
juhaku opened this issue Aug 15, 2024 · 6 comments · Fixed by #1004
Closed

Axum bindings for utoipa - better path registration for axum #991

juhaku opened this issue Aug 15, 2024 · 6 comments · Fixed by #1004
Labels
enhancement New feature or request

Comments

@juhaku
Copy link
Owner

juhaku commented Aug 15, 2024

Description

Currently with axum one needs to do duplicate work when adding utoipa #[utoipa::path] macros to the OpenApi and then registering those handlers for axum::Router.

#[derive(OpenApi)]
#[openapi(
    nest(
        (path = "/api/v1/todos", api = todo::TodoApi)
    ),
    tags(
        (name = "todo", description = "Todo items management API")
    )
)]
struct ApiDoc;

[derive(OpenApi)]
#[openapi(
    paths(list_todos, search_todos, create_todo, mark_done, delete_todo,),
    components(schemas(Todo, TodoError))
)]
pub(super) struct TodoApi;

let app = Router::new()
    .nest("/api/v1/todos", todo::router());
mod todo {
    pub(super) fn router() -> Router {
        let store = Arc::new(Store::default());
        Router::new()
            .route("/", routing::get(list_todos).post(create_todo))
            .route("/search", routing::get(search_todos))
            .route("/:id", routing::put(mark_done).delete(delete_todo))
            .with_state(store)
    }
}

Proposed Solution

Solution currently under experimenting makes huge improvement to the current one by allowing users directly use custom OpenApiRouter that handles #[utoipa::path] attribute macro path registration behind the scenes and registers the route handlers on the single go. This completely makes OpenApi derive macro redundant but can be used if needed together with the router. The benefit is that there will be no more duplicate work and handlers can be registered once. This completely makes OpenApi derive macro redundant in terms of path registration but schemas still need to be registered the old way or directly to the OpenApi type.

let user_router: OpenApiRouter = OpenApiRouter::new()
    .routes(utoipa_axum::get(search_user))
    .routes(utoipa_axum::get(get_user).post_path(post_user).delete_path(delete_user));

let customer_router: OpenApiRouter = OpenApiRouter::new()
    .routes(
        utoipa_axum::get(get_customer)
            .post_path(post_customer)
            .delete_path(delete_customer),
    )
    .routes(utoipa_axum::get(search_customer));

let mut router = OpenApiRouter::new()
    .nest("/api/user", user_router)
    .nest("/api/customer", customer_router)
    .route("/", get(root));
let api = router.to_openapi();
let router: axum::Router = router.into();

Relates #624 Relates #662 Relates #537 Relates #394

@darklajid
Copy link

Just to clarify: The proposed solution doesn't just do away with the (redundant) paths, but also doesn't mention the (also ideally redundant) schema declaration. Does this proposal include automatic schema types derivation?

@juhaku
Copy link
Owner Author

juhaku commented Aug 16, 2024

Does this proposal include automatic schema types derivation?

Not yet at least, though getting away the redundant schema declarations would be also welcome improvement. However I am most likely not going to do it in same PR as this OpenApiRouter thing.

@juhaku juhaku added the blocked Work cannot proceed because some reason label Aug 22, 2024
@juhaku juhaku moved this from In Progress to Todo in utoipa kanban Aug 22, 2024
@juhaku
Copy link
Owner Author

juhaku commented Aug 22, 2024

It seems to be relatively hard to create Handler manually for custom type. It works half a time for functions that are simple but when extensions and other types of FromRequest will be added to function arguments the handler implementation does not work. It would require significant deep knowledge of inner parts of axum framework in order to implement necessary variations for Handler trait.

So far, unless this is solved someway or the other this issue will stay blocked.

juhaku added a commit that referenced this issue Aug 25, 2024
This PR adds a new crate `utiopa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new OpenApiRouter what wraps OpenApi and axum
Router which provides passthrough implementation for most of the axum
Router methods and collects and combines the OpenApi from registered
routes. Routes registred only via `routes!()` macro will get added to
the OpenApi.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the OpenApiRouter.

Fixes #991
juhaku added a commit that referenced this issue Aug 25, 2024
This PR adds a new crate `utiopa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new OpenApiRouter what wraps OpenApi and axum
Router which provides passthrough implementation for most of the axum
Router methods and collects and combines the OpenApi from registered
routes. Routes registred only via `routes!()` macro will get added to
the OpenApi.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the OpenApiRouter.

Fixes #991
@juhaku juhaku removed the blocked Work cannot proceed because some reason label Aug 25, 2024
@juhaku juhaku moved this from Todo to In Progress in utoipa kanban Aug 25, 2024
juhaku added a commit that referenced this issue Aug 25, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Fixes #991
@juhaku juhaku linked a pull request Aug 25, 2024 that will close this issue
juhaku added a commit that referenced this issue Aug 25, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Fixes #991
juhaku added a commit that referenced this issue Aug 26, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Fixes #991
juhaku added a commit that referenced this issue Aug 26, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Example of supported sytanx.
```rust
let user_router: OpenApiRouter = OpenApiRouter::new()
    .routes(routes!(search_user))
    .routes(routes!(get_user, post_user, delete_user));
```

Fixes #991
@juhaku
Copy link
Owner Author

juhaku commented Aug 26, 2024

Now there is a working implementation for axum's utoipa bindings leveraging macros when registering handlers for the router. Now the code above can be written like this with the implementation in PR #1004

#[derive(OpenApi)]
#[openapi(
    tags(
        (name = "todo", description = "Todo items management API")
    )
)]
struct ApiDoc;

let mut todo_router = todo::router();
let todo_api = todo_router.to_openapi();
let api = ApiDoc::openapi().nest("/api/v1/todos", todo_api);
let app = Router::new()
    .nest("/api/v1/todos", todo_router.into());

mod todo {
    [derive(OpenApi)]
    #[openapi(
        components(schemas(Todo, TodoError))
    )]
    pub(super) struct TodoApi;

    pub(super) fn router() -> OpenApiRouter {
        let store = Arc::new(Store::default());
        OpenApiRouter::with_openapi(TodoApi::openapi())
            .routes(routes!(list_todos, create_todo, delete_todo, mark_done))
            .routes(routes!(search_todo))
            .with_state(store)
    }
}

Or even better? We can get rid of the duplicate nest call by only using the OpenApiRouter.

let app = OpenApiRouter::new()
    .nest("/api/v1/todos", todo::router());
let api  = app.to_openapi();
let app: axum::Router = app.into();

juhaku added a commit that referenced this issue Aug 26, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Example of supported sytanx.
```rust
let user_router: OpenApiRouter = OpenApiRouter::new()
    .routes(routes!(search_user))
    .routes(routes!(get_user, post_user, delete_user));
```

Fixes #991
juhaku added a commit that referenced this issue Aug 26, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Example of supported sytanx.
```rust
let user_router: OpenApiRouter = OpenApiRouter::new()
    .routes(routes!(search_user))
    .routes(routes!(get_user, post_user, delete_user));
```

Fixes #991
juhaku added a commit that referenced this issue Aug 26, 2024
This PR adds a new crate `utoipa-axum` which provides bindings between
`axum` and `utoipa`. It aims to blend as much as possible to the
existing philosophy of axum way of registering handlers.

This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum
`Router` which provides passthrough implementation for most of the axum
Router methods and collects and combines the `OpenApi` from registered
routes. Routes registred only via `routes!()` macro will get added to
the `OpenApi`.

Also this commit introduces `routes!()` macro which collects axum
handlers annotated with `#[utoipa::path()]` attribute macro to single
paths intance which is then provided to the `OpenApiRouter`.

Example of supported sytanx.
```rust
let user_router: OpenApiRouter = OpenApiRouter::new()
    .routes(routes!(search_user))
    .routes(routes!(get_user, post_user, delete_user));
```

Fixes #991
@github-project-automation github-project-automation bot moved this from In Progress to Done in utoipa kanban Aug 26, 2024
@oxalica
Copy link
Contributor

oxalica commented Oct 3, 2024

This utility reduces some boilerplate, but it makes utoipa (and its dependency closure) a required dependency in the production server. How can I make utoipa an optional dependency so that the OAPI generation code is not included in the final server binary?

@juhaku
Copy link
Owner Author

juhaku commented Oct 4, 2024

Hm could you elaborate a bit more about your concern. Like in my understanding, in order to achieve this kind of "deep" bindings between the two (utoipa and axum) the axum here needs to be a normal dependency in utoipa-axum crate. Rather what is implemented here in utoipa-axum should be done in axum itself but since it is what it is it is done here.

Nonetheless as I see it, there is no way around it. This indeed ties the two together and to reverse that I am not sure how it could be done that it still would be optionally pluggable to the axum and axum would still recognize the paths registered by utoipa. Probably the hard fact is not to use this in such cases. E.g. if there is a certain build step where the OpenAPI doc is generated beforehand and then just the json is passed to the production server.

Even if there was some sort of generic Router that can be used to register paths the same registration needed to happen in axum as well in the production server.

@juhaku juhaku moved this from Done to Released in utoipa kanban Oct 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Released
Development

Successfully merging a pull request may close this issue.

3 participants