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
56impl<T, S> axum::extract::FromRequest<S> for Bcs<T>
57where
58 T: serde::de::DeserializeOwned,
59 S: Send + Sync,
60{
61 type Rejection = BcsRejection;
62
63 async fn from_request(
64 req: axum::http::Request<axum::body::Body>,
65 state: &S,
66 ) -> Result<Self, Self::Rejection> {
67 if bcs_content_type(req.headers()) {
68 let bytes = axum::body::Bytes::from_request(req, state)
69 .await
70 .map_err(BcsRejection::BytesRejection)?;
71 bcs::from_bytes(&bytes)
72 .map(Self)
73 .map_err(BcsRejection::DeserializationError)
74 } else {
75 Err(BcsRejection::MissingBcsContentType)
76 }
77 }
78}
79
80fn bcs_content_type(headers: &HeaderMap) -> bool {
81 let Some(ContentType(mime)) = ContentType::from_headers(headers) else {
82 return false;
83 };
84
85 let is_bcs_content_type = mime.type_() == "application"
86 && (mime.subtype() == "bcs" || mime.suffix().is_some_and(|name| name == "bcs"));
87
88 is_bcs_content_type
89}
90
91pub enum BcsRejection {
92 MissingBcsContentType,
93 DeserializationError(bcs::Error),
94 BytesRejection(axum::extract::rejection::BytesRejection),
95}
96
97impl axum::response::IntoResponse for BcsRejection {
98 fn into_response(self) -> axum::response::Response {
99 match self {
100 BcsRejection::MissingBcsContentType => (
101 StatusCode::UNSUPPORTED_MEDIA_TYPE,
102 "Expected request with `Content-Type: application/bcs`",
103 )
104 .into_response(),
105 BcsRejection::DeserializationError(e) => (
106 StatusCode::UNPROCESSABLE_ENTITY,
107 format!("Failed to deserialize the BCS body into the target type: {e}"),
108 )
109 .into_response(),
110 BcsRejection::BytesRejection(bytes_rejection) => bytes_rejection.into_response(),
111 }
112 }
113}
114
115impl<T, J> axum::response::IntoResponse for ResponseContent<T, J>
116where
117 T: serde::Serialize,
118 J: serde::Serialize,
119{
120 fn into_response(self) -> axum::response::Response {
121 match self {
122 ResponseContent::Bcs(inner) => Bcs(inner).into_response(),
123 ResponseContent::Json(inner) => axum::Json(inner).into_response(),
124 }
125 }
126}
127
128pub async fn append_info_headers(
129 State(state): State<RestService>,
130 response: Response,
131) -> impl IntoResponse {
132 let mut headers = HeaderMap::new();
133
134 if let Ok(chain_id) = state.chain_id().to_string().try_into() {
135 headers.insert(X_IOTA_CHAIN_ID, chain_id);
136 }
137
138 if let Ok(chain) = state.chain_id().chain().as_str().try_into() {
139 headers.insert(X_IOTA_CHAIN, chain);
140 }
141
142 if let Ok(latest_checkpoint) = state.reader.inner().get_latest_checkpoint() {
143 headers.insert(X_IOTA_EPOCH, latest_checkpoint.epoch().into());
144 headers.insert(
145 X_IOTA_CHECKPOINT_HEIGHT,
146 latest_checkpoint.sequence_number.into(),
147 );
148 headers.insert(X_IOTA_TIMESTAMP_MS, latest_checkpoint.timestamp_ms.into());
149 }
150
151 if let Ok(lowest_available_checkpoint) = state.reader.inner().get_lowest_available_checkpoint()
152 {
153 headers.insert(
154 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT,
155 lowest_available_checkpoint.into(),
156 );
157 }
158
159 if let Ok(lowest_available_checkpoint_objects) = state
160 .reader
161 .inner()
162 .get_lowest_available_checkpoint_objects()
163 {
164 headers.insert(
165 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
166 lowest_available_checkpoint_objects.into(),
167 );
168 }
169
170 (headers, response)
171}