Skip to content

Commit a667e48

Browse files
committed
Introduce musli-axum and musli-yew
1 parent b6da7d8 commit a667e48

File tree

12 files changed

+1440
-1
lines changed

12 files changed

+1440
-1
lines changed

.github/workflows/ci.yml

+17
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,23 @@ jobs:
109109
- run: cargo build -p musli --no-default-features --features ${{matrix.base}},simdutf8
110110
- run: cargo build -p musli --no-default-features --features ${{matrix.base}},parse-full
111111

112+
crate_features:
113+
needs: [rustfmt, clippy]
114+
runs-on: ubuntu-latest
115+
strategy:
116+
fail-fast: false
117+
matrix:
118+
crate:
119+
- musli-axum
120+
env:
121+
RUSTFLAGS: -D warnings
122+
steps:
123+
- uses: actions/checkout@v4
124+
- uses: dtolnay/rust-toolchain@stable
125+
- run: cargo build -p ${{matrix.crate}} --no-default-features
126+
- run: cargo build -p ${{matrix.crate}} --no-default-features --features alloc
127+
- run: cargo build -p ${{matrix.crate}} --no-default-features --features std
128+
112129
recursive:
113130
runs-on: ubuntu-latest
114131
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.121"
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+
api = []
22+
json = ["musli/json", "axum/json", "dep:bytes", "dep:mime"]
23+
ws = ["api", "axum/ws", "dep:rand", "tokio/time", "dep:tokio-stream"]
24+
25+
[dependencies]
26+
musli = { path = "../musli", version = "0.0.121", default-features = false }
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/api.rs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Shared traits for defining API types.
2+
3+
use musli::mode::Binary;
4+
use musli::{Decode, Encode};
5+
6+
/// A marker indicating a decodable type.
7+
pub trait Marker: 'static {
8+
/// The type that can be decoded.
9+
type Type<'de>: Decode<'de, Binary>;
10+
}
11+
12+
/// Trait governing requests.
13+
pub trait Request: Encode<Binary> {
14+
/// The kind of the request.
15+
const KIND: &'static str;
16+
17+
/// Type acting as a token for the response.
18+
type Marker: Marker;
19+
}
20+
21+
/// A broadcast type marker.
22+
pub trait Broadcast: Marker {
23+
/// The kind of the broadcast being subscribed to.
24+
const KIND: &'static str;
25+
}
26+
27+
#[derive(Debug, Clone, Copy, Encode, Decode)]
28+
pub struct RequestHeader<'a> {
29+
pub index: u32,
30+
pub serial: u32,
31+
/// The kind of the request.
32+
pub kind: &'a str,
33+
}
34+
35+
#[derive(Debug, Clone, Encode, Decode)]
36+
pub struct ResponseHeader<'de> {
37+
pub index: u32,
38+
pub serial: u32,
39+
/// The response is a broadcast.
40+
#[musli(default, skip_encoding_if = Option::is_none)]
41+
pub broadcast: Option<&'de str>,
42+
/// An error message in the response.
43+
#[musli(default, skip_encoding_if = Option::is_none)]
44+
pub error: Option<&'de str>,
45+
}

crates/musli-axum/src/json.rs

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

crates/musli-axum/src/lib.rs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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(feature = "api")]
24+
pub mod api;
25+
26+
#[cfg(all(feature = "ws", feature = "api", feature = "alloc"))]
27+
pub mod ws;

0 commit comments

Comments
 (0)