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

feat(aide): add axum_typed_multipart integration #111

Merged
merged 11 commits into from
Mar 6, 2024

Conversation

emonadeo
Copy link
Contributor

@emonadeo emonadeo commented Feb 11, 2024

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

  • Create an API to specify Encoding Objects
  • Validate Encoding Objects and return 4XX for bad content types
  • Documentation

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
Copy link
Collaborator

Wicpar commented Feb 11, 2024

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
Copy link
Contributor Author

Sure, shouldn't be a problem.

* rename `ApiFieldData` to `FieldData`
* pass down `FromRequest` trait impl to `TypedMultipart`
* fix missing async_trait
* reorder structs and trait impls
@Wicpar
Copy link
Collaborator

Wicpar commented Feb 11, 2024

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
Copy link
Contributor Author

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.

@emonadeo emonadeo marked this pull request as ready for review February 13, 2024 19:34
@itsbalamurali
Copy link

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

@Wicpar
Copy link
Collaborator

Wicpar commented Feb 22, 2024

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.

@Wicpar Wicpar merged commit 4ef2089 into tamasfe:master Mar 6, 2024
Wicpar pushed a commit that referenced this pull request Mar 6, 2024
…#111)

* feat(aide): add axum_typed_multipart integration

* remove unused import & format toml

* move aide-axum-typed-multipart to subcrate

* add `FromRequest` impl & refactor

* rename `ApiFieldData` to `FieldData`
* pass down `FromRequest` trait impl to `TypedMultipart`
* fix missing async_trait
* reorder structs and trait impls

* docs: summary and example

* docs: `TypedMultipart` and `FieldData` wrappers

* docs: add `aide-axum-typed-multipart` to README.md

* docs: beautified README.md

* add cliff

* chore(aide-axum-typed-multipart): update changelog

* remove changelog
@emonadeo
Copy link
Contributor Author

emonadeo commented Mar 6, 2024

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

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 6, 2024

i fixed it, no worries. published

@imran-mirza79
Copy link

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
Copy link
Contributor Author

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
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants