Skip to content

Commit

Permalink
Automatic body detection.
Browse files Browse the repository at this point in the history
Also rename request_body attribute to body and axum-integration feature to axum
  • Loading branch information
Flowneee committed Jul 21, 2024
1 parent 0ca36ab commit b91eca2
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 106 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async fn echo_get(query: Query<Request>) -> Json<String> {
tags = "echo"
)]
async fn echo_post(
#[request_body(description = "Echo data", required = true)] body: Json<Request>,
#[body(description = "Echo data", required = true)] body: Json<Request>,
) -> Json<String> {
Json(body.0.data)
}
Expand Down
4 changes: 2 additions & 2 deletions okapi-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ axum = "0.7"
axum-extra = { version = "0.9", features = ["typed-header"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
okapi-operation = { path = "../okapi-operation", features = ["default", "yaml", "axum-integration"] }
okapi-operation-macro = { path = "../okapi-operation-macro" }
okapi-operation = { path = "../okapi-operation", features = ["axum", "yaml"] }
okapi-operation-macro = { path = "../okapi-operation-macro", features = ["axum"] }
18 changes: 15 additions & 3 deletions okapi-examples/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,28 @@ async fn echo_get(query: Query<Request>) -> Json<String> {
tags = "echo"
)]
async fn echo_post(
#[request_body(description = "Echo data", required = true)] body: Json<Request>,
#[body(description = "Echo data", required = true)] body: Json<Request>,
) -> Json<String> {
Json(body.0.data)
}

// Detect schema from known types, Json in this case
#[openapi(
summary = "Echo using PUT request",
operation_id = "echo_put",
tags = "echo"
)]
async fn echo_put(body: Json<Request>) -> Json<String> {
Json(body.0.data)
}

#[tokio::main]
async fn main() {
let app = Router::new()
.route("/echo/get", get(openapi_handler!(echo_get)))
.route("/echo/post", post(openapi_handler!(echo_post)))
.route(
"/echo",
get(oh!(echo_get)).post(oh!(echo_post)).put(oh!(echo_put)),
)
.finish_openapi("/openapi", "Demo", "1.0.0")
.expect("no problem");

Expand Down
7 changes: 7 additions & 0 deletions okapi-operation-macro/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ All notable changes to this project will be documented in the changelog of the r
This project follows the [Semantic Versioning standard](https://semver.org/).


## [Unreleased] - 2024-07-21
### Added
- Feature `axum` for enable axum-specific functionality;
- Request body detection from function arguments for specific frameworks (i.e. axum);
- `#[body]` attribute as replacement for `#[request_body]` (now considered deprecated).


## [0.1.4] - 2024-07-18
### Changed
- `#[request_body]` attribute can be used without braces.
Expand Down
5 changes: 5 additions & 0 deletions okapi-operation-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ repository = "https://github.com/Flowneee/okapi-operation"
proc-macro = true

[dependencies]

darling = "0.14.3"
lazy_static = "1"
proc-macro2 = "1"
quote = "1"
# TODO: bump to 2.x
syn = { version = "1", features = ["full"] }
thiserror = "1"

[features]
axum = []
84 changes: 0 additions & 84 deletions okapi-operation-macro/src/operation/request_body.rs

This file was deleted.

30 changes: 30 additions & 0 deletions okapi-operation-macro/src/operation/request_body/axum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::collections::HashSet;

use syn::{PatType, Type};

use super::{RequestBody, RequestBodyAttrs};
use crate::error::Error;

lazy_static::lazy_static! {
static ref KNOWN_BODY_TYPES: HashSet<&'static str> = [
"Json",
].into_iter().collect();
}

impl RequestBody {
pub(super) fn try_find_axum(pt: &PatType) -> Result<Option<Self>, Error> {
let Type::Path(ref path) = *pt.ty else {
return Ok(None);
};
for pat_seg in path.path.segments.iter().rev() {
if KNOWN_BODY_TYPES.contains(pat_seg.ident.to_string().as_str()) {
return Ok(Some(Self {
argument_type: *pt.ty.clone(),
attrs: RequestBodyAttrs::default(),
}));
}
}

Ok(None)
}
}
122 changes: 122 additions & 0 deletions okapi-operation-macro/src/operation/request_body/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{FnArg, ItemFn, PatType, Path, Type};

use crate::{
error::Error,
utils::{attribute_to_args, quote_option},
};

#[cfg(feature = "axum")]
mod axum;

static REQUEST_BODY_ATTRIBUTE_NAME_DEPRECATED: &str = "request_body";
static REQUEST_BODY_ATTRIBUTE_NAME: &str = "body";

/// Request body definition for inline attribute.
#[derive(Debug, FromMeta, Default)]
struct RequestBodyAttrs {
#[darling(default)]
description: Option<String>,
#[darling(default)]
required: bool,
#[darling(default)]
content: Option<Path>,
}

#[derive(Debug)]
pub(super) struct RequestBody {
attrs: RequestBodyAttrs,
argument_type: Type,
}

impl RequestBody {
/// Create body definition from function signature.
pub(super) fn from_item_fn(item_fn: &mut ItemFn) -> Result<Option<Self>, Error> {
for pt in item_fn.sig.inputs.iter_mut().filter_map(|x| match x {
FnArg::Receiver(_) => None,
FnArg::Typed(y) => Some(y),
}) {
if let Some(x) = Self::try_find_in_arg_attrs(pt)? {
return Ok(Some(x));
}

if let Some(x) = Self::try_find_framework_specific(pt)? {
return Ok(Some(x));
}
}

Ok(None)
}

// NOTE: also removes all related attributes
fn try_find_in_arg_attrs(pt: &mut PatType) -> Result<Option<Self>, Error> {
let mut non_matched_attrs = vec![];
let mut matched_attrs = vec![];

// Check attributes, removing matching
for attr in pt.attrs.drain(..) {
if attr.path.get_ident().map_or(false, |x| {
x == REQUEST_BODY_ATTRIBUTE_NAME || x == REQUEST_BODY_ATTRIBUTE_NAME_DEPRECATED
}) {
matched_attrs.push(attr);
} else {
non_matched_attrs.push(attr);
}
}
pt.attrs = non_matched_attrs;

if matched_attrs.len() > 1 {
return Err(Error::syn_spanned(
pt,
"Only single #[body] argument allowed",
));
}
let Some(attr) = matched_attrs.into_iter().next() else {
return Ok(None);
};
let parsed_attrs = RequestBodyAttrs::from_list(&attribute_to_args(&attr, true)?)?;

Ok(Some(Self {
attrs: parsed_attrs,
argument_type: *pt.ty.clone(),
}))
}

// TODO: allow disable this behaviour
#[allow(unused)]
fn try_find_framework_specific(pt: &PatType) -> Result<Option<Self>, Error> {
#[cfg(feature = "axum")]
if let Some(x) = Self::try_find_axum(pt)? {
return Ok(Some(x));
}

Ok(None)
}
}

impl ToTokens for RequestBody {
fn to_tokens(&self, tokens: &mut TokenStream) {
let description = quote_option(&self.attrs.description);
let required = self.attrs.required;
let content_generator = if let Some(ref x) = self.attrs.content {
quote! {
<#x as ToMediaTypes>::generate
}
} else {
let ty = &self.argument_type;
quote! {
<#ty as ToMediaTypes>::generate
}
};
tokens.extend(quote! {
okapi::openapi3::RequestBody {
description: #description,
required: #required,
content: #content_generator(components)?,
..Default::default()
}
})
}
}
8 changes: 7 additions & 1 deletion okapi-operation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
All notable changes to this project will be documented in the changelog of the respective crates.
This project follows the [Semantic Versioning standard](https://semver.org/).

## [Unreleased] - 2024-07-21
### 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).


## [0.3.0-rc2] - 2024-07-18
### Add
- shorter version of `openapi_handler!` macro - `oh!`.

### Changed
- `#[request_body]` attribute can be used without braces;
- `#[body]` attribute can be used without braces;
- `openapi_handler` in axum integration now accept function with generic parameters;
- switched to using `indexmap` in place of `hashmap` to make produced specs deterministic.

Expand Down
7 changes: 4 additions & 3 deletions okapi-operation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ serde = { version = "1", features = ["derive"] }
default = ["macro"]

macro = ["okapi-operation-macro"]

axum-integration = ["axum", "paste", "tower"]

yaml = ["serde_yaml"]

axum = ["dep:axum", "paste", "tower", "okapi-operation-macro/axum"]
# Deprecated, use feature `axum` instead
axum-integration = ["axum"]

[package.metadata.docs.rs]
all-features = true
15 changes: 12 additions & 3 deletions okapi-operation/docs/axum_integration.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Integration with axum

- [`Integration with axum`](#-integration-with-axum-)
* [Example](#example))
* [Example](#example)
* [Customizing `OpenApiBuilder`](#customizing-openapibuilder)
* [Detecting request body and parameters from arguments](#detecting-request-body-and-parameters-from-arguments)

This module provide integration with [`axum`] based on `#[openapi]` macro.

Expand Down Expand Up @@ -42,7 +43,7 @@ async fn echo_get(query: Query<Request>) -> Json<String> {
tags = "echo"
)]
async fn echo_post(
#[request_body(description = "Echo data", required = true)] body: Json<Request>,
#[body(description = "Echo data", required = true)] body: Json<Request>,
) -> Json<String> {
Json(body.0.data)
}
Expand Down Expand Up @@ -71,7 +72,7 @@ If you need to customize builder template, you can either:

```no_run
use axum::{extract::Query, Json};
use okapi_operation::{axum_integration::*, *};
use okapi_operation::{axum_integratExternal documentationion::*, *};
use serde::Deserialize;
#[tokio::main]
Expand All @@ -92,3 +93,11 @@ async fn main() {
axum::serve(listener, app.into_make_service()).await.unwrap()
}
```

## Detecting request body and parameters from arguments

Request body and some parameters can be automatically detected from function arguments without explicitly marking or describing them. Detection is done simply by type name, i.e. JSON body will be detected from `Json`, `axum::Json`, `reexported::axum::Json`, etc.

Supported request bodies:

* [`axum::extract::Json`]
Loading

0 comments on commit b91eca2

Please sign in to comment.