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::CratestackError;
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>(
24 codec: &C,
25 headers: &HeaderMap,
26 body: &[u8],
27) -> Result<T, CratestackError>
28where
29 C: HttpTransport,
30 T: for<'de> Deserialize<'de>,
31{
32 let content_type = headers
33 .get(axum::http::header::CONTENT_TYPE)
34 .and_then(|value| value.to_str().ok())
35 .unwrap_or(DEFAULT_CONTENT_TYPE);
36 codec.decode_request(content_type, body)
37}
38
39/// Encode an arbitrary serializable value back to bytes using the same
40/// codec as the request. Used by the macro-generated `update` dispatch
41/// arm to re-encode the typed patch before handing it to the existing
42/// update handler as `Bytes`.
43///
44/// Async because the codec's `encode_response` returns an `axum::Response`
45/// whose body has to be buffered out — in practice the codec always
46/// produces an in-memory `Full<Bytes>` body, so this completes in one
47/// poll, but we don't depend on that.
48pub async fn encode_rpc_value<C, T>(
49 codec: &C,
50 headers: &HeaderMap,
51 value: &T,
52) -> Result<Vec<u8>, CratestackError>
53where
54 C: HttpTransport,
55 T: Serialize + ?Sized,
56{
57 let content_type = headers
58 .get(axum::http::header::CONTENT_TYPE)
59 .and_then(|value| value.to_str().ok())
60 .unwrap_or(DEFAULT_CONTENT_TYPE);
61 let response = codec.encode_response(content_type, axum::http::StatusCode::OK, value)?;
62 let (_parts, body) = response.into_parts();
63 let bytes = axum::body::to_bytes(body, cratestack_core::MAX_RESPONSE_REBUFFER_BYTES)
64 .await
65 .map_err(|error| {
66 CratestackError::Internal(format!("failed to buffer encoded RPC body: {error}"))
67 })?;
68 Ok(bytes.to_vec())
69}