Skip to content

Commit

Permalink
feat: Re-export dependencies for extractor macro
Browse files Browse the repository at this point in the history
  • Loading branch information
gengteng committed Dec 2, 2023
1 parent 04bb0cd commit f8e46f5
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 43 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "axum-serde"
version = "0.1.2"
description = "Provides multiple serde-based extractors and responders for the Axum web framework"
version = "0.1.3"
description = "Provides multiple serde-based extractors / responses for the Axum web framework, also offers a macro to easily customize extractor / response."
authors = ["GengTeng <me@gteng.org>"]
license = "MIT"
homepage = "https://github.com/gengteng/axum-serde"
Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

**axum-serde** is a library that provides multiple serde-based extractors and responders for the Axum web framework.

If you were using crates like **axum-yaml**, **axum-msgpack** etc. in axum 0.6 and wish to upgrade to axum 0.7, **axum-serde** can be used as a replacement to simplify the migration, without having to modify existing code too much.

## 🚀 Basic usage

* Install
Expand Down Expand Up @@ -68,18 +70,13 @@ async fn main() -> anyhow::Result<()> {

Use the `extractor` macro to create custom extractors with minimal boilerplate:

* Install

```shell
cargo add axum bytes mime serde axum-serde async-trait
```

* Example

```rust,ignore
use axum_serde::extractor;
use serde::de::DeserializeOwned;
use serde::Serialize;
use axum_serde::{
extractor,
macros::{DeserializeOwned, Serialize},
};
extractor!(
MyFormat, // The name of the data format.
Expand All @@ -103,13 +100,15 @@ fn to_vec<T: Serialize>(_value: &T) -> Result<Vec<u8>, String> {

* Test

More `dev-dependencies` are required to run the tests:

```shell
# Add dependencies for extractor / response tests
# Add dev-dependencies for tests
cargo add axum-test --dev
cargo add serde --features derive --dev
cargo add tokio --features macros --dev

# Test the generated myfmt module
# Run the generated tests
cargo test myfmt
```

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![doc = include_str!("../README.md")]
#![deny(unsafe_code, missing_docs, clippy::unwrap_used)]

mod extractor;
pub mod macros;
#[cfg(feature = "msgpack")]
pub mod msgpack;
pub mod rejection;
Expand Down
73 changes: 44 additions & 29 deletions src/extractor.rs → src/macros.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
//! Extractor macro
//!

pub use async_trait::async_trait;
pub use axum::{
extract::{FromRequest, Request},
http,
response::{IntoResponse, Response},
routing, Router,
};
pub use bytes::Bytes;
pub use mime;
pub use serde::{de::DeserializeOwned, Deserialize, Serialize};

/// This macro is designed to create an extractor type.
/// It uses `serde` for extracting data from requests and serializing data into response body.
///
Expand Down Expand Up @@ -154,52 +165,52 @@ macro_rules! extractor {
#[doc = concat!("Construct a `", stringify!($ext), "<T>` from a byte slice.")]
#[doc = concat!("Most users should prefer to use the FromRequest impl but special cases may require first extracting a Request into Bytes then optionally constructing a `", stringify!($ext), "<T>`.")]
pub fn from_bytes(bytes: &[u8]) -> Result<Self, $crate::Rejection<$de_err>>
where T: serde::de::DeserializeOwned
where T: $crate::macros::DeserializeOwned
{
Ok($ext($de(&bytes).map_err($crate::Rejection::InvalidContentFormat)?))
}
}

#[async_trait::async_trait]
impl<T, S> axum::extract::FromRequest<S> for $ext<T>
#[$crate::macros::async_trait]
impl<T, S> $crate::macros::FromRequest<S> for $ext<T>
where
T: serde::de::DeserializeOwned,
T: $crate::macros::DeserializeOwned,
S: Send + Sync,
{
type Rejection = $crate::Rejection<$de_err>;

async fn from_request(
req: axum::extract::Request,
req: $crate::macros::Request,
state: &S,
) -> Result<Self, Self::Rejection> {
if $crate::check_content_type(req.headers(), Self::CONTENT_TYPE) {
let src = bytes::Bytes::from_request(req, state).await?;
let src = $crate::macros::Bytes::from_request(req, state).await?;
Self::from_bytes(&src)
} else {
Err($crate::Rejection::UnsupportedMediaType(Self::CONTENT_TYPE))
}
}
}

impl<T> axum::response::IntoResponse for $ext<T>
impl<T> $crate::macros::IntoResponse for $ext<T>
where
T: serde::Serialize,
T: $crate::macros::Serialize,
{
fn into_response(self) -> axum::response::Response {
fn into_response(self) -> $crate::macros::Response {
match $ser(&self.0) {
Ok(vec) => (
[(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static(Self::CONTENT_TYPE),
$crate::macros::http::header::CONTENT_TYPE,
$crate::macros::http::HeaderValue::from_static(Self::CONTENT_TYPE),
)],
vec,
)
.into_response(),
Err(err) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
$crate::macros::http::StatusCode::INTERNAL_SERVER_ERROR,
[(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
$crate::macros::http::header::CONTENT_TYPE,
$crate::macros::http::HeaderValue::from_static($crate::macros::mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
err.to_string(),
)
Expand All @@ -211,7 +222,16 @@ macro_rules! extractor {
#[cfg(test)]
mod $test {
use super::*;
use serde::{Deserialize, Serialize};
use $crate::macros::{
Bytes,
Router,
routing::{get, post},
{Deserialize, Serialize},
http::{
StatusCode,
header::{HeaderValue, CONTENT_TYPE}
}
};

#[derive(Deserialize, Serialize, Default)]
struct Value {
Expand All @@ -224,10 +244,7 @@ macro_rules! extractor {

#[tokio::test]
async fn extractor() {
use axum::Router;
use axum::routing::post;
use axum_test::TestServer;
use axum_test::http::HeaderValue;

async fn handler($ext(_user): $ext<Value>) {
}
Expand All @@ -238,37 +255,35 @@ macro_rules! extractor {
let server = TestServer::new(my_app).expect("Failed to create test server");

let value = Value::default();
let bytes = bytes::Bytes::from($ser(&value).expect("Failed to serialize value"));
let bytes = Bytes::from($ser(&value).expect("Failed to serialize value"));

let response = server.post(TEST_ROUTE)
.bytes(bytes.clone())
.add_header(axum::http::header::CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
.add_header(CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
.await;

assert_eq!(response.status_code(), axum_test::http::StatusCode::OK);
assert_eq!(response.status_code(), StatusCode::OK);

let response = server.post(TEST_ROUTE)
.bytes(bytes.clone())
.await;
assert_eq!(response.status_code(), axum_test::http::StatusCode::UNSUPPORTED_MEDIA_TYPE);
assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);

let response = server.post(TEST_ROUTE)
.bytes(bytes)
.add_header(axum::http::header::CONTENT_TYPE, HeaderValue::from_static("invalid/type"))
.add_header(CONTENT_TYPE, HeaderValue::from_static("invalid/type"))
.await;
assert_eq!(response.status_code(), axum_test::http::StatusCode::UNSUPPORTED_MEDIA_TYPE);
assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);

let response = server.post(TEST_ROUTE)
.bytes(bytes::Bytes::from_static(b"invalid data"))
.add_header(axum::http::header::CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
.bytes($crate::macros::Bytes::from_static(b"invalid data"))
.add_header(CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
.await;
assert_eq!(response.status_code(), axum_test::http::StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
}

#[tokio::test]
async fn response() {
use axum::Router;
use axum::routing::get;
use axum_test::TestServer;

async fn handler() -> $ext<Value> {
Expand Down

0 comments on commit f8e46f5

Please sign in to comment.