Skip to content

Commit

Permalink
Implement ToMediaTypes/ToResponses for additional types (strings, byt…
Browse files Browse the repository at this point in the history
…es, Form)
  • Loading branch information
Flowneee committed Jul 23, 2024
1 parent b91eca2 commit 6e0e554
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: nightly
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
Expand Down
3 changes: 1 addition & 2 deletions okapi-operation-macro/src/operation/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::Meta;

use super::cookie::{Cookie, COOKIE_ATTRIBUTE_NAME};
use crate::{
operation::{
header::{Header, HEADER_ATTRIBUTE_NAME},
Expand All @@ -13,8 +14,6 @@ use crate::{
utils::{meta_to_meta_list, nested_meta_to_meta},
};

use super::cookie::{Cookie, COOKIE_ATTRIBUTE_NAME};

// TODO: support cookie parameters
// TODO: support parameters from function signature

Expand Down
9 changes: 9 additions & 0 deletions okapi-operation-macro/src/operation/request_body/axum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ use super::{RequestBody, RequestBodyAttrs};
use crate::error::Error;

lazy_static::lazy_static! {
// NOTE: `Form` is not enabled because it have different content types
// based on method https://docs.rs/axum/latest/axum/struct.Form.html#as-extractor
static ref KNOWN_BODY_TYPES: HashSet<&'static str> = [
// std types
"String",

// axum types
"Json",

// 3rd party types
"Bytes",
].into_iter().collect();
}

Expand Down
3 changes: 2 additions & 1 deletion okapi-operation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ This project follows the [Semantic Versioning standard](https://semver.org/).
### Added
- Feature `axum` as replacement for `axum-integration` (now considered deprecated);
- Request body detection from function arguments for specific frameworks (i.e. axum);
- `#[body]` attribute as replacement for `#[request_body]` (now considered deprecated).
- `#[body]` attribute as replacement for `#[request_body]` (now considered deprecated);
- `ToMediaTypes`/`ToResponses` implementations for string-like types (String, &'static str, ...) and bytes-like types (Bytes, Vec<u8>, ...), implemented in axum, and `axum::form::Form`.


## [0.3.0-rc2] - 2024-07-18
Expand Down
5 changes: 3 additions & 2 deletions okapi-operation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ repository = "https://github.com/Flowneee/okapi-operation"
okapi-operation-macro = { path = "../okapi-operation-macro", version = "0.1", optional = true }

anyhow = "1"
bytes = "1.4"
http = "1"
indexmap = "2.2.6"
mime = "0.3"
okapi = { version = "0.7.0-rc.1", features = ["preserve_order"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
bytes = "1.4"

axum = { version = "0.7", optional = true }
tower = { version = "0.4", default-features = false, optional = true }
paste = { version = "1", optional = true }
serde_yaml = { version = "0.8", optional = true }
indexmap = "2.2.6"

[dev-dependencies]
axum = "0.7"
Expand Down
4 changes: 3 additions & 1 deletion okapi-operation/docs/axum_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ If you need to customize builder template, you can either:

```no_run
use axum::{extract::Query, Json};
use okapi_operation::{axum_integratExternal documentationion::*, *};
use okapi_operation::{axum_integration::*, *};
use serde::Deserialize;
#[tokio::main]
Expand Down Expand Up @@ -100,4 +100,6 @@ Request body and some parameters can be automatically detected from function arg

Supported request bodies:

* [`String`] (as `text/plain`)
* [`axum::extract::Json`]
* [`bytes::Bytes`] (as `application/octet_stream`)
9 changes: 6 additions & 3 deletions okapi-operation/src/axum_integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ mod router;
mod trait_impls;
mod utils;

use axum::response::{IntoResponse, Response};
use axum::{extract::State, Json};
use axum::{
extract::State,
response::{IntoResponse, Response},
Json,
};
use http::{
header::{self, ACCEPT},
HeaderMap, HeaderValue, StatusCode,
Expand Down Expand Up @@ -143,4 +146,4 @@ macro_rules! openapi_service {
}
}

// tests in tests/axum_integration_macros.rs because of https://github.com/rust-lang/rust/issues/52234
// tests in tests/axum_integration.rs because of https://github.com/rust-lang/rust/issues/52234
44 changes: 41 additions & 3 deletions okapi-operation/src/axum_integration/trait_impls.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
use crate::{impl_to_media_types_for_wrapper, impl_to_responses_for_wrapper};
use axum::{response::Html, Form, Json};
use mime::{APPLICATION_JSON, APPLICATION_WWW_FORM_URLENCODED, TEXT_HTML};
use okapi::{
map,
openapi3::{MediaType, RefOr, Response, Responses},
Map,
};

impl_to_media_types_for_wrapper!(axum::Json<T>, "application/json");
use crate::{
impl_to_media_types_for_wrapper, impl_to_responses_for_wrapper, Components, ToMediaTypes,
ToResponses,
};

impl_to_responses_for_wrapper!(axum::Json<T>);
// Json
impl_to_media_types_for_wrapper!(Json<T>, APPLICATION_JSON.to_string());
impl_to_responses_for_wrapper!(Json<T>);

// Form
impl_to_media_types_for_wrapper!(Form<T>, APPLICATION_WWW_FORM_URLENCODED.to_string());
impl_to_responses_for_wrapper!(Form<T>);

// Html
impl<T> ToMediaTypes for Html<T> {
fn generate(_components: &mut Components) -> Result<Map<String, MediaType>, anyhow::Error> {
Ok(map! {
TEXT_HTML.to_string() => MediaType::default()
})
}
}

impl<T> ToResponses for Html<T> {
fn generate(components: &mut Components) -> Result<Responses, anyhow::Error> {
Ok(Responses {
responses: map! {
"200".into() => RefOr::Object(Response {
content: <Self as ToMediaTypes>::generate(components)?,
..Default::default()
}),
},
..Default::default()
})
}
}
4 changes: 2 additions & 2 deletions okapi-operation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ pub use okapi_operation_macro::openapi;
#[cfg(feature = "axum")]
pub mod axum_integration;

use okapi::openapi3::Operation;

pub use self::{
builder::OpenApiBuilder,
components::{Components, ComponentsBuilder},
to_media_types::ToMediaTypes,
to_responses::ToResponses,
};

use okapi::openapi3::Operation;

mod builder;
mod components;
mod to_media_types;
Expand Down
71 changes: 67 additions & 4 deletions okapi-operation/src/to_media_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub trait ToMediaTypes {
/// ```
#[macro_export]
macro_rules! impl_to_media_types_for_wrapper {
($ty:path, $mime:literal) => {
($ty:path, $mime:expr) => {
impl<T: $crate::schemars::JsonSchema> $crate::ToMediaTypes for $ty {
fn generate(
components: &mut $crate::Components,
Expand All @@ -41,8 +41,71 @@ macro_rules! impl_to_media_types_for_wrapper {
};
}

impl ToMediaTypes for () {
fn generate(_components: &mut Components) -> Result<Map<String, MediaType>, anyhow::Error> {
Ok(okapi::map! {})
macro_rules! forward_impl_to_media_types {
($ty_for:ty, $ty_base:ty) => {
impl $crate::ToMediaTypes for $ty_for {
fn generate(
components: &mut $crate::Components,
) -> Result<
$crate::okapi::Map<String, $crate::okapi::openapi3::MediaType>,
$crate::anyhow::Error,
> {
<$ty_base as $crate::ToMediaTypes>::generate(components)
}
}
};
}

mod impls {
use std::borrow::Cow;

use bytes::{Bytes, BytesMut};
use mime::{APPLICATION_OCTET_STREAM, TEXT_PLAIN};
use okapi::{
map,
openapi3::SchemaObject,
schemars::schema::{InstanceType, SingleOrVec},
};

use super::*;

impl ToMediaTypes for () {
fn generate(_components: &mut Components) -> Result<Map<String, MediaType>, anyhow::Error> {
Ok(map! {})
}
}

impl ToMediaTypes for String {
fn generate(_components: &mut Components) -> Result<Map<String, MediaType>, anyhow::Error> {
Ok(map! {
TEXT_PLAIN.to_string() => MediaType::default()
})
}
}
forward_impl_to_media_types!(&'static str, String);
forward_impl_to_media_types!(Cow<'static, str>, String);

impl ToMediaTypes for Vec<u8> {
fn generate(_components: &mut Components) -> Result<Map<String, MediaType>, anyhow::Error> {
// In schemars Bytes defined as array of integers, but OpenAPI recommend
// use string type with binary format
// https://swagger.io/docs/specification/describing-request-body/file-upload/
let schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
format: Some("binary".into()),
..SchemaObject::default()
};

Ok(map! {
APPLICATION_OCTET_STREAM.to_string() => MediaType {
schema: Some(schema),
..MediaType::default()
},
})
}
}
forward_impl_to_media_types!(&'static [u8], Vec<u8>);
forward_impl_to_media_types!(Cow<'static, [u8]>, Vec<u8>);
forward_impl_to_media_types!(Bytes, Vec<u8>);
forward_impl_to_media_types!(BytesMut, Vec<u8>);
}
52 changes: 52 additions & 0 deletions okapi-operation/src/to_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,26 @@ macro_rules! impl_to_responses_for_wrapper {
};
}

macro_rules! forward_impl_to_responses {
($ty_for:ty, $ty_base:ty) => {
impl $crate::ToResponses for $ty_for {
fn generate(
components: &mut $crate::Components,
) -> Result<$crate::okapi::openapi3::Responses, $crate::anyhow::Error> {
<$ty_base as $crate::ToResponses>::generate(components)
}
}
};
}

mod impls {
use std::borrow::Cow;

use bytes::{Bytes, BytesMut};
use okapi::openapi3::Response;

use super::*;
use crate::ToMediaTypes;

impl ToResponses for () {
fn generate(_components: &mut Components) -> Result<Responses, anyhow::Error> {
Expand Down Expand Up @@ -84,4 +102,38 @@ mod impls {
Ok(ok)
}
}

impl ToResponses for String {
fn generate(components: &mut Components) -> Result<Responses, anyhow::Error> {
Ok(Responses {
responses: okapi::map! {
"200".into() => RefOr::Object(Response {
content: <Self as ToMediaTypes>::generate(components)?,
..Default::default()
})
},
..Default::default()
})
}
}
forward_impl_to_responses!(&'static str, String);
forward_impl_to_responses!(Cow<'static, str>, String);

impl ToResponses for Vec<u8> {
fn generate(components: &mut Components) -> Result<Responses, anyhow::Error> {
Ok(Responses {
responses: okapi::map! {
"200".into() => RefOr::Object(Response {
content: <Self as ToMediaTypes>::generate(components)?,
..Default::default()
})
},
..Default::default()
})
}
}
forward_impl_to_responses!(&'static [u8], Vec<u8>);
forward_impl_to_responses!(Cow<'static, [u8]>, Vec<u8>);
forward_impl_to_responses!(Bytes, Vec<u8>);
forward_impl_to_responses!(BytesMut, Vec<u8>);
}
27 changes: 27 additions & 0 deletions okapi-operation/tests/axum_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,33 @@ mod openapi {

assert_eq!(body_schema, expected_schema);
}

#[test]
fn string_body_detection() {
#[openapi]
async fn handle(_arg: String) {}

let schema = Router::<()>::new()
.route("/", get(oh!(handle)))
.generate_openapi_builder()
.build()
.expect("Schema generation shoildn't fail");

let operation = schema.paths["/"]
.clone()
.get
.expect("GET / should be present")
.request_body
.expect("GET / request body should be present");
let RefOr::Object(request_body) = operation else {
panic!("GET / request body should be RefOr::Object");
};

assert!(
request_body.content["text/plain"].clone().schema.is_none(),
"String body (text/plain) shouldn't have schema"
);
}
}

#[cfg(feature = "axum")]
Expand Down
6 changes: 6 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
unstable_features = true

group_imports = "StdExternalCrate"
imports_granularity = "Crate"
newline_style = "Unix"
reorder_imports = true

0 comments on commit 6e0e554

Please sign in to comment.