Skip to content

Commit

Permalink
Introduce musli-axum and musli-yew
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed May 29, 2024
1 parent f83c0c7 commit 0b841b3
Show file tree
Hide file tree
Showing 12 changed files with 1,437 additions and 1 deletion.
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ jobs:
- run: cargo build -p musli --no-default-features --features ${{matrix.base}},simdutf8
- run: cargo build -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.122"
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"]
api = []
json = ["musli/json", "axum/json", "dep:bytes", "dep:mime"]
ws = ["api", "axum/ws", "dep:rand", "tokio/time", "dep:tokio-stream"]

[dependencies]
musli = { path = "../musli", version = "0.0.122", default-features = false }
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
45 changes: 45 additions & 0 deletions crates/musli-axum/src/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Shared traits for defining API types.
use musli::mode::Binary;
use musli::{Decode, Encode};

/// A marker indicating a decodable type.
pub trait Marker: 'static {
/// The type that can be decoded.
type Type<'de>: Decode<'de, Binary>;
}

/// Trait governing requests.
pub trait Request: Encode<Binary> {
/// The kind of the request.
const KIND: &'static str;

/// Type acting as a token for the response.
type Marker: Marker;
}

/// A broadcast type marker.
pub trait Broadcast: Marker {
/// The kind of the broadcast being subscribed to.
const KIND: &'static str;
}

#[derive(Debug, Clone, Copy, Encode, Decode)]
pub struct RequestHeader<'a> {
pub index: u32,
pub serial: u32,
/// The kind of the request.
pub kind: &'a str,
}

#[derive(Debug, Clone, Encode, Decode)]
pub struct ResponseHeader<'de> {
pub index: u32,
pub serial: u32,
/// The response is a broadcast.
#[musli(default, skip_encoding_if = Option::is_none)]
pub broadcast: Option<&'de str>,
/// An error message in the response.
#[musli(default, skip_encoding_if = Option::is_none)]
pub error: Option<&'de str>,
}
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::SystemContext::new();

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))
}
}
27 changes: 27 additions & 0 deletions crates/musli-axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! [<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(feature = "api")]
pub mod api;

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

0 comments on commit 0b841b3

Please sign in to comment.