Skip to main content

ranvier_http/
body.rs

1//! # Body Ergonomics — Transition-Level Body Extraction
2//!
3//! Provides `JsonBody<T>` — a `Transition` that reads the raw HTTP request body
4//! from the `Bus` (injected by `.post_body()` / `.put_body()` / `.patch_body()`)
5//! and deserializes it as JSON.
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use ranvier_http::prelude::*;
11//! use serde::Deserialize;
12//!
13//! #[derive(Deserialize)]
14//! struct CreateNote { title: String, body: String }
15//!
16//! let create = Axon::new("CreateNote")
17//!     .then(JsonBody::<CreateNote>::new())
18//!     .then(|note: CreateNote| async move {
19//!         format!("Created: {}", note.title)
20//!     });
21//!
22//! Ranvier::http()
23//!     .post_body("/notes", create)
24//!     .run(resources)
25//!     .await?;
26//! ```
27
28use std::marker::PhantomData;
29
30use async_trait::async_trait;
31use bytes::Bytes;
32use http::Response;
33use http::StatusCode;
34use http_body_util::Full;
35use ranvier_core::prelude::*;
36use ranvier_core::transition::ResourceRequirement;
37use serde::de::DeserializeOwned;
38
39use crate::extract::HttpRequestBody;
40use crate::response::IntoResponse;
41
42/// A `Transition` that reads `HttpRequestBody` from the [`Bus`] and deserializes it as JSON.
43///
44/// Requires the route to be registered with `.post_body()`, `.put_body()`, or `.patch_body()`
45/// so that the ingress collects and inserts the body bytes before the circuit runs.
46///
47/// # Type Parameters
48///
49/// - `T`: The deserialization target type. Must implement `serde::DeserializeOwned + Send + 'static`.
50/// - `R`: The resource requirement type for the circuit.
51pub struct JsonBody<T, R = ()> {
52    _phantom: PhantomData<(T, R)>,
53}
54
55impl<T, R> JsonBody<T, R> {
56    pub fn new() -> Self {
57        Self {
58            _phantom: PhantomData,
59        }
60    }
61}
62
63impl<T, R> Default for JsonBody<T, R> {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl<T, R> Clone for JsonBody<T, R> {
70    fn clone(&self) -> Self {
71        Self::new()
72    }
73}
74
75impl<T, R> std::fmt::Debug for JsonBody<T, R> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "JsonBody<{}>", std::any::type_name::<T>())
78    }
79}
80
81#[derive(Debug, thiserror::Error)]
82pub enum JsonBodyError {
83    #[error("missing HttpRequestBody in bus — use .post_body()/.put_body()/.patch_body() for this route")]
84    MissingBody,
85    #[error("failed to parse JSON body: {0}")]
86    ParseError(String),
87}
88
89impl IntoResponse for JsonBodyError {
90    fn into_response(self) -> Response<Full<Bytes>> {
91        let status = match self {
92            Self::MissingBody => StatusCode::INTERNAL_SERVER_ERROR,
93            Self::ParseError(_) => StatusCode::BAD_REQUEST,
94        };
95        Response::builder()
96            .status(status)
97            .body(Full::new(Bytes::from(self.to_string())))
98            .unwrap()
99    }
100}
101
102#[async_trait]
103impl<T, R> Transition<(), T> for JsonBody<T, R>
104where
105    T: DeserializeOwned + Send + Sync + 'static,
106    R: ResourceRequirement + Clone + Send + Sync + 'static,
107{
108    type Error = JsonBodyError;
109    type Resources = R;
110
111    async fn run(&self, _input: (), _res: &R, bus: &mut Bus) -> Outcome<T, JsonBodyError> {
112        let bytes = match bus.read::<HttpRequestBody>() {
113            Some(body) => body.0.clone(),
114            None => return Outcome::Fault(JsonBodyError::MissingBody),
115        };
116
117        match serde_json::from_slice::<T>(&bytes) {
118            Ok(value) => Outcome::Next(value),
119            Err(e) => Outcome::Fault(JsonBodyError::ParseError(e.to_string())),
120        }
121    }
122}