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}