iota_rest_api/
response.rs1use axum::{
6 extract::State,
7 http::{HeaderMap, StatusCode},
8 response::{IntoResponse, Response},
9};
10
11use crate::{
12 APPLICATION_BCS, RestService, TEXT_PLAIN_UTF_8,
13 content_type::ContentType,
14 types::{
15 X_IOTA_CHAIN, X_IOTA_CHAIN_ID, X_IOTA_CHECKPOINT_HEIGHT, X_IOTA_EPOCH,
16 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT, X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
17 X_IOTA_TIMESTAMP_MS,
18 },
19};
20
21pub struct Bcs<T>(pub T);
22
23#[derive(Debug)]
24pub enum ResponseContent<T, J = T> {
25 Bcs(T),
26 Json(J),
27}
28
29impl<T> axum::response::IntoResponse for Bcs<T>
30where
31 T: serde::Serialize,
32{
33 fn into_response(self) -> axum::response::Response {
34 match bcs::to_bytes(&self.0) {
35 Ok(buf) => (
36 [(
37 axum::http::header::CONTENT_TYPE,
38 axum::http::HeaderValue::from_static(APPLICATION_BCS),
39 )],
40 buf,
41 )
42 .into_response(),
43 Err(err) => (
44 StatusCode::INTERNAL_SERVER_ERROR,
45 [(
46 axum::http::header::CONTENT_TYPE,
47 axum::http::HeaderValue::from_static(TEXT_PLAIN_UTF_8),
48 )],
49 err.to_string(),
50 )
51 .into_response(),
52 }
53 }
54}
55
56#[axum::async_trait]
57impl<T, S> axum::extract::FromRequest<S> for Bcs<T>
58where
59 T: serde::de::DeserializeOwned,
60 S: Send + Sync,
61{
62 type Rejection = BcsRejection;
63
64 async fn from_request(
65 req: axum::http::Request<axum::body::Body>,
66 state: &S,
67 ) -> Result<Self, Self::Rejection> {
68 if bcs_content_type(req.headers()) {
69 let bytes = axum::body::Bytes::from_request(req, state)
70 .await
71 .map_err(BcsRejection::BytesRejection)?;
72 bcs::from_bytes(&bytes)
73 .map(Self)
74 .map_err(BcsRejection::DeserializationError)
75 } else {
76 Err(BcsRejection::MissingBcsContentType)
77 }
78 }
79}
80
81fn bcs_content_type(headers: &HeaderMap) -> bool {
82 let Some(ContentType(mime)) = ContentType::from_headers(headers) else {
83 return false;
84 };
85
86 let is_bcs_content_type = mime.type_() == "application"
87 && (mime.subtype() == "bcs" || mime.suffix().is_some_and(|name| name == "bcs"));
88
89 is_bcs_content_type
90}
91
92pub enum BcsRejection {
93 MissingBcsContentType,
94 DeserializationError(bcs::Error),
95 BytesRejection(axum::extract::rejection::BytesRejection),
96}
97
98impl axum::response::IntoResponse for BcsRejection {
99 fn into_response(self) -> axum::response::Response {
100 match self {
101 BcsRejection::MissingBcsContentType => (
102 StatusCode::UNSUPPORTED_MEDIA_TYPE,
103 "Expected request with `Content-Type: application/bcs`",
104 )
105 .into_response(),
106 BcsRejection::DeserializationError(e) => (
107 StatusCode::UNPROCESSABLE_ENTITY,
108 format!("Failed to deserialize the BCS body into the target type: {e}"),
109 )
110 .into_response(),
111 BcsRejection::BytesRejection(bytes_rejection) => bytes_rejection.into_response(),
112 }
113 }
114}
115
116impl<T, J> axum::response::IntoResponse for ResponseContent<T, J>
117where
118 T: serde::Serialize,
119 J: serde::Serialize,
120{
121 fn into_response(self) -> axum::response::Response {
122 match self {
123 ResponseContent::Bcs(inner) => Bcs(inner).into_response(),
124 ResponseContent::Json(inner) => axum::Json(inner).into_response(),
125 }
126 }
127}
128
129pub async fn append_info_headers(
130 State(state): State<RestService>,
131 response: Response,
132) -> impl IntoResponse {
133 let mut headers = HeaderMap::new();
134
135 if let Ok(chain_id) = state.chain_id().to_string().try_into() {
136 headers.insert(X_IOTA_CHAIN_ID, chain_id);
137 }
138
139 if let Ok(chain) = state.chain_id().chain().as_str().try_into() {
140 headers.insert(X_IOTA_CHAIN, chain);
141 }
142
143 if let Ok(latest_checkpoint) = state.reader.inner().get_latest_checkpoint() {
144 headers.insert(X_IOTA_EPOCH, latest_checkpoint.epoch().into());
145 headers.insert(
146 X_IOTA_CHECKPOINT_HEIGHT,
147 latest_checkpoint.sequence_number.into(),
148 );
149 headers.insert(X_IOTA_TIMESTAMP_MS, latest_checkpoint.timestamp_ms.into());
150 }
151
152 if let Ok(lowest_available_checkpoint) = state.reader.inner().get_lowest_available_checkpoint()
153 {
154 headers.insert(
155 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT,
156 lowest_available_checkpoint.into(),
157 );
158 }
159
160 if let Ok(lowest_available_checkpoint_objects) = state
161 .reader
162 .inner()
163 .get_lowest_available_checkpoint_objects()
164 {
165 headers.insert(
166 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
167 lowest_available_checkpoint_objects.into(),
168 );
169 }
170
171 (headers, response)
172}