cratestack_axum/rpc/
grpc_bridge.rs1use axum::http::HeaderMap;
20use axum::response::Response;
21use cratestack_core::CoolErrorResponse;
22use serde::de::DeserializeOwned;
23
24use super::codec_helpers::decode_rpc_body;
25use crate::transport::HttpTransport;
26
27pub async fn bridge_grpc_response<C, T>(
41 response: Response,
42 codec: &C,
43 headers: &HeaderMap,
44) -> Result<T, (String, String)>
45where
46 C: HttpTransport,
47 T: DeserializeOwned,
48{
49 let status = response.status();
50 let body_bytes = match axum::body::to_bytes(response.into_body(), usize::MAX).await {
51 Ok(bytes) => bytes,
52 Err(error) => {
53 return Err((
54 "INTERNAL_ERROR".to_owned(),
55 format!("failed to buffer dispatch response: {error}"),
56 ));
57 }
58 };
59
60 if status.is_success() {
61 decode_rpc_body::<_, T>(codec, headers, &body_bytes)
62 .map_err(|error| (error.code().to_owned(), error.public_message().into_owned()))
63 } else {
64 match decode_rpc_body::<_, CoolErrorResponse>(codec, headers, &body_bytes) {
65 Ok(parsed) => Err((parsed.code, parsed.message)),
66 Err(error) => Err((error.code().to_owned(), error.public_message().into_owned())),
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use axum::http::StatusCode;
74 use cratestack_core::CoolError;
75 use serde::{Deserialize, Serialize};
76
77 use super::*;
78 use crate::transport::encode_transport_result_with_status_for;
79
80 #[derive(Debug, Serialize, Deserialize, PartialEq)]
81 struct Widget {
82 name: String,
83 }
84
85 fn capabilities() -> cratestack_core::RouteTransportCapabilities {
86 cratestack_core::RouteTransportCapabilities {
87 request_types: &["application/cbor"],
88 response_types: &["application/cbor"],
89 default_response_type: "application/cbor",
90 supports_sequence_response: false,
91 }
92 }
93
94 #[tokio::test]
95 async fn success_response_decodes_to_domain_value() {
96 let codec = cratestack_codec_cbor::CborCodec;
97 let headers = HeaderMap::new();
98 let response = encode_transport_result_with_status_for(
99 &codec,
100 &headers,
101 &capabilities(),
102 StatusCode::OK,
103 Ok::<_, CoolError>(Widget {
104 name: "gizmo".to_owned(),
105 }),
106 );
107
108 let result: Result<Widget, _> = bridge_grpc_response(response, &codec, &headers).await;
109 assert_eq!(
110 result.unwrap(),
111 Widget {
112 name: "gizmo".to_owned()
113 }
114 );
115 }
116
117 #[tokio::test]
118 async fn error_response_decodes_to_cool_error_code_and_message() {
119 let codec = cratestack_codec_cbor::CborCodec;
120 let headers = HeaderMap::new();
121 let response = encode_transport_result_with_status_for::<_, Widget>(
122 &codec,
123 &headers,
124 &capabilities(),
125 StatusCode::OK,
126 Err(CoolError::NotFound("widget not found".to_owned())),
127 );
128
129 let result: Result<Widget, _> = bridge_grpc_response(response, &codec, &headers).await;
130 let (code, message) = result.unwrap_err();
131 assert_eq!(code, "NOT_FOUND");
132 assert_eq!(message, "widget not found");
133 }
134}