Skip to main content

cratestack_axum/rpc/
codec_helpers.rs

1//! Codec helpers used by the macro-emitted dispatcher: decode the request
2//! body, re-encode a typed value.
3
4use axum::http::HeaderMap;
5use cratestack_core::CoolError;
6use serde::{Deserialize, Serialize};
7
8use crate::HttpTransport;
9
10pub(super) const DEFAULT_CONTENT_TYPE: &str = "application/cbor";
11
12/// Decode an RPC unary request body into `T`, picking the codec based on
13/// the request's `Content-Type` header. Missing header → CBOR (the
14/// default for the REST binding too).
15///
16/// Used by the macro-generated RPC dispatcher; safe to use directly.
17//
18// TODO: this is nearly identical to `decode_transport_request_for` but
19// differs in the missing-Content-Type fallback — this helper defaults to
20// CBOR, while `decode_transport_request_for` errors with
21// `UnsupportedMediaType`. Reconciling the two would change RPC behavior,
22// so the bodies are kept distinct for now.
23pub fn decode_rpc_body<C, T>(codec: &C, headers: &HeaderMap, body: &[u8]) -> Result<T, CoolError>
24where
25    C: HttpTransport,
26    T: for<'de> Deserialize<'de>,
27{
28    let content_type = headers
29        .get(axum::http::header::CONTENT_TYPE)
30        .and_then(|value| value.to_str().ok())
31        .unwrap_or(DEFAULT_CONTENT_TYPE);
32    codec.decode_request(content_type, body)
33}
34
35/// Encode an arbitrary serializable value back to bytes using the same
36/// codec as the request. Used by the macro-generated `update` dispatch
37/// arm to re-encode the typed patch before handing it to the existing
38/// update handler as `Bytes`.
39///
40/// Async because the codec's `encode_response` returns an `axum::Response`
41/// whose body has to be buffered out — in practice the codec always
42/// produces an in-memory `Full<Bytes>` body, so this completes in one
43/// poll, but we don't depend on that.
44pub async fn encode_rpc_value<C, T>(
45    codec: &C,
46    headers: &HeaderMap,
47    value: &T,
48) -> Result<Vec<u8>, CoolError>
49where
50    C: HttpTransport,
51    T: Serialize + ?Sized,
52{
53    let content_type = headers
54        .get(axum::http::header::CONTENT_TYPE)
55        .and_then(|value| value.to_str().ok())
56        .unwrap_or(DEFAULT_CONTENT_TYPE);
57    let response = codec.encode_response(content_type, axum::http::StatusCode::OK, value)?;
58    let (_parts, body) = response.into_parts();
59    let bytes = axum::body::to_bytes(body, usize::MAX)
60        .await
61        .map_err(|error| {
62            CoolError::Internal(format!("failed to buffer encoded RPC body: {error}"))
63        })?;
64    Ok(bytes.to_vec())
65}