Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support generating OpenAPI/Swagger docs #50

Closed
hronro opened this issue Jul 31, 2021 · 65 comments
Closed

Support generating OpenAPI/Swagger docs #50

hronro opened this issue Jul 31, 2021 · 65 comments
Labels
A-axum C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot

Comments

@hronro
Copy link

hronro commented Jul 31, 2021

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.

@davidpdrsn davidpdrsn added C-feature-request Category: A feature request, i.e: not implemented / a PR. E-medium Call for participation: Experience needed to fix: Medium / intermediate labels Jul 31, 2021
@Michael-J-Ward
Copy link

Just documenting some prior art in this area

  • dropshot creaed by oxide computing
  • weave - which goes a step further and can generate typescript clients to consume the api

@davidpdrsn davidpdrsn added E-hard Call for participation: Experience needed to fix: Hard / a lot and removed E-medium Call for participation: Experience needed to fix: Medium / intermediate labels Aug 1, 2021
@aesteve
Copy link

aesteve commented Aug 1, 2021

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.
You already have the router/server you built using the library, which knows about the routes, parameters, body types, etc. And there's a toOpenAPI method which generates the appropriate OpenAPI definition object. This OpenAPI definition can then be exposed in YAML or JSON format, and mounted on the appropriate endpoint of your choice. But the OpenAPI definition being an object (let's assume it would be a struct in Rust) also allows for enhancing the docs with comments, descriptions, server information, etc. which is a really nice feature.
Annotation (or macro, in rust) driven libraries tend to be quite tedious in this regard, where would you put the annotation for the global app description? The app doc version? You have to annotate something? But what?


A good first step could be to try to generate an OpenAPI definition from the existing structs in Axum.
For example: mapping endpoints to OpenAPI operations, using a set of Rust OpenAPI structs matching their concepts (Operation, Response, Schema, etc.) using existing crates like https://crates.io/crates/openapiv3 for instance .

From this point on, we could get a glimpse at what pieces of information are missing.
For instance: examples for query parameters will be missing from the UrlParams extractor I suppose. Same goes for Request/Response bodies schemas, or operations (get/post/put/...) descriptions.

There is the hardest part imo, and could lead to creating another crate?
Can extractors be re-worked, in some way, to include such missing information? Is it desirable? Will it make the crate too cluttered, or too complex? That's a very tough question at this point.


A note on "re-usability".
One fantastic thing in the builder approach (as opposed to annotation or macro driven libraries) is in re-using the definitions in different places.
Let's imagine an Order API with endpoints like: GET /api/orders/:order_id and PUT /api/orders/:order_id for respectively reading and changing a customer order.
The order_id query param probably must respect some formating rules like: [0-9]{4}-[0-9]{4} or whatever. It also (in OpenAPI terms) probably has a description pointing to a doc, and an example to ease the developer experience of API users.
In an annotation-driven library: chances are the API developer will have to repeat the same annotation on multiple endpoints (methods) definition.
In many "builder approaches" like Tapir (see above) or other libraries using the same idea, the developer could just write the order_id parameter definition once, and use it in every endpoint definition. That's a big win in many regards, DRY obviously, but also in separation of concerns: a single file can contain every param (extractor in the case of Axum) with definition, formating rules, description, examples, and keep the service methods bounded to the real implementation (updating the DB, etc.).
Unfortunately I'm a very beginner in Rust, and can't even know if Axum's extractor pattern would fit this. But I thought it was worth mentioning.


Hopefully this helps in designing OpenAPI support at some point in time.
Good luck with Axum, as said before, the builder approach definitely has some advantages over macro libraries (although both are great) and it's really really good to see such a library built on top of tokio and hyper.
Thanks for creating this!

@jakobhellermann
Copy link

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 Code
use 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.



@aesteve

Can extractors be re-worked, in some way, to include such missing information? Is it desirable? Will it make the crate too cluttered, or too complex? That's a very tough question at this point.

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 DescribeSchema can be derived and OperationsParameter is implemented for extract::Json<T: DescribeSchema>, extract::UrlParams<..> etc.
The macros could also be extended to support e.g. #[axum_openapi::handler(operation = openapiv3::Operation { .. })].

@davidpdrsn
Copy link
Member

@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 🤞

@OldManFroggy
Copy link

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.

@jakobhellermann
Copy link

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 openapiv3::Operation like this:

.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 OnMethod and IntoService pub and unsealing the Handler.

My code is here if anyone wants to check it out.

@dbofmmbt
Copy link
Contributor

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?

@davidpdrsn
Copy link
Member

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.

@szagi3891
Copy link

@davidpdrsn or perhaps it is better to focus on OpenApi as an MVP first. Release, stabilise. And then possibly extend it in future steps ? :)

@szagi3891
Copy link

The "poem-web" library takes a very interesting approach to this problem:
https://github.com/poem-web/poem/blob/master/examples/openapi/oneof/src/main.rs

Handlers are methods of structure. This allows the macro to statically generate the entire specification.

@davidpdrsn
Copy link
Member

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.

@davidpdrsn
Copy link
Member

From #459 (comment):

An update on this: I haven't had much motivation to work on this lately so if someone wants to continue the work that'd be much appreciated 😊

This PR should contain the overall structure and hopefully it's clear how to otherwise just ping me if you questions. The goal is also to develop things in an external crate so the work doesn't necessarily need to happen in this repo.

@dzmitry-lahoda
Copy link

dzmitry-lahoda commented Jan 14, 2022

the only reason i look at rocket now is openapi generator https://github.com/GREsau/okapi . will donate 25 USD for generator.

@tasn
Copy link
Contributor

tasn commented Jan 18, 2022

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.

@piaoger
Copy link

piaoger commented Jan 24, 2022

the only reason i look at rocket now is openapi generator https://github.com/GREsau/okapi . will donate 25 USD for generator.

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.
I made a simple benchmark for warp, axum and poem and found they share similar behavior: performant, small binary ,and low memory usage. One sell point of poem is OpenAPI generating.

@Silentdoer
Copy link

Silentdoer commented Jan 24, 2022

it should like poem's openapi? such as :#[oai(path = "/hello", method = "get", method = "post")]

@tasn
Copy link
Contributor

tasn commented Jan 25, 2022

@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).

@ltog
Copy link

ltog commented Feb 16, 2022

@jakobhellermann

My code is here if anyone wants to check it out.

I can't access this repository. Is there any chance you can make it accessible again?

@jakobhellermann
Copy link

I can't access this repository. Is there any chance you can make it accessible again?

Should be accessible now again.

@ltog
Copy link

ltog commented Feb 21, 2022

@jakobhellermann : Thank you!

@SaadiSave
Copy link

SaadiSave commented Feb 23, 2022

@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).

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 Router) until the design work needed for OpenAPI status codes is complete.

@adriangb
Copy link

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).

@DaAitch
Copy link

DaAitch commented Feb 26, 2023

However at the end of the day, you need your code to match up with the contract, so in the end it just means doing the same work twice.

Why "twice"? The amount of overlapping work is just a mapping and impl and contract is always in sync, verified by the compiler.

  • data definitions: generated source from contract
  • paths:
    • urls: mapped by operation id, generated async trait from operation ids from contract
    • parameters: generated source from contract (async fn get_pets(req: GetPetsRequest /* .parameters */) -> GetPetsResponse;)
    • requestBody: same (.. req: GetPetsRequest /* .body */ ..)
    • responses: defined in contract, generated source used in async trait, impossible to return invalid response
    • ... and so on

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.

@AThilenius
Copy link

AThilenius commented Feb 28, 2023

@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.

  • What about Protobuf as an IDL?
    • OpenAPI spawned out of generic HTTP REST; it's messy and very complicated by necessity, because HTTP is messy. Using Protobuf limits what the contract can express considerably, making the codegen much easier and the generated server/client impls more restrictive. For me, that's all advantage and little downside.
  • How do you work with Axum Extractors?
    • This would be the magic of a contract-first approach for Axum IMO: still working with it's extractors. I still need to be able to inject state, get the hostname, read typed headers, et. al. But obviously I can't accept in arbitrary JSON any more, the input type must be known and typed to what ever was generated from the contract.
    • I could see this being solvable by mirroring the Axum types in a more restricted way, then mapping those onto Axum in the generated code. This might also let you ensure no response can be returned that the contract didn't specify (for example, a Redirect), but I haven't thought that idea out much.

Edit:
Also, while this almost certainly can/should be a separate tool, I think having the discussion here has the advantage of letting Axum authors chime in on typings (like ideas on how to play nice with Extractors). I'm not advocating that Axum itself support this.

@yellowred
Copy link

I would discourage from using protobuf for schemas definition as it has 2 major disadvantages:

  1. It requires compilation of manifests.
  2. It has limited type system.

The best approach would be to use schemas written in Rust.

@AThilenius
Copy link

AThilenius commented Feb 28, 2023

I would discourage from using protobuf for schemas definition as it has 2 major disadvantages:

  1. It requires compilation of manifests.
  2. It has limited type system.

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.

@yellowred
Copy link

@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.

@AThilenius
Copy link

@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 Handler impl over the set of FnOnce that Axom will work with as request handlers. It distinguishes between FromRequestParts and FromRequest which is key to keeping Axum extractors but forcing a contract type signature.

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 say handler for ElizaService with typing such that the contract is validated no matter what extractor args you have. And because Axum distinguished FromRequestParts and FromRequest you can prevent the handlers from getting access to the HTTP body by accident.

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.

@AThilenius
Copy link

AThilenius commented Mar 3, 2023

@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 😙👌

@fergus-hou
Copy link

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
Perhaps this could provide some inspiration for Axum.

@tasn
Copy link
Contributor

tasn commented Jun 13, 2023

We've been using aide at https://github.com/svix/svix-webhooks and it's been awesome.

@fergus-hou
Copy link

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.

@tasn
Copy link
Contributor

tasn commented Jun 13, 2023

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.

@netthier
Copy link

netthier commented Jun 13, 2023

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 😄

@ProbablyClem
Copy link

Hello guys, I created a new crate that enables automatic paths, schemas and responses models import by utoipa.
Which makes it way easier to generate open-api specifications from utoipa-compatible frameworks such as axum.

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

@kontsaki
Copy link

hello, is there a chance Axum will ever support generating OpenAPI spec out-of-the-box ?

@juhaku
Copy link

juhaku commented Aug 29, 2024

Yo folks, I thought this is a good place to bring about updates relating to the later this fall coming utoipa 5.0.0 release and where it is standing with axum. There has been a recent PR juhaku/utoipa#1004 that deepens the integration between axum and utoipa to level it feels almost like "plain" axum. It reduces the boilerplate drastically and agony that is currently present. Here is the example related to it: https://github.com/juhaku/utoipa/tree/master/examples/axum-utoipa-bindings

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.

@ttys3
Copy link
Contributor

ttys3 commented Sep 1, 2024

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 auto import feature and make utoipauto official.

because utoipa axum binding impl make users of axum directly depend on utoipa

@jifalops
Copy link

jifalops commented Dec 3, 2024

oasgen is a newer crate I haven't seen mentioned anywhere. It adds very little boilerplate but is still in early development.

@musjj
Copy link

musjj commented Jan 15, 2025

People creating OpenAPI generators should definitely look into poem-openapi for prior art. I don't know why it's so often overlooked in these discussions. IMO it's the gold standard for OpenAPI support and I have yet to see anything close to it. Here's a snippet from the documentation:

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: Result<CreateUserResponse, CreateUserResponseError>. It's almost like you're writing a normal rust function, but you can generate a complete OpenAPI specification from it.

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 #[oai(...)] attribute fully defines this. OpenApiService will perform all the registration automatically for you.

Everything about this is just beautiful to me, really.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-axum C-feature-request Category: A feature request, i.e: not implemented / a PR. E-hard Call for participation: Experience needed to fix: Hard / a lot
Projects
None yet
Development

Successfully merging a pull request may close this issue.