Giter VIP home page Giter VIP logo

aide's People

Contributors

0xdeafbeef avatar adhalianna avatar arlyon avatar gagbo avatar ilya-zlobintsev avatar imp avatar jakkusakura avatar johannescpk avatar leonkowarschickkenbun avatar marclave avatar markdingram avatar mfelsche avatar nikarh avatar oknozor avatar r3bu1ld3r avatar randoooom avatar stormshield-kg avatar svix-andor avatar tamasfe avatar wicpar avatar yassun7010 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aide's Issues

Provide some way to add Parameters manually for `nest` calls

In our application, we have a nested axum route router with a Path Parameter, like so:

.nest("/foo/:id",
  component
    .routes()
    .layer(axum::middleware::map_request(my_extractor)
)

which allows accessing some data from our state through that extractor in the component.routes().
It'd be very nice if there was some way to have a nest_with, or some way to mutably access the api_routes defined in an ApiRouter, such that I could manually add the parameter information to this nested router.

Router not initializing when there are more than 10 `api_route()`

Is there a limitation on the number of api_routes for an ApiRouter? There's no compiler error. ApiRouter just doesn't return a router when I have more than 10 api_routes.

// This works
    ApiRouter::new()
        .api_route("/api/healthcheck1", get(healthcheck::controller))
        .api_route("/api/healthcheck2", get(healthcheck::controller))
        .api_route("/api/healthcheck3", get(healthcheck::controller))
        .api_route("/api/healthcheck4", get(healthcheck::controller))
        .api_route("/api/healthcheck5", get(healthcheck::controller))
        .api_route("/api/healthcheck6", get(healthcheck::controller))
        .api_route("/api/healthcheck7", get(healthcheck::controller))
        .api_route("/api/healthcheck8", get(healthcheck::controller))
        .api_route("/api/healthcheck9", get(healthcheck::controller))
        .api_route("/api/healthcheck10", get(healthcheck::controller))
        .route("/api.json", get(serve_docs))
        .with_state(state)
        .finish_api_with(&mut api, api_docs)
        .layer(Extension(Arc::new(api)))
// This does not work and does not throw a compiler error
    ApiRouter::new()
        .api_route("/api/healthcheck1", get(healthcheck::controller))
        .api_route("/api/healthcheck2", get(healthcheck::controller))
        .api_route("/api/healthcheck3", get(healthcheck::controller))
        .api_route("/api/healthcheck4", get(healthcheck::controller))
        .api_route("/api/healthcheck5", get(healthcheck::controller))
        .api_route("/api/healthcheck6", get(healthcheck::controller))
        .api_route("/api/healthcheck7", get(healthcheck::controller))
        .api_route("/api/healthcheck8", get(healthcheck::controller))
        .api_route("/api/healthcheck9", get(healthcheck::controller))
        .api_route("/api/healthcheck10", get(healthcheck::controller))
        .api_route("/api/healthcheck11", get(healthcheck::controller))
        .route("/api.json", get(serve_docs))
        .with_state(state)
        .finish_api_with(&mut api, api_docs)
        .layer(Extension(Arc::new(api)))

[Community poll] Hellish dependencies and a possible breaking change to fix it all

Due to the structure of Rust, and the still missing feture in stable of default impls that would allow to fix this issue entirely, this crate is constrained on many levels.

In theory for everything to be nice and easy, users should be able to implement OperationInput and OperationOutput on types as needed. This is no issue for types in one's own crate, however as soon as a dependency like axum and its many utility crates like axum-sqlx-tx need one of those traits it needs to be implemented either in this crate, or their crate.

Since only one version of crates can be implemented at once i am forced to use a fork of this library. It's possible quite a few of you do too.

Many of those types simply have no implementation for generating docs, and those who do need one must be implementable by the user's crate even on foreign types

I am proposing two new structs: NoDoc<T>(pub T) and WithDoc<T, D>(pub T, PhantomData<D>).
NoDoc<T>(pub T) simpy wraps a type and implements the traits like IntoResponse or FromRequestParts, etc... as is if T implements them and adds no doc, so it would be very useful for axum-sqlx-tx

async fn get_axum_request(NoDoc(mut tx): NoDoc<Tx<Postgres>>)  {}

WithDoc<T, D>(pub T, PhantomData<D>) is the same, but has D which is a user provided struct that proxies OperationInput and OperationOutput for T.

struct MyDoc;

impl OperationInput for Mydoc {
 ...
}

async fn get_axum_request(WithDoc(mut tx, _): WithDoc<Tx<Postgres>, MyDoc>)  {}

Other utilities like Deref Into AsRef, etc... will be provided, please tell me if there are any you with to see.

This will allow the separation and or deletion of the many unmaintainable feature gates into separate crates that can allow for better multi-version support

Thanks to @tamasfe i can make these changes, however such a fundamental change in how the dependencies are managed this is completely breaking for everyone and as such i would like the input of as many people that use this crate as possible.

@tamasfe i know you don't have the time, but since this is your crate i don't want to make big changes without your approval.

Return Json error with status code

Thanks for this awesome project! The examples are a bit lacking on how to work with returning errors and status code.

For example I have a handler:

#[axum::debug_handler]
pub async fn create_user(
    State(pool): State<PgPool>,
    Json(user): Json<CreateUserInput>,
) -> Result<(StatusCode, Json<CreateUserOutput>), (StatusCode, Json<CreateUserOutputError>)> {
}

This raises errors. I could be doing something wrong and would be glad if someone could guide me.

`already borrowed: BorrowMutError` on `GEN_CTX` when trying to add redoc to axum

I'm running into an issue trying to add redoc to axum, when executing the program it ends up panicking.

OS: MacOS Ventura 13.0.1 - Darwin 22.1.0 Darwin Kernel Version 22.1.0: Sun Oct 9 20:15:09 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T6000 arm64
RUSTC: rustc 1.65.0 (897e37553 2022-11-02)

use std::net::SocketAddr;

use aide::{openapi::{OpenApi, Info}, axum::{ApiRouter, IntoApiResponse}, redoc::Redoc};
use axum::{Router, Extension, Json};
use aide::axum::routing::get;


#[tokio::main]
async fn main() {
    let router = make_router();

    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(router.into_make_service())
        .await.unwrap();
}


fn make_router() -> Router {
    let mut api = OpenApi {
        info: Info {
            description: Some("Some API".to_string()),
            ..Info::default()
        },
        ..OpenApi::default()
    };

    let router = ApiRouter::new()
        .api_route("/ping", get(pong))
        .route("/redoc", Redoc::new("/api.json").axum_route())
        .route("/api.json", get(serve_api));

    let router = router.finish_api(&mut api);
    router
}

async fn pong() -> &'static str {
    "pong"
}

async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoApiResponse {
    Json(api)
}

I have created a proof of concept which can be viewed here: https://github.com/elertan/axum-aide-redoc-already-borrowed-gen-ctx-poc

How to use with Result types?

If I try to use a result type such as:

async fn get_thing(
    State(state): State<RouterState>,
    Query(req): Query<Req>,
) -> Result<impl IntoApiResponse, StatusCode> {
    let res = state.service
        .get_all(&req.term, &req.cursor)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .map(PlantResponse::from_model);

    Ok(Json(res))
}

I get this error:

   --> src/user_routes.rs:29:40
    |
29  |         .route("/get_thing", get_with(get_thing, default_docs))
    |                               -------- ^^^^^^^^^^ the trait `OperationHandler<_, _>` is not implemented for `fn(axum::extract::State<RouterState>, axum::extract::Query<Req>) -> impl std::future::Future<Output = Result<impl IntoApiResponse, hyper::StatusCode>> {user_routes::get_thing}`
    |                               |
    |                               required by a bound introduced by this call

How to require multiple securitySchemas?

https://swagger.io/docs/specification/authentication/api-keys/
As of specification, you can define multiple apiKeys to work in a group.

I'm looking for a way to specify the same with aide, but I can't find it.

Let's say I have 2 apiKeys, like:

components:
  securitySchemes:
    bar1:
      type: apiKey
      in: header
      name: X-BAR1
    bar2:
      type: apiKey
      in: header
      name: X-BAR2

OpenApi::security field is a Vec<IndexMap<String, Vec<String>>> and setting it like

OpenApi {
  components: Some(Components {
    security_schemes: [
      ("bar1".to_string(), ReferenceOr::Item(SecurityScheme::ApiKey {
        name: "X-BAR1".to_string(),
        location: ApiKeyLocation::Header,
        description: None,
        extensions: [].into(),
      })), ("bar2".to_string(), ReferenceOr::Item(SecurityScheme::ApiKey {
        name: "X-BAR2".to_string(),
        location: ApiKeyLocation::Header,
        description: None,
        extensions: [].into(),
      }))
    ].into(),
    ..openapi::Components::default()
  }),
  security: vec![[("foo".to_owned(), vec!["bar1".to_owned(), "bar2".to_owned()])].into()],
  ..OpenApi::default()
}

produces

components:
  securitySchemes:
    bar1:
      type: apiKey
      in: header
      name: X-BAR1
    bar2:
      type: apiKey
      in: header
      name: X-BAR2
security:
- foo:
  - bar1
  - bar2

that it's not what I want.

TransformOperation::security_requirement accepts only an &str, passing "foo" from previous example, inside the method, produces

      security:
      - foo: []

but redocs doesn't recognizes it.

TransformOperation::security_requirement_scopes accepts a &str and anything that can be used as an Iterator of String, passing "foo" and ["bar1", "bar2"] from previous example, inside the method, produces

      security:
      - foo:
        - bar1
        - bar2

What's the correct approach here? Is it a crate limitation?

axum JWT example does not build with aide

Hi all,

I'm pretty new to axum and aide, so apologies if I am misunderstanding anything, or if my question doesn't make sense.

I am starting a project that uses axum and aide, and some of the endpoints use JWTs for authn/z. I tried following axum's jwt example, and when using axum's Router, route and get/post everything works as intended, but when I try changing the code to use aide's ApiRouter, api_route and get/post I get "trait bound is not satisfied" errors (see below for the full error).

Am I doing something wrong here? Am I missing something?

Full error message:

error[E0277]: the trait bound `fn(axum::Json<AuthPayload>) -> impl Future<Output = Result<axum::Json<AuthBody>, AuthError>> {authorize}: OperationHandler<_, _>` is not satisfied
   --> jwt/src/main.rs:71:39
    |
71  |         .api_route("/authorize", post(authorize));
    |                                  ---- ^^^^^^^^^ the trait `OperationHandler<_, _>` is not implemented for fn item `fn(axum::Json<AuthPayload>) -> impl Future<Output = Result<axum::Json<AuthBody>, AuthError>> {authorize}`
    |                                  |
    |                                  required by a bound introduced by this call
    |
note: required by a bound in `aide::axum::routing::post`
   --> /Users/XXXXX/.cargo/registry/src/github.com-1ecc6299db9ec823/aide-0.10.0/src/axum/routing.rs:341:1
    |
341 | method_router_top_level!(post, post_with);
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `aide::axum::routing::post`
    = note: this error originates in the macro `method_router_top_level` (in Nightly builds, run with -Z macro-backtrace for more info)

Diff from tokio-rs/axum@c18ff9d for reproduction:

diff --git a/examples/jwt/Cargo.toml b/examples/jwt/Cargo.toml
index a18eb6ec78...f57616740e 100644
--- a/examples/jwt/Cargo.toml
+++ b/examples/jwt/Cargo.toml
@@ -5,6 +5,7 @@
 publish = false

 [dependencies]
+aide = { version = "0.10", features = ["axum", "macros"]}
 axum = { path = "../../axum", features = ["headers"] }
 headers = "0.3"
 jsonwebtoken = "8.0"
diff --git a/examples/jwt/src/main.rs b/examples/jwt/src/main.rs
index 4ee20d4bf7...db2ebed14b 100644
--- a/examples/jwt/src/main.rs
+++ b/examples/jwt/src/main.rs
@@ -6,14 +6,17 @@
 //! JWT_SECRET=secret cargo run -p example-jwt
 //! ```

+use aide::axum::{
+    routing::{get, post},
+    ApiRouter,
+};
 use axum::{
     async_trait,
     extract::{FromRequestParts, TypedHeader},
     headers::{authorization::Bearer, Authorization},
     http::{request::Parts, StatusCode},
     response::{IntoResponse, Response},
-    routing::{get, post},
-    Json, RequestPartsExt, Router,
+    Json, RequestPartsExt,
 };
 use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
 use once_cell::sync::Lazy;
@@ -63,9 +66,9 @@
         .with(tracing_subscriber::fmt::layer())
         .init();

-    let app = Router::new()
-        .route("/protected", get(protected))
-        .route("/authorize", post(authorize));
+    let app = ApiRouter::new()
+        .api_route("/protected", get(protected))
+        .api_route("/authorize", post(authorize));

redoc breaks when there is Json<Struct>

The following code will break redoc

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Request {
}

pub async fn request(
    Json(request): Json<Request>,
) -> impl IntoApiResponse {
    (StatusCode::OK, Json(())).into_response()
}
  ApiRouter::new()
        .api_route("/foo", post(request))

I got

Something went wrong...
Invalid reference token: definitions
Stack trace
Error: Invalid reference token: definitions
    at o.get (http://localhost:3300/redoc:18:261574)
    at Yc (http://localhost:3300/redoc:49:71447)
    at eu (http://localhost:3300/redoc:49:76077)
    at nu.generateExample (http://localhost:3300/redoc:49:78343)
    at new nu (http://localhost:3300/redoc:49:77771)
    at http://localhost:3300/redoc:49:78906
    at Array.map (<anonymous>)
    at new au (http://localhost:3300/redoc:49:78877)
    at new su (http://localhost:3300/redoc:49:79673)
    at get requestBody (http://localhost:3300/redoc:49:82742)

ReDoc Version: 2.0.0
Commit: 5fb4daa

Redoc feature incompatible with axum's state

Hi,

I encountered an issue when trying to use the Redoc feature with axum's state. I could reproduce the issue by adding state to the Redoc example from docs.rs:

use std::sync::Arc;
use aide::{
    axum::{
        routing::{get, post},
        ApiRouter, IntoApiResponse,
    },
    openapi::{Info, OpenApi},
    redoc::Redoc,
};
use axum::{Extension, Json};
use schemars::JsonSchema;
use serde::Deserialize;

#[derive(Deserialize, JsonSchema)]
struct User {
    name: String,
}

async fn hello_user(Json(user): Json<User>) -> impl IntoApiResponse {
    format!("hello {}", user.name)
}

async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoApiResponse {
    Json(api)
}

// Example state
struct AppState {
    some_state: String,
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        some_state: String::from("test"),
    });
    let app = ApiRouter::new()
        .route("/redoc", Redoc::new("/api.json").axum_route())
        .api_route("/hello", post(hello_user))
        .route("/api.json", get(serve_api))
        // Added state
        .with_state(state);

    let mut api = OpenApi {
        info: Info {
            description: Some("an example API".to_string()),
            ..Info::default()
        },
        ..OpenApi::default()
    };

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(
            app
                .finish_api(&mut api)
                .layer(Extension(api))
                .into_make_service(),
        )
        .await
        .unwrap();
}

This results in:

error[E0308]: mismatched types
   --> src/main.rs:50:21
    |
50  |         .with_state(state);
    |          ---------- ^^^^^ expected `()`, found struct `Arc`
    |          |
    |          arguments to this function are incorrect
    |
    = note: expected unit type `()`
                  found struct `Arc<AppState>`
note: associated function defined here
   --> /home/arctic-alpaca/.cargo/registry/src/github.com-1ecc6299db9ec823/aide-0.9.0/src/axum/mod.rs:272:12
    |
272 |     pub fn with_state<S2>(self, state: S) -> ApiRouter<S2, B> {
    |            ^^^^^^^^^^

I think this could be related to what the axum docs describe here, but I'm not sure.

Feature request: support for axum-login (and consequently axum-sessions)

Route handlers that make use of axum-sessions::[Readable|Writable]Session or axum-login::AuthContext do not implement aide::OperationHandler.

error[E0277]: the trait bound `fn(ReadableSession) -> impl Future<Output = impl IntoApiResponse> {get_session_id}: OperationHandler<_, _>` is not satisfied

Same error with the other extractors.

Path summary combines docs for each method

I would expect this to document the post and get requests separately:

ApiRouter::new()
    .api_route_with("/files", post(upload_file), |x| x.summary("Upload a file"))
    .api_route_with("/files", get(list_files), |x| x.summary("Get a list of files."))

Instead, they get concatenated into a single summary at the URI level, delimited by \n

"/files": {
  "summary": "Upload a file.\nGet a list of files.",
  "get": {
    "parameters": [
    ... (no summary)

These docs are pretty outdated, but it's the first thing I could find: https://swagger.io/docs/specification/2-0/describing-responses/#response-that-returns-a-file

Anyway, they show that you can include a summary for each method. I'm guessing the same applies to OpenAPI 3.1

  /ping:
    get:
      summary: Checks if the server is alive.
      responses:

When I have some more free time, I can dig into the 3.1 docs, and I can write a complete reproducing example. Let me know if that would be helpful.

Better handle jsonschema -> serde issues when converting ints

Hi! I am testing out this library and noticed that we don't get any error message when passing in int-like floats.

{ "number": 2.9 }

{
    "error": "request schema validation failed",
    "schema_validation": [
        {
            "error": "2.3 is not of type \"integer\"",
            "instanceLocation": "/number",
            "keywordLocation": "/properties/number/type"
        }
    ]
}
{ "number": 3.0 }

{
    "error": "invalid request"
}

It seems that jsonschema and serde_json differ in their definition of what a valid int looks like. Since json blurs the lines between floats and integers, the representation 3.0 passes the jsonschema validation (since 3.0 is is an integer https://json-schema.org/understanding-json-schema/reference/numeric.html) despite being rejected by serde_json (https://docs.rs/serde_json/latest/src/serde_json/number.rs.html#94).

How can we handle this? At the very least we could maybe intercept and use serde_path_to_error to potentially expose the serde rejection in that case? Or potentially adjust the jsonschema to reject technically-valid json integers with a decimal.

This doesn't really fall into the domain of schemars, as it is (correctly) producing a schema that is as close to serde as possible, and jsonschema is (correctly) allowing decimal numbers with a 0 component pass, but serde is rejecting it, since its definition of an int differs from that of jsonschema. That leads us to this crate, that sits between a rock and a hard place so-to-speak, attempting to marry the three.

serde rename wrong in openapi doc output

Field in Responsemarked as #[serde(rename(deserialize="abc"))] output abc in openapi, that's wrong.
It should be serialize.
Not sure it's aide's bug or JsonSchema's bug.

Docs examples for aide::axum do not compile

It looks like the minimal example given here does not compile due to Json not implementing OperationOutput / OperationInput. Is there a minimal example to refer to other than the one in https://github.com/tamasfe/aide/tree/master/examples/example-axum ? That seems to be implementing a custom Json extractor so I'm guessing that would need to be done for the other build in extractors from axum such as Query?

I'm really interested in making use of aide but it's looking like I need to provide new type wrappers for ever axum extractor I want to make use of which is surprising...

Hopefully I'm just misunderstanding something ๐Ÿ˜…

Add a bunch of generic tuple implementations of OperationOutput

Hello,

Thanks for the crate it seems to have everything we need and I'm currently testing a migration to see if everything would work fine for us.

I'd like to know if you could consider adding a lot more tuple arities to the OperationOutput trait implementors, as I have a route that returns 3-tuples now, and it gets boilerplate-y to deal with those, re-adding auto-matically derived traits and having the extra (un)wrapping calls to make at usage sites:

/// This fails
static_assertions::assert_impl_all!((StatusCode, HeaderMap<HeaderValue>, StreamBody<ByteStream>): aide::operation::OperationOutput);

/// This works, but I have to add the extra new type below and change all my APIs with wrappers/into_inner calls
static_assertions::assert_impl_all!(ThreePartResponse<StatusCode, HeaderMap<HeaderValue>, StreamBody<ByteStream>: aide::operation::OperationOutput);

pub struct ThreePartResponse<T1, T2, T3>(T1, T2, T3);
impl<T1, T2, T3> axum::response::IntoResponse for ThreePartResponse<T1, T2, T3>
where
    T1: axum::response::IntoResponseParts,
    T2: axum::response::IntoResponseParts,
    T3: axum::response::IntoResponse,
{
    fn into_response(self) -> axum::response::Response {
        self.into_inner().into_response()
   }
}

impl<T1, T2, T3> aide::OperationOutput for ThreePartResponse<T1, T2, T3> {
    type Inner = std::convert::Infallible;
}
impl<T1, T2, T3> ThreePartResponse<T1, T2, T3> {
    pub fn into_inner(self) -> (T1, T2, T3) {
        (self.0, self.1, self.2)
    }
}

Cheers, and enjoy end of year festivities,

Gerry

Reduce constrain on `bytes` dependency

Currently the bytes dependency is fixed to exactly 1.2.1 when there is no actual reason for it. Using another library with a hard requirement like octocrab causes a conflict between the two versions. Since the library only implements a trait, the version requirement could be looser.

MissingPointerError: Token "AppError" does not exist in example

great library! I noticed for the example swagger spec, it renders the spec below. However, the AppError response is undefined in the rendering of Redocly + Swagger UI since AppError doesnt exist inside of the Schemas for the OAS spec

there's also more errors on https://editor.swagger.io
image

image image
openapi: 3.1.0
info:
  title: Aide axum Open API
  summary: An example Todo application
  description: |
    # Todo API

    A very simple Todo server with documentation.

    The purpose is to showcase the documentation workflow of Aide rather
    than a correct implementation.
  version: ''
paths:
  /todo/:
    get:
      description: List all Todo items.
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoList'
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
    post:
      description: Create a new incomplete Todo item.
      requestBody:
        description: New Todo details.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewTodo'
        required: true
      responses:
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoCreated'
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /todo/{id}:
    get:
      description: Get a single Todo item.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoItem'
              example:
                complete: false
                description: fix bugs
                id: 00000000-0000-0000-0000-000000000000
        '404':
          description: todo was not found
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
    delete:
      description: Delete a Todo item.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '204':
          description: The Todo has been deleted.
        '404':
          description: The todo was not found
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /todo/{id}/complete:
    put:
      description: Complete a Todo.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '204':
          description: no content
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /docs/:
    get:
      description: This documentation page.
      responses:
        '200':
          description: HTML content
          content:
            text/html:
              schema:
                type: string
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
      security:
        - ApiKey: []
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Auth-Key
      description: A key that is ignored.
  schemas:
    NewTodo:
      description: New Todo details.
      type: object
      required:
        - description
      properties:
        description:
          description: The description for the new Todo.
          type: string
    SelectTodo:
      type: object
      required:
        - id
      properties:
        id:
          description: The ID of the Todo.
          type: string
          format: uuid
    TodoCreated:
      description: New Todo details.
      type: object
      required:
        - id
      properties:
        id:
          description: The ID of the new Todo.
          type: string
          format: uuid
    TodoItem:
      description: A single Todo item.
      type: object
      required:
        - complete
        - description
        - id
      properties:
        complete:
          description: Whether the item was completed.
          type: boolean
        description:
          description: The description of the item.
          type: string
        id:
          type: string
          format: uuid
    TodoList:
      type: object
      required:
        - todo_ids
      properties:
        todo_ids:
          type: array
          items:
            type: string
            format: uuid
tags:
  - name: todo
    description: Todo Management

Tags for release

@tamasfe I seem to have forgotten to deploy tags for the updates, can you describe the process so that i can try to fix that ?
I know there is the git cliff that i added documentation for in contributing, i might have gotten that wrong, but i have no idea how to work that in with the github tags.

How to show variant name of tagged enum?

My endpoint returns a tagged enum

enum Foo {
Price(FooPrice),
...
}

However, in redoc it's displayed as many object. I have to click through them to see the details. I checked the generated openai.json, each variant is inlined within the enum, rather than taking a reference to #/components/schemas/Foo_FooPrice

I wonder if there's a way to let variants display with their name, rather than object?
image

insertion failed due to conflict with previously registered route: /*__private__axum_fallback'

Hi.

Upgrading to latest axum in my project has broken schema generation.

I get the following panic:

hread 'web::routes::tests::generate_open_api_schema' panicked at 'Invalid route "/*__private__axum_fallback": insertion failed due to conflict with previously registered route: /*__private__axum_fallback', /Users/myuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/aide-0.11.0/src/axum/mod.rs:453:35
stack backtrace:
   0: rust_begin_unwind
             at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library/std/src/panicking.rs:575:5
   1: core::panicking::panic_fmt
             at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library/core/src/panicking.rs:64:14
   2: axum::routing::Router<S,B>::nest
             at /Users/myuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.6.18/src/routing/mod.rs:164:13
   3: aide::axum::ApiRouter<S,B>::nest
             at /Users/myuser/.cargo/registry/src/index.crates.io-6f17d22bba15001f/aide-0.11.0/src/axum/mod.rs:453:23
   4: do_ddns::web::routes::get_pure_router_and_open_api
             at ./src/web/routes.rs:70:24
   5: do_ddns::web::routes::tests::generate_open_api_schema
             at ./src/web/routes.rs:126:24
   6: do_ddns::web::routes::tests::generate_open_api_schema::{{closure}}
             at ./src/web/routes.rs:125:35
   7: core::ops::function::FnOnce::call_once
             at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library/core/src/ops/function.rs:250:5
   8: core::ops::function::FnOnce::call_once
             at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library/core/src/ops/function.rs:250:5

Code in question: https://github.com/alcroito/digitalocean-dyndns/blob/master/crates/dyndns/src/web/routes.rs#L70

New axum version: 0.6.18
Previously working one: 0.6.12.

I haven't had time to make a small repro yet, but posting for visibility.

Route layers for route functions

First of all, thank you so much for this great library! I enjoy the code first experience with auto generated docs.

Currently for aide::axum::routing::{delete, get, patch, put, post} and other routing functions it is impossible to set route layers.
For example this code is valid for axum::routing::{delete, get, patch, put, post} :

let routes = Router::new()
        .route(
            "/",
            get(expenses_handlers::find_many)
                .route_layer(cache_layer.clone())
                .route_layer(auth_layer.verify(vec![Roles::Admin, Roles::Customer])),
        )
        .route(
            "/:id",
            get(expenses_handlers::find_one)
                .route_layer(cache_layer)
                .route_layer(auth_layer.verify(vec![Roles::Admin, Roles::Customer])),
        )

but with aide::axum::routing functions there is no .route_layer() function, only .layer() function is available, but it is impossible to use multiple .layer() function for a specific route. So it would be great if this code become valid:

let routes = ApiRouter::new()
        .api_route(
            "/",
            get(expenses_handlers::find_many)
                .route_layer(cache_layer.clone()) // TODO Find out how to add multiple layers
                .route_layer(auth_layer.verify(vec![Roles::Admin, Roles::Customer])),
        )
        .api_route(
            "/:id",
            get(expenses_handlers::find_one)
                .route_layer(cache_layer) // TODO Find out how to add multiple layers
                .route_layer(auth_layer.verify(vec![Roles::Admin, Roles::Customer])),
        )

But currently following errors is received:

error[E0599]: no method named `route_layer` found for struct `ApiMethodRouter` in the current scope
  --> src/api/expenses/mod.rs:47:18
   |
46 | /             get(expenses_handlers::find_many)
47 | |                 .route_layer(cache_layer.clone())
   | |                 -^^^^^^^^^^^ method not found in `ApiMethodRouter<_, _>`
   | |_________________|
   | 

error[E0599]: no method named `route_layer` found for struct `ApiMethodRouter` in the current scope
  --> src/api/expenses/mod.rs:53:18
   |
52 | /             get(expenses_handlers::find_one)
53 | |                 .route_layer(cache_layer)
   | |                 -^^^^^^^^^^^ method not found in `ApiMethodRouter<_, _>`
   | |_________________|

Feature request: Add support for TypedHeader extractor

There is no implementation currently of OperationInput (didn't check OperationOutput yet) for TypedHeader extractor. That means we cannot use those handler in api_routes as far as I understand.

I didn't check too much the spec of OpenAPI, but if Iโ€ฏwere to make a PR for this, I would need to actually implement the optional methods of the trait so that it appears in the schema right?

`ApiRouter` has derived `Clone` bounds on generics

I'm trying out aide, and loving it so far. I've run into an issue using axum_test_helper, which exposes a test client which significantly helps when writing tests.

The issue I've run into is roughly:

  • TestClient::new(router) requires router is Clone
  • ApiRouter is not Clone, because the Body (the default for the B parameter) is not Clone, because deriving Clone adds a Clone bound on all generic parameters
  • Router (from axum itself) doesn't derive Clone, but has a manual impl without such requirements:
impl<S, B> Clone for Router<S, B> {
   fn clone(&self) -> Self {
     // ...
   }
}

I'm not familiar with the internals of aide, but from a brief look, it doesn't seem like this requirement is necessary (moreover, it probably shouldn't be if it is, since IMO an openapi generator shouldn't be intercepting actual requests and cloning them).

Would you be interested in a PR that removes this limitation?

Thanks ๐Ÿ˜

Inconsistency between `api_route` and `api_route_with`

Hi, thank you for the library!

I've noticed the following issue with api_route_with method.
If I try to register two different handlers for get and post for the same path, only the latest handler appears in the doc.

ApiRouter::new()
    .api_route_with(
        "/test",
        post(create),
        |p| p,
    )
    .api_route_with(
        "/test",
        get(get_list),
        |p| p,
    )

Inconsistent response inference for operation functions `get` vs `get_with`, `post` vs `post_with`, etc.

This issue tracks the tangential problem that was described in #81 (comment) after it was closed.

Using get (and analogous operation functions), response types are inferred, but using get_with does not infer the response types unless you globally enable them with infer_responses(true). Ideally, these operation functions would have the same inference behavior, or this could be better documented.

Would it be better if both used the global configuration (like get_with), or if both would automatically infer response types (like get)? Consolidating to either approach would be a breaking change.

On the one hand, I believe that the preferable default for both functions is to infer response types by default. Having the library infer the response type is the "I don't want to worry about it" approach that is consistent with not needing to explicitly configure anything. If you want manual control over the response types, that should be the explicit case.

On the other hand, if you have a global configuration that is supposed to decide whether responses should be inferred, then these functions should probably respect that, unless it is explicitly overridden in a narrower scope. Currently, the global default is false which means inference would be disabled for both of these, unless explicitly enabled.

One way to reconcile these conflicting priorities would be by changing the default global configuration to true.

Looking for maintainers

It seems I cannot dedicate any time to OSS currently, so I'm looking for maintainers to triage/merge PRs and to publish occasional releases, so that people who rely on this library do not have to wait months for responses/fixes/features.

How to dedup schemas?

If you take a look at my example project, it generates 26 times the same schema for Foo.
The behavior I would expect is to generate a single schema for Foo in /components/schemas and then use $ref to use it.
Looking at the code, I see the opportunity in ReferenceOr::Reference, but I can't find a place to generate it, the code always uses the other variant

Support optional JSON extractors

Axum allows for Result<Json<Value>, JsonRejection> to be passed as a variant of an optional extractor.

A handler with that kind of argument does not implement OperationHandler.

It would be useful to support in case the handler has its own error handling logic.

Release 0.13 (Axum 0.7 Support)

Any idea when to expect a new release? Axum 0.7 support is working and it would be nice to be able to pull in aide from crates.io.
Are there any specific blockers?

aide::gen::extract_schemas(true) pulls in far too many definitions

The resulting doc will contain everything from encoding definitions to basic definitions of "schema". The OpenApi file becomes very large. In addition, it fails to process in swagger editor due to errors.

From the OpenAPI 3.1 docs:

To allow use of a different default $schema value for all Schema Objects contained within an OAS document, a jsonSchemaDialect value may be set within the OpenAPI Object. If this default is not set, then the OAS dialect schema id MUST be used for these Schema Objects. The value of $schema within a Schema Object always overrides any default.

Is this correctly handled? The OpenAPI object does not have jsonSchemaDialect set...

Changing schemars' generation settings

While using aide I'd like to change schemars' settings, specifically I'd like to disable making Options being turned into anyOfs with nulls. Schemars supports this by setting option_nullable = true and option_add_null_type = false on its settings struct which I can do by doing something like this:

aide::gen::in_context(|ctx| {
    ctx.schema = schemars::gen::SchemaGenerator::new(schemars::gen::SchemaSettings::draft07().with(|s| {
        s.option_nullable = true;
        s.option_add_null_type = false;
    }));
});

But if I want to use other aide features such as schema extraction then this isn't going to work because that also modifies the schema generator. I worked around this for now by just looking at the source and making the same modifications on the schema settings as extract_schemas does, but this is not optimal as it requires me to know internals of aide. Would you consider exposing these settings through aide so this is not required?

Feature request: Add support for `serde_qs::axum::QsQuery` as an alternative to `axum::extract::Query`

The crate serde_qs has a feature flag for axum which exposes this custom extractor as an alternative to the crate serde_urlencoded which axum uses.

The advantage of QsQuery is that it adds support for sequences when serializing or deserializing query strings.

I wouldn't mind contributing to aide with a pull request if this would be considered a good addition, if you could point me in the right direction on where the current implementation is which adds support for Query?

Documentation generation missing paths

Since updating aide for axum 0.6.0-rc.5 compatibility the generation of the documentation seems to be broken. Specificially the "paths":{} section of the generated json is empty. Testing the example in the repository reproduces the issue:

Capture

[Feature Request] Swagger UI integration/example

Since its most recent release Swagger UI supports OpenAPI 3.1 specs, meaning it can be used to render aide-generated docs.
As it offers a few useful features over Redoc, such as a convenient "Try it out" button, it would be interesting to have support for this similar to aide::redoc or at least docs in the form of an example using it.
However, the build process seems a bit more involved. While there is a static dist version similar to how Redoc works, it's recommended to use the npm package in conjunction with a tool like webpack instead to save space, as described here.
Not sure what the best way to go forward is because of that.

Fix OpenApi deserialize

I'm encountering an issue trying to deserialize OpenApi from the JSON file downloaded via redoc UI. OpenApi structure can't be correctly deserialized due to incorrect implementation of the serde_version::deserialize function for the openapi field.

This minimal test:

#[cfg(test)]
mod tests {
    use crate::openapi::OpenApi;

    #[test]
    fn test_default_openapi_deserialize() {
        let mut api = OpenApi::default();
        api.openapi = "3.1.0";

        let json = serde_json::to_string(&api).unwrap();

        let static_json = Box::leak(json.into_boxed_str());

        let deser_api = serde_json::from_str::<OpenApi>(static_json).unwrap();

        assert_eq!(api, deser_api);
    }
}

Ends up with error:

thread 'openapi::openapi::tests::test_default_openapi_deserialize' panicked at 'called `Result::unwrap()` on an `Err` value: Error("expected `,` or `}`", line: 1, column: 12)'
0

RUSTC: rustc 1.72.0 (5680fa18f 2023-08-23)

the trait `OperationOutput` is not implemented for `axum_jsonschema::Json<T>`

I suspect the problem is that the published version of axum-jsonschema is 0.6.0 instead of 0.7.0 - that is, the latter does not appear to be a released version for use in cargo. Attempting to use 0.6.0 with aide 0.12.0 results in the trait OperationOutput is not implemented for axum_jsonschema::Json<T> for both derive OperationIo and impl OperationOutput

Reproduce using aide/examples/example-axum by modifying aide/examples/example-axum/Cargo.toml to specify:

axum-jsonschema = { version = "0.6.0" , features = [
    "aide",
] }

instead of axum-jsonschema = { path = "../../crates/axum-jsonschema" ...

This results in:

error[E0277]: the trait bound `axum_jsonschema::Json<T>: OperationOutput` is not satisfied
  --> examples/example-axum/src/extractors.rs:10:23
   |
10 | #[derive(FromRequest, OperationIo)]
   |                       ^^^^^^^^^^^ the trait `OperationOutput` is not implemented for `axum_jsonschema::Json<T>`
   |
   = help: the following other types implement trait `OperationOutput`:
...

Example doesn't compile using the latest aide and Axum crates

error[E0277]: the trait bound `axum::http::Response<Body>: axum_core::response::into_response::IntoResponse` is not satisfied
  --> api/src/docs.rs:45:86
   |
45 |   async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoApiResponse {
   |  ______________________________________________________________________________________^
46 | |     Json(api).into_response()
47 | | }
   | |_^ the trait `axum_core::response::into_response::IntoResponse` is not implemented for `axum::http::Response<Body>`
   |
   = help: the following other types implement trait `axum_core::response::into_response::IntoResponse`:
             http_body::empty::Empty<axum::body::Bytes>
             axum_jsonschema::Json<T>
             JsonSchemaRejection
             http_body::combinators::box_body::BoxBody<axum::body::Bytes, E>
             http_body::combinators::box_body::UnsyncBoxBody<axum::body::Bytes, E>
             axum_extra::either::Either<E1, E2>
             axum_extra::either::Either3<E1, E2, E3>
             axum_extra::either::Either4<E1, E2, E3, E4>
           and 138 others
   = note: required for `axum::http::Response<Body>` to implement `IntoApiResponse`

RAM usage

After using aide for generating the OpenApi doc for an API the memory consumption has increased from a couple of megabytes to more than a gigabyte. Is it my problem or aide use a lot of RAM?

[Feature Request] exposing the static `SchemaContext`

I am talking about this:

thread_local! {
static CONTEXT: RefCell<SchemaContext> = RefCell::new(SchemaContext::new());
}

Use case

Extending axum-jsonschema crate. For example, the code below will compile the schema validation on every request. Making my own context seems redundant, as the type could already exist in axum-jsonschema's SchemaContext.

pub mod path {
  use crate::errors::AppError;
  use aide::OperationIo;
  use axum::{extract::FromRequestParts, http::request::Parts};
  use jsonschema::output::BasicOutput;
  use schemars::schema_for;
  use serde::de::DeserializeOwned;
  use serde_json::{Map, Value};
  // use axum_macros::FromRequestParts;

  #[derive(OperationIo)]
  // #[from_request(via(axum::extract::Path), rejection(AppError))]
  #[aide(input_with = "axum::extract::Path<T>", json_schema)]
  pub struct Path<T>(pub T);

  #[axum::async_trait]
  impl<T, S> FromRequestParts<S> for Path<T>
  where
    T: DeserializeOwned + schemars::JsonSchema + Send,
    S: Send + Sync,
  {
    type Rejection = AppError;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
      let raw_parrams = match axum::extract::RawPathParams::from_request_parts(parts, state).await {
        Ok(p) => p,
        Err(e) => return Err(e.into()),
      };

      let value = raw_parrams
        .into_iter()
        .map(|p| (p.0.to_owned(), Value::String(p.1.to_owned())))
        .collect::<Map<_, _>>();
      let value = Value::Object(value);

      let validation_result = {
        let schema =
          match jsonschema::JSONSchema::compile(&serde_json::to_value(schema_for!(T)).unwrap()) {
            Ok(s) => s,
            Err(error) => {
              tracing::error!(
                  %error,
                  type_name = std::any::type_name::<T>(),
                  "invalid JSON schema for type"
              );
              jsonschema::JSONSchema::compile(&Value::Object(Map::default())).unwrap()
            }
          };

        let out = schema.apply(&value).basic();

        match out {
          BasicOutput::Valid(_) => Ok(()),
          BasicOutput::Invalid(v) => Err(v),
        }
      };

      if let Err(errors) = validation_result {
        return Err(axum_jsonschema::JsonSchemaRejection::Schema(errors).into());
      }

      T::deserialize(value)
        .map_err(|e| {
          AppError::new("invalid request")
            .with_details(serde_json::json!({"path_serde": &format!("{e:#?}")}))
        })
        .map(Self)
    }
  }
}

Merging routers containing routes with the same path but different methods

If I'm reading the code correctly, then if you merge two routers, each with a route that is on the same path but listening for a different method then the second one will overwrite the specification in paths. I'm talking about the following:

/// See [`axum::Router::merge`] for details.
///
/// If an another [`ApiRouter`] is provided, the generated documentations
/// are merged as well..
pub fn merge<R>(mut self, other: R) -> Self
where
R: Into<ApiRouter<S, B>>,
{
let other: ApiRouter<S, B> = other.into();
self.paths.extend(other.paths);
self.router = self.router.merge(other.router);
self
}

Specifically, instead of paths.extend(other.paths), the entry should be retrieved and merging should be done on the method level, so that two routers with overlapping paths but nonoverlapping methods can be merged correctly.

[Feature Request] proc_macro router auto gen

Hi, I am experimenting on giving a rocket like experience but with axum, like so:

#[aide::make_router(
  state = Arc<AppState>,
  nest(api_router_1, path = "/apiV1"),
  nest(api_router_2, path = "/apiV2"),
)]
mod root_router {
  use std::sync::Arc;
  use axum::extract::State;
  use crate::AppState;

  #[aide::get("/")]
  pub async fn index(_: State<Arc<AppState>>) -> String {
    "index".to_owned()
  }

  #[aide::post("/yay")]
  pub async fn yay() -> String {
    "yay".to_owned()
  }

  fn yay_docs(op: TransformOperation) -> TransformOperation {
    op.description("yay")
      .response::<201, String>()
  }

  // Untouched examples
  #[some_crate::some_attr_macro]
  pub fn some_other_fn() {}
  pub struct MyStruct2 {}
}

It would generate something like this:

mod root_router {
  // ... original code but remove aide specific attributes

  pub fn make_router() -> aide::ApiRouter<Arc<AppState>> {
    ApiRouter::new()
    .api_route("/", get(index))
    .api_route("/yay", post_with(yay, yay_docs))
    .nest_api_service("/apiV1", api_router_1::make_router())
    .nest_api_service("/apiV2", api_router_2::make_router())
  }
}

Transform operations can be detected with this format SOME_ROUTE_FUNCTION_NAME_doc. The _doc at the end of a function name is what determine whether to use get or get_with. The aide::make_router attribute macro is the only one implemented. The other macros like the aide::post attribute macros are fake, but should be create for documentation purposes(panic if used outside of aide::make_router.

I have a poc on my local machine but with axum router. Are you interested if I work on this feature for aide?

ApiMethodRouter does not implement Clone

Hi, I have the following code working with Axum's MethodRouter, but not with Aide::axum's ApiMethodRouter, because the latter does not implement Clone. Is that on purpose? Is there a workaround for when I need to pass the methodrouter several times?

Thanks for any help!

pub trait RouterExt<S, B>
where
    B: HttpBody + Send + 'static,
    S: Clone + Send + Sync + 'static,
{
    fn directory_route(self, path: &str, method_router: ApiMethodRouter<S, B>) -> Self;
}

impl<S, B> RouterExt<S, B> for ApiRouter<S, B>
where
    B: HttpBody + Send + 'static,
    S: Clone + Send + Sync + 'static,
{
    fn directory_route(self, path: &str, method_router: ApiMethodRouter<S, B>) -> Self {
        self.route(path, method_router.clone())
            .api_route(&format!("{path}/"), method_router)
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.