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

Introduce musli-axum #146

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ jobs:
- json
- value
- serde
- api
env:
RUSTFLAGS: -D warnings
steps:
Expand All @@ -130,9 +131,27 @@ jobs:
- run: cargo check -p musli --no-default-features --features ${{matrix.base}}
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},alloc
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},std
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},std,alloc
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},simdutf8
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},parse-full

crate_features:
needs: [rustfmt, clippy]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
crate:
- musli-axum
env:
RUSTFLAGS: -D warnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo build -p ${{matrix.crate}} --no-default-features
- run: cargo build -p ${{matrix.crate}} --no-default-features --features alloc
- run: cargo build -p ${{matrix.crate}} --no-default-features --features std

recursive:
runs-on: ubuntu-latest
steps:
Expand Down
33 changes: 33 additions & 0 deletions crates/musli-axum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "musli-axum"
version = "0.0.124"
authors = ["John-John Tedro <[email protected]>"]
edition = "2021"
description = """
Types for integrating Müsli with axum.
"""
documentation = "https://docs.rs/musli"
readme = "README.md"
homepage = "https://github.com/udoprog/musli"
repository = "https://github.com/udoprog/musli"
license = "MIT OR Apache-2.0"
keywords = ["framework", "http", "web"]
categories = ["asynchronous", "network-programming", "web-programming::http-server"]

[features]
default = ["alloc", "std", "ws", "json"]
alloc = ["musli/alloc"]
std = ["musli/std"]
json = ["musli/json", "axum/json", "dep:bytes", "dep:mime"]
ws = ["axum/ws", "dep:rand", "tokio/time", "dep:tokio-stream"]

[dependencies]
musli = { path = "../musli", version = "0.0.124", default-features = false, features = ["api"] }

axum = { version = "0.7.5", default-features = false, optional = true }
bytes = { version = "1.6.0", optional = true }
mime = { version = "0.3.17", default-features = false, optional = true }
rand = { version = "0.8.5", default-features = false, optional = true, features = ["small_rng"] }
tracing = { version = "0.1.40", default-features = false }
tokio = { version = "1.37.0", default-features = false, features = ["time"], optional = true }
tokio-stream = { version = "0.1.15", default-features = false, optional = true }
11 changes: 11 additions & 0 deletions crates/musli-axum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# musli-axum

[<img alt="github" src="https://img.shields.io/badge/github-udoprog/musli-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/udoprog/musli)
[<img alt="crates.io" src="https://img.shields.io/crates/v/musli-axum.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/musli-axum)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-musli--axum-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/musli-axum)
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/udoprog/musli/ci.yml?branch=main&style=for-the-badge" height="20">](https://github.com/udoprog/musli/actions?query=branch%3Amain)

This crate provides a set of utilities for working with [Axum] and [Müsli].

[Axum]: https://github.com/tokio-rs/axum
[Müsli]: https://github.com/udoprog/musli
155 changes: 155 additions & 0 deletions crates/musli-axum/src/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use alloc::boxed::Box;
use alloc::string::{String, ToString};

use axum::async_trait;
use axum::extract::rejection::BytesRejection;
use axum::extract::{FromRequest, Request};
use axum::http::header::{self, HeaderValue};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use bytes::{BufMut, Bytes, BytesMut};
use musli::de::DecodeOwned;
use musli::json::Encoding;
use musli::mode::Text;
use musli::Encode;

const ENCODING: Encoding = Encoding::new();

/// A rejection from the JSON extractor.
pub enum JsonRejection {
ContentType,
Report(String),
BytesRejection(BytesRejection),
}

impl From<BytesRejection> for JsonRejection {
#[inline]
fn from(rejection: BytesRejection) -> Self {
JsonRejection::BytesRejection(rejection)
}
}

impl IntoResponse for JsonRejection {
fn into_response(self) -> Response {
let status;
let body;

match self {
JsonRejection::ContentType => {
status = StatusCode::UNSUPPORTED_MEDIA_TYPE;
body = String::from("Expected request with `Content-Type: application/json`");
}
JsonRejection::Report(report) => {
status = StatusCode::BAD_REQUEST;
body = report;
}
JsonRejection::BytesRejection(rejection) => {
return rejection.into_response();
}
}

(
status,
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
body,
)
.into_response()
}
}

/// Encode the given value as JSON.
pub struct Json<T>(pub T);

#[async_trait]
impl<T, S> FromRequest<S> for Json<T>
where
T: DecodeOwned<Text>,
S: Send + Sync,
{
type Rejection = JsonRejection;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if !json_content_type(req.headers()) {
return Err(JsonRejection::ContentType);
}

let bytes = Bytes::from_request(req, state).await?;
Self::from_bytes(&bytes)
}
}

fn json_content_type(headers: &HeaderMap) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
return false;
};

let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
return false;
};

let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
mime
} else {
return false;
};

let is_json_content_type = mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json"));

is_json_content_type
}

impl<T> IntoResponse for Json<T>
where
T: Encode<Text>,
{
fn into_response(self) -> Response {
// Use a small initial capacity of 128 bytes like serde_json::to_vec
// https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
let mut buf = BytesMut::with_capacity(128).writer();

match ENCODING.to_writer(&mut buf, &self.0) {
Ok(()) => (
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
)],
buf.into_inner().freeze(),
)
.into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
err.to_string(),
)
.into_response(),
}
}
}

impl<T> Json<T>
where
T: DecodeOwned<Text>,
{
fn from_bytes(bytes: &[u8]) -> Result<Self, JsonRejection> {
let cx = musli::context::DefaultContext::default();

if let Ok(value) = ENCODING.from_slice_with(&cx, bytes) {
return Ok(Json(value));
}

let report = cx.report();
let report = report.to_string();
Err(JsonRejection::Report(report))
}
}
24 changes: 24 additions & 0 deletions crates/musli-axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! [<img alt="github" src="https://img.shields.io/badge/github-udoprog/musli-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/udoprog/musli)
//! [<img alt="crates.io" src="https://img.shields.io/crates/v/musli-axum.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/musli-axum)
//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-musli--axum-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/musli-axum)
//!
//! This crate provides a set of utilities for working with [Axum] and [Müsli].
//!
//! [Axum]: https://github.com/tokio-rs/axum
//! [Müsli]: https://github.com/udoprog/musli

#![no_std]

#[cfg(feature = "std")]
extern crate std;

#[cfg(feature = "alloc")]
extern crate alloc;

#[cfg(all(feature = "json", feature = "alloc"))]
mod json;
#[cfg(all(feature = "json", feature = "alloc"))]
pub use self::json::Json;

#[cfg(all(feature = "ws", feature = "alloc"))]
pub mod ws;
Loading