tamasfe / aide Goto Github PK
View Code? Open in Web Editor NEWAn API documentation library
License: Apache License 2.0
An API documentation library
License: Apache License 2.0
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_route
s defined in an ApiRouter, such that I could manually add the parameter information to this nested router.
Is there a limitation on the number of api_route
s for an ApiRouter? There's no compiler error. ApiRouter just doesn't return a router when I have more than 10 api_route
s.
// 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)))
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.
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.
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
Previously
oha http://127.0.0.1:3000/todo/123 -c 1000 -z 60s -q 10000
will eat 5 gigs of ram after 1 minute.
Because closure which builds HTML was cloned on each request
https://github.com/tamasfe/aide/blob/master/crates/aide/src/redoc/mod.rs#L169
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
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?
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));
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
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.
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.
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.
If parameters are used in the URL, they are not generated correctly in OpenAPI-Spec.
Current: (from example)
Expected: (from https://redocly.github.io/redoc/#tag/store/operation/getOrderById)
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.
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.
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 ๐
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
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.
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
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
@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.
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
?
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.
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<_, _>`
| |_________________|
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?
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 parametersRouter
(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 ๐
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,
)
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
.
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.
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
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.
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?
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...
An error occurs in the last few automated documentation builds.
The lack of documentation on https://docs.rs/ causes great discomfort during the initial acquaintance with the library.
A possible solution would be to add default = ["axum", "axum-sqlx-tx/runtime-tokio-rustls"]
to the [features]
section for the aide crate.
Could you please add compatibility with axum v 0.7 that was recently released?
Seems there were some breaking changes in the latest axum stable version.
While using aide I'd like to change schemars' settings, specifically I'd like to disable making Option
s being turned into anyOf
s 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?
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
?
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.
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)
(StatusCode, R)
is a commonly used return value in axum, all my api are in the form of (StatusCode, Json<T>)
But this type does not implement IntoApiResponse
.
Another type axum_core::response::Response
also seems to be missing IntoApiResponse
.
From https://docs.rs/axum/latest/axum/struct.Router.html#wildcards - a route like /assets/*path
should extract a param.
There's a good old discussion with no concrete answer at OAI/OpenAPI-Specification#892 over precisely this should be represented in OpenAPI 3.X - from a quick scan the best approach could be to follow the AWS Gateway API style: /assets/{path+}
I can provide a PR?
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`:
...
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`
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?
I am talking about this:
aide/crates/axum-jsonschema/src/lib.rs
Lines 112 to 114 in be3b6d7
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)
}
}
}
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:
aide/crates/aide/src/axum/mod.rs
Lines 489 to 502 in dcac375
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.
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?
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)
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.