-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support generating OpenAPI/Swagger docs #50
Comments
A (hopefully helpful?) source of inspiration in OpenAPI generation design: tapir which is a Scala server/router with a philosophy comparable to Axum (router / builder pattern and no macro / annotation stuff). The parts I'm finding interesting in the design: it's all methods added to existing structures. A good first step could be to try to generate an OpenAPI definition from the existing structs in Axum. From this point on, we could get a glimpse at what pieces of information are missing. There is the hardest part imo, and could lead to creating another crate? A note on "re-usability". Hopefully this helps in designing OpenAPI support at some point in time. |
I experimented with the annotation-based approach and got it to generate OpenAPI Schema---
openapi: 3.0.3
info:
title: ""
version: ""
paths:
"/pets/{id}":
get:
operationId: find_pet_by_id
parameters:
- in: path
name: id
required: true
schema:
type: integer
format: int64
responses:
default:
description: Default OK response
delete:
operationId: delete_pet
parameters:
- in: path
name: id
required: true
schema:
type: integer
format: int64
responses:
default:
description: Default OK response
/pets:
get:
operationId: find_pets
parameters:
- in: query
name: tags
schema:
nullable: true
type: array
items:
type: string
- in: query
name: limit
schema:
nullable: true
type: integer
format: int32
responses:
default:
description: Default OK response
post:
operationId: add_pet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/AddPetRequestBody"
required: true
responses:
default:
description: Default OK response
/openapi.yaml:
get:
operationId: api_yaml
responses: {}
/openapi.json:
get:
operationId: api_json
responses: {}
components:
schemas:
NewPet:
type: object
properties:
name:
type: string
tag:
nullable: true
type: string
PetExtra:
type: object
properties:
id:
type: integer
format: int64
Pet:
type: object
properties:
new_pet:
type: object
properties:
name:
type: string
tag:
nullable: true
type: string
pet_extra:
type: object
properties:
id:
type: integer
format: int64
AddPetRequestBody:
type: object
properties:
name:
type: string
tag:
nullable: true
type: string
FindPetsQueryParams:
type: object
properties:
tags:
nullable: true
type: array
items:
type: string
limit:
nullable: true
type: integer
format: int32 from Rust Codeuse axum::prelude::*;
use std::net::SocketAddr;
use axum_openapi::DescribeSchema;
#[tokio::main]
async fn main() {
let app = axum_openapi::routes!(route("/pets", get(find_pets).post(add_pet))
.route("/pets/:id", get(find_pet_by_id).delete(delete_pet))
.route("/openapi.yaml", get(axum_openapi::api_yaml))
.route("/openapi.json", get(axum_openapi::api_json)));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
hyper::server::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
mod model {
use axum_openapi::DescribeSchema;
#[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)]
pub struct Pet {
#[serde(flatten)]
new_pet: NewPet,
#[serde(flatten)]
pet_extra: PetExtra,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)]
pub struct PetExtra {
id: i64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)]
pub struct NewPet {
name: String,
tag: Option<String>,
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)]
pub struct FindPetsQueryParams {
tags: Option<Vec<String>>,
limit: Option<i32>,
}
/// Returns all pets from the system that the user has access to
#[axum_openapi::handler]
async fn find_pets(query_params: Option<axum::extract::Query<FindPetsQueryParams>>) {
println!("find_pets called");
println!("Query params: {:?}", query_params);
}
#[derive(Debug, serde::Serialize, serde::Deserialize, DescribeSchema)]
pub struct AddPetRequestBody {
name: String,
tag: Option<String>,
}
/// Creates a new pet in the store. Duplicates are allowed.
#[axum_openapi::handler]
async fn add_pet(request_body: axum::extract::Json<AddPetRequestBody>) {
println!("add_pet called");
println!("Request body: {:?}", request_body);
}
/// Returns a user based on a single ID, if the user does not have access to the pet
#[axum_openapi::handler]
async fn find_pet_by_id(path_params: axum::extract::UrlParams<(i64,)>) {
let (id,) = path_params.0;
println!("find_pet_by_id called");
println!("id = {}", id);
}
/// deletes a single pet based on the ID supplied
#[axum_openapi::handler]
async fn delete_pet(path_params: axum::extract::UrlParams<(i64,)>) {
let (id,) = path_params.0;
println!("delete_pet called");
println!("id = {}", id);
} Using three macros: #[axum_openapi::handler]
async fn handler() {}
#[derive(DescribeSchema)]
struct RequestBody {}
let app = axum_openapi::routes!(route("path", get(get_handler).post(post_handler))` The first two are relatively straightforward, the third one I'm not too happy because it isn't very resilient.
The way I implemented it there are two traits: pub trait DescribeSchema {
fn describe_schema() -> openapiv3::Schema;
}
pub trait OperationParameter {
fn modify_op(operation: &mut openapiv3::Operation, required: bool);
} where |
@jakobhellermann Thanks for looking into it, though I think we should try and find a solution that doesn't rely on macros. Or at least evaluate the ergonomics of such a solution. I haven't had the time to experiment yet, but I imagine the solution described here should work 🤞 |
Not looked into it, but if you could somehow make use of the existing doc generation framework cargo doc, with say a cargo swagger :) and have it look for additional information from the doc macro's - yes it means more manual doc than pure code/generation/inspection but it would also be in line with how docs are generated currently for rust systems. --just another very loose-formed idea I am throwing out there. |
It looks like you can generate the openapi descriptions purely based in traits resulting in an api like this: fn main() {
let app = route("/pets", get(find_pets).post(add_pet))
.route("/pets/:id", get(find_pet_by_id).delete(delete_pet));
let openapi = app.openapi();
let app = app
.route("/openapi.yaml", openapi_yaml_endpoint(openapi.clone()))
.route("/openapi.json", openapi_json_endpoint(openapi));
} In addition to that it's useful to add be able to ignore handlers or provide a fallback .route("/other", get(handler.ignore_openapi()))
.route("/yet/another", get(some_handler.with_openapi(my_fallback_operation))) I was able to implement this in a third party crate only by making the fields of My code is here if anyone wants to check it out. |
Just wanted to say that this would ease understanding how routing code is working, probably making e.g. #174 easier to debug. At least as I understood it, generating OpenAPI docs would be as useful as listing existing routes on Rails? |
I've been working on a POC in #170 but actually been thinking that rather than generating openapi specifically it might be better if Axum generated some Axum specific AST, which users then could write openapi generators for. That way we could support other formats or different kinds of debugging as @dbofmmbt mentions. I'll continue working on it when 0.2 is released over the next few weeks. |
@davidpdrsn or perhaps it is better to focus on OpenApi as an MVP first. Release, stabilise. And then possibly extend it in future steps ? :) |
The "poem-web" library takes a very interesting approach to this problem: Handlers are methods of structure. This allows the macro to statically generate the entire specification. |
If someone wanted to explore a macro based approach that should be entirely possible to build, without having to add additional stuff directly to axum I want to explore a direction with as few macros as possible but that shouldn't hold back others. |
From #459 (comment):
|
the only reason i look at rocket now is openapi generator https://github.com/GREsau/okapi . will donate 25 USD for generator. |
Just my 2c on why it's so important to us: in addition to the nice benefits of having the OpenAPI generated for you rather than manually (and potentially incorrectly) creating it. We also use the generated spec in CI to verify that we don't accidentally break API, or that when we do change the API it's reviewed by specific code owners. This relates to one of Rust's most fun benefits: fearless refactoring. This is the missing piece for that. |
I echo you . I had used Warp in production and it works well. For a new small microservice, I'd like to give axum or poem a try. |
it should like poem's openapi? such as :#[oai(path = "/hello", method = "get", method = "post")] |
@Silentdoer, axum already has all of the information other than security (easy to add using a trait on from request), and supported status codes (not sure how to fix it). Adding macros everywhere feels like an overkill (and also sucks, I love it that axum is free from macros). |
I can't access this repository. Is there any chance you can make it accessible again? |
Should be accessible now again. |
@jakobhellermann : Thank you! |
Are status codes really needed for an MVP? OpenAPI generation, even if it is incomplete, it would be very helpful, not to mention that the user can probably add status codes themselves. I think what we have right now is a good MVP. If an incomplete implementation cannot be merged into axum, it could exist as its own crate (using extension methods on |
I think an interesting approach to e.g. avoid accidentally breaking the contract by making a code change is to export that contract statically, commit it to your repo and snapshot test against it. That also means someone can read it without compiling and running the code. That contract could even be some sort of contract test thing (more complex than an openapi.json file). |
Why "twice"? The amount of overlapping work is just a mapping and impl and contract is always in sync, verified by the compiler.
We can also use serde_json, serde_xml, ... depending on content type of the contract and much more. I doubt it's possible to generate such contracts from code. |
@DaAitch I love the idea! That would fit our use case fantastic. I absolutely love Axum's extractor approach, it's been a real joy to work with because it's so flexible and modular and plays into Rust's strong type system. We've also been using Utoipa purely to generate frontend client code. Without meaning any disrespect to the authors of that library, it's way too much boilerplate and headache for our use, so here I am again, scouring the internet for a simple RPC solution with code client codegen. But I've kind of fallen for Axum, and having plain old HTTP JSON requests is just so easy to debug from browsers, and is ubiquitously supported. I just want strongly typed client code (for a subset of routes) 😭 A few questions come to mind.
Edit: |
I would discourage from using protobuf for schemas definition as it has 2 major disadvantages:
The best approach would be to use schemas written in Rust. |
Sorry none of that makes any sense to me. Protobuf (as an IDL) is just that... an Interface Definition Language. Not sure what manifests you're talking about? Definitions in Rust aren't a good idea, rust has a very expressive type system that very much doesn't map to most other languages. Protobuf has a limited type system for a very good reason; it's a decent lowest-common-denominator of types among many languages. Additionally extracting types out of a rust codebase every time you want to generate a client in another languages isn't ideal, nor are the extracted definitions human readable. Protobuf has some disadvantages for sure, those just aren't it. All optional fields and non-discriminated default vs. null values are some of them. |
@AThilenius As you said, Protobuf is a language. Not only does one have to master it but also has to compile it in order for it to be used in Rust. As for types, if you ok with a generic common denominator then it's probably fine, but if your language has the same rich types for eg byte sequences, then you would not want to lose this. There is some work to provide a better option here https://github.com/google/tarpc. |
Protobuf is not compiled any more than JSON. Tarpc is exclusively Rust<->Rust because it doesn't use an IDL. Unsurprisingly discussions they have had about polyglot support involve protobuuf. Anywho. @DaAitch I spent some time playing with Axum types today. Trying to grok them is a bit like dropping acid while you have imposter syndrome. The generics are... intense. This is the For example async fn sample_handler(
// Regular Axum extractor, to get at Headers, State, or what ever else you need.
TypedHeader(_cookies): TypedHeader<headers::Cookie>,
// The request object can be either the proto struct itself, or anything that ex. `impl Into<ElizaSayReq>`.
req: ElizaSayReq,
// Ditto for the response, and like Axum it can support `Result<ElizaSayResp, TErr>` types.
) -> ElizaSayResp {
ElizaSayResp {
response: format!("You said: {}", req.message),
}
} This handler can be registered as the I'm going to take a crack at adding support for the Connect-Web protocol. I like their work, they just don't have a Rust server so it's of limited interest to me. This exploration isn't exclusive to protobuf though, if you still have hopes of OpenAPI. Same thing can be used for contract enforcement there. |
@DaAitch https://github.com/AThilenius/axum-connect Two days of feverish work. It's functional. Still missing a lot, but vets the idea end to end. Contract driven Connect-Web impls right there alongside existing Axum code. Mmm. It's beautiful 😙👌 |
I found salvo has a very simple and clear example of OpenAPI implementation: https://github.com/salvo-rs/salvo/blob/main/examples/oapi-todos/src/main.rs |
We've been using aide at https://github.com/svix/svix-webhooks and it's been awesome. |
It seems like aide hasn't been updated for a long time. Utoipa seems to be better. However, utoipa still requires writing a lot of template code. |
It hasn't been updated in a month and a half and it's been working great. I don't think there's any cause for alarm. The main maintainer was replaced with a contributor, but that's fine. It would definitely be best if this became a first class citizen in axum though. |
I also use aide, and I feel like it supports most stuff I've worked with. Adding support for potential missing types is also quite easy without knowing the codebase. EDIT: Nice timing, aide just saw some merged PRs from the new maintainer, and there's a poll for a breaking feature going on that'd make adding support for custom types possible without having to fork the codebase 😄 |
Hello guys, I created a new crate that enables automatic paths, schemas and responses models import by utoipa. You can find it here: utoipauto Basically, just need to add one macro annotation and the rest would be done automatically. I think it's neat ! ...
use utoipauto::utoipauto;
...
#[utoipauto]
#[derive(OpenApi)]
#[openapi()]
pub struct ApiDoc;
... I hope It can helps some of you that had the same concerns as me |
hello, is there a chance Axum will ever support generating OpenAPI spec out-of-the-box ? |
Yo folks, I thought this is a good place to bring about updates relating to the later this fall coming This has not yet been released but next beta release is expected soon. But if you use master as a base you can already poke it around as well. |
while juhaku/utoipa#1004 is a good approach. but I think @ProbablyClem 's utoipauto impl has better DX, and I hope utoipa could refine the because utoipa axum binding impl make users of axum directly depend on |
|
People creating OpenAPI generators should definitely look into use poem::Error;
use poem_openapi::{payload::{PlainText, Json}, ApiResponse, Object, OpenApi};
#[derive(Object)]
struct CreateUserRequest {
username: String,
nickname: String,
email: String,
}
#[derive(ApiResponse)]
enum CreateUserResponse {
/// Returns when the user is successfully created.
#[oai(status = 200)]
Ok,
}
#[derive(ApiResponse)]
enum CreateUserResponseError {
/// Returns when the user already exists.
#[oai(status = 409)]
UserAlreadyExists,
/// Returns when the request parameters is incorrect.
#[oai(status = 400)]
BadRequest(PlainText<String>),
}
struct Api;
#[OpenApi]
impl Api {
/// Create a new user.
#[oai(path = "/user", method = "post")]
async fn create(
&self,
user: Json<CreateUserRequest>,
) -> Result<CreateUserResponse, CreateUserResponseError> {
todo!()
}
}
#[tokio::main]
async fn main() {
let api_service =
OpenApiService::new(Api, "Hello World", "1.0").server("http://localhost:3000");
let ui = api_service.swagger_ui();
let app = Route::new().nest("/", api_service).nest("/docs", ui);
Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(app)
.await;
} Notice how beautifully idiomatic the return type is: And you don't have to duplicate the definition of the status codes, response types, etc. elsewhere and be forced to manually sync them forever. It's all entirely defined by the function signature. You don't have to manually register the routes either, the Everything about this is just beautiful to me, really. |
If you are wring a RESTful API service, the OpenAPI/Swagger documentation is very useful. It would be great if
axum
is able to generate them automatically.The text was updated successfully, but these errors were encountered: