tamasfe / aide

An API documentation library
Apache License 2.0
412 stars 68 forks source link

feat(aide): add axum_typed_multipart integration #111

Closed emonadeo closed 8 months ago

emonadeo commented 9 months ago

Currently, aide generates a very generic schema from the multipart extractor:

// ...
"requestBody": {
  "description": "multipart form data",
  "content": {
    "multipart/form-data": {
      "schema": {
        "type": "array"
      }
    }
  },
  "required": true
}

This is impossible to fix with vanilla axum, as it only provides a plain Multipart extractor which imperatively reads the fields without providing types beyond that.

This PR adds integration for https://github.com/murar8/axum_typed_multipart, which provides a fully typed multipart extractor.

Example

use aide::{
    axum::IntoApiResponse,
}
use aide_axum_typed_multipart::{FieldData, TypedMultipart};
use axum::{body::Bytes, http::StatusCode};
use axum_typed_multipart::TryFromMultipart;

#[derive(Debug, TryFromMultipart, JsonSchema)]
struct CreateImage {
    title: String,
    description: String,
    #[form_data(limit = "unlimited")]
    image: FieldData<Bytes>,
}

async fn create_image(
    TypedMultipart(multipart): TypedMultipart<CreateImage>,
) -> impl IntoApiResponse {
    // do something
    return StatusCode::OK;
}

// add into api router, e.g. post_with(create_image, create_image_docs)

generates

"requestBody": {
  "content": {
    "multipart/form-data": {
      "schema": {
        "$ref": "#/components/schemas/CreateImage"
      }
    }
  },
  "required": true
},

and the schemas

"CreateImage": {
  "type": "object",
  "required": [
    "description",
    "image",
    "title"
  ],
  "properties": {
    "description": {
      "type": "string"
    },
    "image": {
      "$ref": "#/components/schemas/Array_of_uint8"
    },
    "title": {
      "type": "string"
    }
  }
}
"Array_of_uint8": {
  "type": "array",
  "items": {
    "type": "integer",
    "format": "uint8",
    "minimum": 0
  }
},

Still not great

The image input, represented in Rust as axum::body::Bytes serialises to Array_of_uint8 JsonSchema as implemented in schemars with the bytes feature enabled, but OpenAPI offers more advanced options to specify this.

The current state of this PR does not yet cover Encoding Objects, which allow something like this:

example taken from the specification

requestBody:
  content:
    multipart/form-data:
      schema:
        type: object
        properties:
          id:
            # default is text/plain
            type: string
            format: uuid
          address:
            # default is application/json
            type: object
            properties: {}
          historyMetadata:
            # need to declare XML format!
            description: metadata in XML format
            type: object
            properties: {}
          profileImage: {}
      encoding:
        historyMetadata:
          # require XML Content-Type in utf-8 encoding
          contentType: application/xml; charset=utf-8
        profileImage:
          # only accept png/jpeg
          contentType: image/png, image/jpeg
          headers:
            X-Rate-Limit-Limit:
              description: The number of allowed requests in the current period
              schema:
                type: integer

Tasks

I can see multiple directions to design such an API, but I would prefer to discuss this before I implement something in a bad direction, which is why I created this draft PR. Feedback is much appreciated.

Wicpar commented 9 months ago

Looks great, thank you !

The issue with implementing interop between libraries is that inevitably the up to date version will clash with the implemented version. We had massive issues when a major version changed for axum and sqlx. For this reason we now prefer to implement interop with the newtype pattern that works as a drop in replacement.

Would that be ok for you ? We can either accept a subcrate or you can publish it on your own as aide-typed-multipart.

emonadeo commented 9 months ago

Sure, shouldn't be a problem.

Wicpar commented 9 months ago

Looks good to me !

You may want to create your own proc macro based on the axum one if you wish to truly document all the different aspects. However just having the general type for most cases is good enough, i would accept it as-is and allow for future expansion as the need arises in your usecase. It's up to you.

emonadeo commented 9 months ago

Yes, I have been considering using a macro to document the encoding, but I am very inexperienced in authoring macros. Let me explore this a bit and follow up with a new PR.

For the time being I think this is sufficient for 0.1.0. I will add documentation and then ask for review and merge.

itsbalamurali commented 8 months ago

Thanks @emonadeo this PR! @Wicpar any ETA on merging and cutting a new release?

Wicpar commented 8 months ago

Not yet, i have a tight deadline at the moment. I still need to configure the changelog before release. If you do that (based on the other crates and the contributing.md command) i can merge sooner.

emonadeo commented 8 months ago

Oops I broke the tests, I forgot that Rust runs code blocks in docstrings. Will fix in a new PR.

Wicpar commented 8 months ago

i fixed it, no worries. published

imran-mirza79 commented 7 months ago

Is there a way to validate the file extension like you did for file limit. For instance, if I want to take only png or pdf files, how do I do it ? Checking for the extension of file is an option, but I was wondering if we can do it in a way that is similar to checking limit #[form_data(limit = "unlimited")]

emonadeo commented 7 months ago

Is there a way to validate the file extension like you did for file limit. For instance, if I want to take only png or pdf files, how do I do it ? Checking for the extension of file is an option, but I was wondering if we can do it in a way that is similar to checking limit #[form_data(limit = "unlimited")]

#[form_data(limit = "unlimited")] is part of the derive macro provided by axum_typed_multipart, aide_axum_typed_multipart (this PR) only implements the traits needed to generate OpenAPI schemas from a TypedMultipart<> extractor.

I think checking the file extension is perfectly fine. You could also check the Content-Type header of the file, although I wouldn't rely on it. And if you really want to make sure that you 100% have a file that is a valid png or pdf, you'll need to validate the file contents (probably using a third party library).

imran-mirza79 commented 7 months ago

Is there a way to validate the file extension like you did for file limit. For instance, if I want to take only png or pdf files, how do I do it ? Checking for the extension of file is an option, but I was wondering if we can do it in a way that is similar to checking limit #[form_data(limit = "unlimited")]

#[form_data(limit = "unlimited")] is part of the derive macro provided by axum_typed_multipart, aide_axum_typed_multipart (this PR) only implements the traits needed to generate OpenAPI schemas from a TypedMultipart<> extractor.

I think checking the file extension is perfectly fine. You could also check the Content-Type header of the file, although I wouldn't rely on it. And if you really want to make sure that you 100% have a file that is a valid png or pdf, you'll need to validate the file contents (probably using a third party library).

Ooh got it, Thankss!