Skip to content

Commit

Permalink
Enhance file uploads
Browse files Browse the repository at this point in the history
This commit adds more test cases for file uploads and enhances docs for
file uploads.
  • Loading branch information
juhaku committed Oct 10, 2024
1 parent 18e96c9 commit 65ff028
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 0 deletions.
4 changes: 4 additions & 0 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,8 @@ impl ComponentSchema {
let default = pop_feature!(features => Feature::Default(_));
let title = pop_feature!(features => Feature::Title(_));
let deprecated = pop_feature!(features => Feature::Deprecated(_)).try_to_token_stream()?;
let content_encoding = pop_feature!(features => Feature::ContentEncoding(_));
let content_media_type = pop_feature!(features => Feature::ContentMediaType(_));

let child = type_tree
.children
Expand Down Expand Up @@ -1078,6 +1080,8 @@ impl ComponentSchema {
tokens.extend(min_items.to_token_stream())
}

content_encoding.to_tokens(tokens)?;
content_media_type.to_tokens(tokens)?;
default.to_tokens(tokens)?;
title.to_tokens(tokens)?;
example.to_tokens(tokens)?;
Expand Down
2 changes: 2 additions & 0 deletions utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ impl Parse for UnnamedFieldStructFeatures {
As,
Deprecated,
Description,
ContentEncoding,
ContentMediaType,
Bound
)))
}
Expand Down
72 changes: 72 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// * `deprecated` Can be used to mark the field as deprecated in the generated OpenAPI spec but
/// not in the code. If you'd like to mark the field as deprecated in the code as well use
/// Rust's own `#[deprecated]` attribute instead.
/// * `content_encoding = ...` Can be used to define content encoding used for underlying schema object.
/// See [`Object::content_encoding`][schema_object_encoding]
/// * `content_media_type = ...` Can be used to define MIME type of a string for underlying schema object.
/// See [`Object::content_media_type`][schema_object_media_type]
///
/// # Enum Optional Configuration Options for `#[schema(...)]`
///
Expand Down Expand Up @@ -1529,6 +1533,73 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// }
/// ```
///
/// # Defining file uploads
///
/// File uploads can be defined in accordance to Open API specification [file uploads][file_uploads].
///
///
/// _**Example sending `jpg` and `png` images as `application/octet-stream`.**_
/// ```rust
/// #[utoipa::path(
/// post,
/// request_body(
/// content(
/// ("image/png"),
/// ("image/jpg"),
/// ),
/// ),
/// path = "/test_images"
/// )]
/// async fn test_images(_body: Vec<u8>) {}
/// ```
///
/// _**Example of sending `multipart` form.**_
/// ```rust
/// #[derive(utoipa::ToSchema)]
/// struct MyForm {
/// order_id: i32,
/// #[schema(content_media_type = "application/octet-stream")]
/// file_bytes: Vec<u8>,
/// }
///
/// #[utoipa::path(
/// post,
/// request_body(content = inline(MyForm), content_type = "multipart/form-data"),
/// path = "/test_multipart"
/// )]
/// async fn test_multipart(_body: MyForm) {}
/// ```
///
/// _**Example of sending arbitrary binary content as `application/octet-stream`.**_
/// ```rust
/// #[utoipa::path(
/// post,
/// request_body = Vec<u8>,
/// path = "/test-octet-stream",
/// responses(
/// (status = 200, description = "success response")
/// ),
/// )]
/// async fn test_octet_stream(_body: Vec<u8>) {}
/// ```
///
/// _**Example of sending `png` image as `base64` encoded.**_
/// ```rust
/// #[derive(utoipa::ToSchema)]
/// #[schema(content_encoding = "base64")]
/// struct MyPng(String);
///
/// #[utoipa::path(
/// post,
/// request_body(content = inline(MyPng), content_type = "image/png"),
/// path = "/test_png",
/// responses(
/// (status = 200, description = "success response")
/// ),
/// )]
/// async fn test_png(_body: MyPng) {}
/// ```
///
/// # Examples
///
/// _**More complete example.**_
Expand Down Expand Up @@ -1738,6 +1809,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// [include_str]: https://doc.rust-lang.org/std/macro.include_str.html
/// [server_derive_syntax]: derive.OpenApi.html#servers-attribute-syntax
/// [server]: openapi/server/struct.Server.html
/// [file_uploads]: <https://spec.openapis.org/oas/v3.1.0.html#considerations-for-file-uploads>
pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
let path_attribute = syn::parse_macro_input!(attr as PathAttr);

Expand Down
174 changes: 174 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2789,3 +2789,177 @@ fn derive_into_params_with_ignored_field() {
])
)
}

#[test]
fn derive_octet_stream_request_body() {
#![allow(dead_code)]

#[utoipa::path(
post,
request_body = Vec<u8>,
path = "/test-octet-stream",
responses(
(status = 200, description = "success response")
),
)]
async fn test_octet_stream(_body: Vec<u8>) {}

let operation = serde_json::to_value(__path_test_octet_stream::operation())
.expect("Operation is JSON serializable");
let request_body = operation
.pointer("/requestBody")
.expect("must have request body");

assert_json_eq!(
&request_body,
json!({
"content": {
"application/octet-stream": {
"schema": {
"items": {
"type": "integer",
"format": "int32",
"minimum": 0,
},
"type": "array",
},
},
},
"required": true,
})
);
}

#[test]
fn derive_img_png_request_body() {
#![allow(dead_code)]

#[derive(utoipa::ToSchema)]
#[schema(content_encoding = "base64")]
struct MyPng(String);

#[utoipa::path(
post,
request_body(content = inline(MyPng), content_type = "image/png"),
path = "/test_png",
responses(
(status = 200, description = "success response")
),
)]
async fn test_png(_body: MyPng) {}

let operation =
serde_json::to_value(__path_test_png::operation()).expect("Operation is JSON serializable");
let request_body = operation
.pointer("/requestBody")
.expect("must have request body");

assert_json_eq!(
&request_body,
json!({
"content": {
"image/png": {
"schema": {
"type": "string",
"contentEncoding": "base64"
},
},
},
"required": true,
})
);
}

#[test]
fn derive_multipart_form_data() {
#![allow(dead_code)]

#[derive(utoipa::ToSchema)]
struct MyForm {
order_id: i32,
#[schema(content_media_type = "application/octet-stream")]
file_bytes: Vec<u8>,
}

#[utoipa::path(
post,
request_body(content = inline(MyForm), content_type = "multipart/form-data"),
path = "/test_multipart",
responses(
(status = 200, description = "success response")
),
)]
async fn test_multipart(_body: MyForm) {}

let operation = serde_json::to_value(__path_test_multipart::operation())
.expect("Operation is JSON serializable");
let request_body = operation
.pointer("/requestBody")
.expect("must have request body");

assert_json_eq!(
&request_body,
json!({
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"format": "int32"
},
"file_bytes": {
"type": "array",
"items": {
"type": "integer",
"format": "int32",
"minimum": 0,
},
"contentMediaType": "application/octet-stream"
},
},
"required": ["order_id", "file_bytes"]
},
},
},
"required": true,
})
);
}

#[test]
fn derive_images_as_application_octet_stream() {
#![allow(dead_code)]

#[utoipa::path(
post,
request_body(
content(
("image/png"),
("image/jpg"),
),
),
path = "/test_images",
responses(
(status = 200, description = "success response")
),
)]
async fn test_multipart(_body: Vec<u8>) {}

let operation = serde_json::to_value(__path_test_multipart::operation())
.expect("Operation is JSON serializable");
let request_body = operation
.pointer("/requestBody")
.expect("must have request body");

assert_json_eq!(
&request_body,
json!({
"content": {
"image/jpg": {},
"image/png": {},
},
})
);
}
32 changes: 32 additions & 0 deletions utoipa/src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,24 @@ builder! {
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<Xml>,

/// The `content_encoding` keyword specifies the encoding used to store the contents, as specified in
/// [RFC 2054, part 6.1](https://tools.ietf.org/html/rfc2045) and [RFC 4648](RFC 2054, part 6.1).
///
/// Typically this is either unset for _`string`_ content types which then uses the content
/// encoding of the underlying JSON document. If the content is in _`binary`_ format such as an image or an audio
/// set it to `base64` to encode it as _`Base64`_.
///
/// See more details at <https://json-schema.org/understanding-json-schema/reference/non_json_data#contentencoding>
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content_encoding: String,

/// The _`content_media_type`_ keyword specifies the MIME type of the contents of a string,
/// as described in [RFC 2046](https://tools.ietf.org/html/rfc2046).
///
/// See more details at <https://json-schema.org/understanding-json-schema/reference/non_json_data#contentmediatype>
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content_media_type: String,

/// Optional extensions `x-something`.
#[serde(skip_serializing_if = "Option::is_none", flatten)]
pub extensions: Option<Extensions>,
Expand All @@ -1679,6 +1697,8 @@ impl Default for Array {
min_items: Default::default(),
xml: Default::default(),
extensions: Default::default(),
content_encoding: Default::default(),
content_media_type: Default::default(),
}
}
}
Expand Down Expand Up @@ -1807,6 +1827,18 @@ impl ArrayBuilder {
set_value!(self xml xml)
}

/// Set of change [`Object::content_encoding`]. Typically left empty but could be `base64` for
/// example.
pub fn content_encoding<S: Into<String>>(mut self, content_encoding: S) -> Self {
set_value!(self content_encoding content_encoding.into())
}

/// Set of change [`Object::content_media_type`]. Value must be valid MIME type e.g.
/// `application/json`.
pub fn content_media_type<S: Into<String>>(mut self, content_media_type: S) -> Self {
set_value!(self content_media_type content_media_type.into())
}

/// Add openapi extensions (`x-something`) for [`Array`].
pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
set_value!(self extensions extensions)
Expand Down

0 comments on commit 65ff028

Please sign in to comment.