Skip to main content

cratestack_axum/transport/
http_transport.rs

1use axum::http::StatusCode;
2use axum::response::Response;
3use cratestack_core::{CratestackCodec, CratestackError, CratestackErrorResponse};
4use futures_util::Stream;
5use serde::{Deserialize, Serialize};
6
7use crate::codec::encode_codec_response;
8
9use super::CBOR_SEQUENCE_CONTENT_TYPE;
10use super::internal::encode_cbor_sequence_response;
11use super::media_type::media_type_matches;
12use super::stream_sequence::encode_cbor_sequence_stream_response;
13
14pub trait HttpTransport: Clone + Send + Sync + 'static {
15    /// Whether this transport actually has an encoder for `content_type`
16    /// (cratestack#489). `RouteTransportCapabilities::response_types`
17    /// (`cratestack-core`) is a compile-time list of what the transport
18    /// *shape* can carry (e.g. both CBOR and JSON, for every route), not
19    /// what the concrete codec(s) wired into this particular router were
20    /// built with — a router constructed with a single `JsonCodec` still
21    /// emits a `response_types` list that names `application/cbor`.
22    ///
23    /// Two callers narrow that static list to what this transport can
24    /// genuinely produce before any `Accept` matching happens:
25    /// `select_transport_response_content_type` (response negotiation,
26    /// `transport/media_type.rs`) and `encodable_response_types` (the
27    /// `Accept` preflight, `transport/validate.rs`). Each filters through
28    /// this method and hands the already-filtered slice to
29    /// `select_response_content_type` / `validate_transport_accept_header`,
30    /// which take a plain `&[&str]` and never call `can_encode`
31    /// themselves. The result is that the server can never select — or
32    /// pre-approve, then later fail on — a `Content-Type` it has no
33    /// encoder for.
34    ///
35    /// Defaulted rather than required: this trait is public API — and is
36    /// re-exported through both the `cratestack-pg` and `cratestack-api`
37    /// facades via their `pub use cratestack_axum::*`, so it is part of
38    /// two published surfaces, not just this crate's. A required method
39    /// would break any downstream `HttpTransport` impl that isn't one of
40    /// the two in this crate. The default reports every content type as
41    /// encodable, i.e. it preserves exactly the pre-cratestack#489
42    /// behavior (trust the static capability list) for any implementor
43    /// that hasn't opted in yet — such an implementor keeps the #489 bug
44    /// until it overrides this. Both in-repo impls below override it with
45    /// their real answer.
46    fn can_encode(&self, _content_type: &str) -> bool {
47        true
48    }
49
50    fn decode_request<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CratestackError>
51    where
52        T: for<'de> Deserialize<'de>;
53
54    fn encode_response<T>(
55        &self,
56        content_type: &str,
57        status: StatusCode,
58        value: &T,
59    ) -> Result<Response, CratestackError>
60    where
61        T: Serialize + ?Sized;
62
63    fn encode_sequence_response<T>(
64        &self,
65        content_type: &str,
66        status: StatusCode,
67        values: &[T],
68    ) -> Result<Response, CratestackError>
69    where
70        T: Serialize;
71
72    fn encode_sequence_error_response(
73        &self,
74        content_type: &str,
75        status: StatusCode,
76        value: &CratestackErrorResponse,
77    ) -> Result<Response, CratestackError>;
78
79    /// Genuinely incremental counterpart to [`Self::encode_sequence_response`]
80    /// for `@stream` procedures (cratestack#283): `values` is encoded and
81    /// flushed item-by-item via `axum::body::Body::from_stream` instead
82    /// of collected into a `Vec` first. Only meaningful for
83    /// `application/cbor-seq` — implementations reject any other
84    /// `content_type`, mirroring how the higher-level
85    /// `encode_transport_stream_result_with_status_for` only calls this
86    /// when cbor-seq was negotiated (anything else falls back to the
87    /// buffered `encode_sequence_response` path there).
88    fn encode_sequence_stream_response<T, S>(
89        &self,
90        content_type: &str,
91        status: StatusCode,
92        values: S,
93    ) -> Result<Response, CratestackError>
94    where
95        T: Serialize + Send + 'static,
96        S: Stream<Item = Result<T, CratestackError>> + Send + 'static;
97}
98
99impl<C> HttpTransport for C
100where
101    C: CratestackCodec,
102{
103    fn can_encode(&self, content_type: &str) -> bool {
104        media_type_matches(content_type, C::CONTENT_TYPE)
105            || (content_type == CBOR_SEQUENCE_CONTENT_TYPE
106                && C::CONTENT_TYPE == CborCodecMarker::CONTENT_TYPE)
107    }
108
109    fn decode_request<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CratestackError>
110    where
111        T: for<'de> Deserialize<'de>,
112    {
113        if media_type_matches(content_type, C::CONTENT_TYPE) {
114            crate::codec::decode_codec_request(self, body)
115        } else {
116            Err(CratestackError::UnsupportedMediaType(format!(
117                "unsupported request Content-Type {content_type}"
118            )))
119        }
120    }
121
122    fn encode_response<T>(
123        &self,
124        content_type: &str,
125        status: StatusCode,
126        value: &T,
127    ) -> Result<Response, CratestackError>
128    where
129        T: Serialize + ?Sized,
130    {
131        if media_type_matches(content_type, C::CONTENT_TYPE) {
132            encode_codec_response(self, status, value)
133        } else {
134            Err(CratestackError::NotAcceptable(format!(
135                "no encoder configured for response Content-Type {content_type}"
136            )))
137        }
138    }
139
140    fn encode_sequence_response<T>(
141        &self,
142        content_type: &str,
143        status: StatusCode,
144        values: &[T],
145    ) -> Result<Response, CratestackError>
146    where
147        T: Serialize,
148    {
149        if content_type == CBOR_SEQUENCE_CONTENT_TYPE {
150            encode_cbor_sequence_response(self, status, values)
151        } else {
152            self.encode_response(content_type, status, values)
153        }
154    }
155
156    fn encode_sequence_error_response(
157        &self,
158        content_type: &str,
159        status: StatusCode,
160        value: &CratestackErrorResponse,
161    ) -> Result<Response, CratestackError> {
162        if content_type == CBOR_SEQUENCE_CONTENT_TYPE {
163            encode_cbor_sequence_response(self, status, std::slice::from_ref(value))
164        } else {
165            self.encode_response(content_type, status, value)
166        }
167    }
168
169    fn encode_sequence_stream_response<T, S>(
170        &self,
171        content_type: &str,
172        status: StatusCode,
173        values: S,
174    ) -> Result<Response, CratestackError>
175    where
176        T: Serialize + Send + 'static,
177        S: Stream<Item = Result<T, CratestackError>> + Send + 'static,
178    {
179        if content_type == CBOR_SEQUENCE_CONTENT_TYPE {
180            encode_cbor_sequence_stream_response(self.clone(), status, values)
181        } else {
182            Err(CratestackError::NotAcceptable(format!(
183                "incremental sequence streaming requires {CBOR_SEQUENCE_CONTENT_TYPE}, got \
184                 response Content-Type {content_type}"
185            )))
186        }
187    }
188}
189
190pub(crate) struct CborCodecMarker;
191
192impl CborCodecMarker {
193    pub(crate) const CONTENT_TYPE: &'static str = "application/cbor";
194}