Skip to content

Commit 7cb6d15

Browse files
committed
Introduce musli-axum and musli-yew
1 parent 9d2aabe commit 7cb6d15

File tree

19 files changed

+2113
-6
lines changed

19 files changed

+2113
-6
lines changed

.github/workflows/ci.yml

+19
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ jobs:
122122
- json
123123
- value
124124
- serde
125+
- api
125126
env:
126127
RUSTFLAGS: -D warnings
127128
steps:
@@ -130,9 +131,27 @@ jobs:
130131
- run: cargo check -p musli --no-default-features --features ${{matrix.base}}
131132
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},alloc
132133
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},std
134+
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},std,alloc
133135
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},simdutf8
134136
- run: cargo check -p musli --no-default-features --features ${{matrix.base}},parse-full
135137

138+
crate_features:
139+
needs: [rustfmt, clippy]
140+
runs-on: ubuntu-latest
141+
strategy:
142+
fail-fast: false
143+
matrix:
144+
crate:
145+
- musli-axum
146+
env:
147+
RUSTFLAGS: -D warnings
148+
steps:
149+
- uses: actions/checkout@v4
150+
- uses: dtolnay/rust-toolchain@stable
151+
- run: cargo build -p ${{matrix.crate}} --no-default-features
152+
- run: cargo build -p ${{matrix.crate}} --no-default-features --features alloc
153+
- run: cargo build -p ${{matrix.crate}} --no-default-features --features std
154+
136155
recursive:
137156
runs-on: ubuntu-latest
138157
steps:

crates/musli-axum/Cargo.toml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "musli-axum"
3+
version = "0.0.124"
4+
authors = ["John-John Tedro <[email protected]>"]
5+
edition = "2021"
6+
description = """
7+
Types for integrating Müsli with axum.
8+
"""
9+
documentation = "https://docs.rs/musli"
10+
readme = "README.md"
11+
homepage = "https://github.com/udoprog/musli"
12+
repository = "https://github.com/udoprog/musli"
13+
license = "MIT OR Apache-2.0"
14+
keywords = ["framework", "http", "web"]
15+
categories = ["asynchronous", "network-programming", "web-programming::http-server"]
16+
17+
[features]
18+
default = ["alloc", "std", "ws", "json"]
19+
alloc = ["musli/alloc"]
20+
std = ["musli/std"]
21+
json = ["musli/json", "axum/json", "dep:bytes", "dep:mime"]
22+
ws = ["axum/ws", "dep:rand", "tokio/time", "dep:tokio-stream"]
23+
24+
[dependencies]
25+
musli = { path = "../musli", version = "0.0.124", default-features = false, features = ["api"] }
26+
27+
axum = { version = "0.7.5", default-features = false, optional = true }
28+
bytes = { version = "1.6.0", optional = true }
29+
mime = { version = "0.3.17", default-features = false, optional = true }
30+
rand = { version = "0.8.5", default-features = false, optional = true, features = ["small_rng"] }
31+
tracing = { version = "0.1.40", default-features = false }
32+
tokio = { version = "1.37.0", default-features = false, features = ["time"], optional = true }
33+
tokio-stream = { version = "0.1.15", default-features = false, optional = true }

crates/musli-axum/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# musli-axum
2+
3+
[<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)
4+
[<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)
5+
[<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)
6+
[<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)
7+
8+
This crate provides a set of utilities for working with [Axum] and [Müsli].
9+
10+
[Axum]: https://github.com/tokio-rs/axum
11+
[Müsli]: https://github.com/udoprog/musli

crates/musli-axum/src/json.rs

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use alloc::boxed::Box;
2+
use alloc::string::{String, ToString};
3+
4+
use axum::async_trait;
5+
use axum::extract::rejection::BytesRejection;
6+
use axum::extract::{FromRequest, Request};
7+
use axum::http::header::{self, HeaderValue};
8+
use axum::http::{HeaderMap, StatusCode};
9+
use axum::response::{IntoResponse, Response};
10+
use bytes::{BufMut, Bytes, BytesMut};
11+
use musli::de::DecodeOwned;
12+
use musli::json::Encoding;
13+
use musli::mode::Text;
14+
use musli::Encode;
15+
16+
const ENCODING: Encoding = Encoding::new();
17+
18+
/// A rejection from the JSON extractor.
19+
pub enum JsonRejection {
20+
ContentType,
21+
Report(String),
22+
BytesRejection(BytesRejection),
23+
}
24+
25+
impl From<BytesRejection> for JsonRejection {
26+
#[inline]
27+
fn from(rejection: BytesRejection) -> Self {
28+
JsonRejection::BytesRejection(rejection)
29+
}
30+
}
31+
32+
impl IntoResponse for JsonRejection {
33+
fn into_response(self) -> Response {
34+
let status;
35+
let body;
36+
37+
match self {
38+
JsonRejection::ContentType => {
39+
status = StatusCode::UNSUPPORTED_MEDIA_TYPE;
40+
body = String::from("Expected request with `Content-Type: application/json`");
41+
}
42+
JsonRejection::Report(report) => {
43+
status = StatusCode::BAD_REQUEST;
44+
body = report;
45+
}
46+
JsonRejection::BytesRejection(rejection) => {
47+
return rejection.into_response();
48+
}
49+
}
50+
51+
(
52+
status,
53+
[(
54+
header::CONTENT_TYPE,
55+
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
56+
)],
57+
body,
58+
)
59+
.into_response()
60+
}
61+
}
62+
63+
/// Encode the given value as JSON.
64+
pub struct Json<T>(pub T);
65+
66+
#[async_trait]
67+
impl<T, S> FromRequest<S> for Json<T>
68+
where
69+
T: DecodeOwned<Text>,
70+
S: Send + Sync,
71+
{
72+
type Rejection = JsonRejection;
73+
74+
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
75+
if !json_content_type(req.headers()) {
76+
return Err(JsonRejection::ContentType);
77+
}
78+
79+
let bytes = Bytes::from_request(req, state).await?;
80+
Self::from_bytes(&bytes)
81+
}
82+
}
83+
84+
fn json_content_type(headers: &HeaderMap) -> bool {
85+
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
86+
content_type
87+
} else {
88+
return false;
89+
};
90+
91+
let content_type = if let Ok(content_type) = content_type.to_str() {
92+
content_type
93+
} else {
94+
return false;
95+
};
96+
97+
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
98+
mime
99+
} else {
100+
return false;
101+
};
102+
103+
let is_json_content_type = mime.type_() == "application"
104+
&& (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json"));
105+
106+
is_json_content_type
107+
}
108+
109+
impl<T> IntoResponse for Json<T>
110+
where
111+
T: Encode<Text>,
112+
{
113+
fn into_response(self) -> Response {
114+
// Use a small initial capacity of 128 bytes like serde_json::to_vec
115+
// https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
116+
let mut buf = BytesMut::with_capacity(128).writer();
117+
118+
match ENCODING.to_writer(&mut buf, &self.0) {
119+
Ok(()) => (
120+
[(
121+
header::CONTENT_TYPE,
122+
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
123+
)],
124+
buf.into_inner().freeze(),
125+
)
126+
.into_response(),
127+
Err(err) => (
128+
StatusCode::INTERNAL_SERVER_ERROR,
129+
[(
130+
header::CONTENT_TYPE,
131+
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
132+
)],
133+
err.to_string(),
134+
)
135+
.into_response(),
136+
}
137+
}
138+
}
139+
140+
impl<T> Json<T>
141+
where
142+
T: DecodeOwned<Text>,
143+
{
144+
fn from_bytes(bytes: &[u8]) -> Result<Self, JsonRejection> {
145+
let cx = musli::context::DefaultContext::default();
146+
147+
if let Ok(value) = ENCODING.from_slice_with(&cx, bytes) {
148+
return Ok(Json(value));
149+
}
150+
151+
let report = cx.report();
152+
let report = report.to_string();
153+
Err(JsonRejection::Report(report))
154+
}
155+
}

crates/musli-axum/src/lib.rs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! [<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)
2+
//! [<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)
3+
//! [<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)
4+
//!
5+
//! This crate provides a set of utilities for working with [Axum] and [Müsli].
6+
//!
7+
//! [Axum]: https://github.com/tokio-rs/axum
8+
//! [Müsli]: https://github.com/udoprog/musli
9+
10+
#![no_std]
11+
12+
#[cfg(feature = "std")]
13+
extern crate std;
14+
15+
#[cfg(feature = "alloc")]
16+
extern crate alloc;
17+
18+
#[cfg(all(feature = "json", feature = "alloc"))]
19+
mod json;
20+
#[cfg(all(feature = "json", feature = "alloc"))]
21+
pub use self::json::Json;
22+
23+
#[cfg(all(feature = "ws", feature = "alloc"))]
24+
pub mod ws;

0 commit comments

Comments
 (0)