lenso_capability_http_endpoint/
response.rs1use std::{error::Error, fmt};
4
5pub use http::{HeaderName, HeaderValue, StatusCode, header};
6use lenso_kernel::RuntimeFailure;
7use serde::Serialize;
8
9use crate::{
10 EndpointHandleInvocationError, HandleError, HandleResponse, HandleResponseHeadersItem, Json,
11};
12
13const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8";
14const PROBLEM_CONTENT_TYPE: &str = "application/problem+json; charset=utf-8";
15const TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8";
16
17#[derive(Debug)]
19pub enum ResponseBuildError {
20 Json(serde_json::Error),
22 NonTextHeaderValue(http::header::ToStrError),
24}
25
26impl fmt::Display for ResponseBuildError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Json(error) => write!(formatter, "response JSON serialization failed: {error}"),
30 Self::NonTextHeaderValue(error) => {
31 write!(formatter, "response header is not valid text: {error}")
32 }
33 }
34 }
35}
36
37impl Error for ResponseBuildError {}
38
39impl From<serde_json::Error> for ResponseBuildError {
40 fn from(error: serde_json::Error) -> Self {
41 Self::Json(error)
42 }
43}
44
45impl From<ResponseBuildError> for EndpointHandleInvocationError {
46 fn from(error: ResponseBuildError) -> Self {
47 Self::Runtime(RuntimeFailure::Internal {
48 detail: error.to_string(),
49 })
50 }
51}
52
53pub trait IntoResponse {
55 fn into_response(self) -> Result<HandleResponse, ResponseBuildError>;
57}
58
59impl IntoResponse for HandleResponse {
60 fn into_response(self) -> Result<HandleResponse, ResponseBuildError> {
61 Ok(self)
62 }
63}
64
65impl<T> IntoResponse for Json<T>
66where
67 T: Serialize,
68{
69 fn into_response(self) -> Result<HandleResponse, ResponseBuildError> {
70 json(StatusCode::OK, &self.0)
71 }
72}
73
74impl<T> IntoResponse for (StatusCode, T)
75where
76 T: IntoResponse,
77{
78 fn into_response(self) -> Result<HandleResponse, ResponseBuildError> {
79 let (status, body) = self;
80 let mut response = body.into_response()?;
81 response.status = i64::from(status.as_u16());
82 Ok(response)
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
88pub struct Problem {
89 #[serde(rename = "type")]
90 type_uri: &'static str,
91 title: String,
92 status: u16,
93 detail: String,
94 code: String,
95}
96
97impl Problem {
98 #[must_use]
100 pub fn new(status: StatusCode, code: impl Into<String>, detail: impl Into<String>) -> Self {
101 Self {
102 type_uri: "about:blank",
103 title: status.canonical_reason().unwrap_or("HTTP error").to_owned(),
104 status: status.as_u16(),
105 detail: detail.into(),
106 code: code.into(),
107 }
108 }
109}
110
111impl IntoResponse for Problem {
112 fn into_response(self) -> Result<HandleResponse, ResponseBuildError> {
113 Ok(with_content_type(
114 StatusCode::from_u16(self.status).expect("Problem stores a validated HTTP status"),
115 PROBLEM_CONTENT_TYPE,
116 serde_json::to_vec(&self).expect("serializing a string-only problem cannot fail"),
117 ))
118 }
119}
120
121#[doc(hidden)]
123pub trait IntoEndpointError {
124 fn into_endpoint_error(self) -> Result<HandleResponse, EndpointHandleInvocationError>;
125}
126
127impl<T> IntoEndpointError for T
128where
129 T: IntoResponse,
130{
131 fn into_endpoint_error(self) -> Result<HandleResponse, EndpointHandleInvocationError> {
132 self.into_response().map_err(Into::into)
133 }
134}
135
136impl IntoEndpointError for EndpointHandleInvocationError {
137 fn into_endpoint_error(self) -> Result<HandleResponse, EndpointHandleInvocationError> {
138 Err(self)
139 }
140}
141
142#[doc(hidden)]
144pub trait IntoEndpointResult {
145 fn into_endpoint_result(self) -> Result<Result<HandleResponse, HandleError>, RuntimeFailure>;
146}
147
148impl<T, E> IntoEndpointResult for Result<T, E>
149where
150 T: IntoResponse,
151 E: IntoEndpointError,
152{
153 fn into_endpoint_result(self) -> Result<Result<HandleResponse, HandleError>, RuntimeFailure> {
154 match self {
155 Ok(response) => {
156 response
157 .into_response()
158 .map(Ok)
159 .map_err(|error| RuntimeFailure::Internal {
160 detail: error.to_string(),
161 })
162 }
163 Err(error) => match error.into_endpoint_error() {
164 Ok(response) => Ok(Ok(response)),
165 Err(EndpointHandleInvocationError::Domain(error)) => Ok(Err(error)),
166 Err(EndpointHandleInvocationError::Runtime(error)) => Err(error),
167 },
168 }
169 }
170}
171
172pub fn json(
174 status: StatusCode,
175 body: &impl Serialize,
176) -> Result<HandleResponse, ResponseBuildError> {
177 Ok(with_content_type(
178 status,
179 JSON_CONTENT_TYPE,
180 serde_json::to_vec(body)?,
181 ))
182}
183
184pub fn problem(
186 status: StatusCode,
187 code: impl Into<String>,
188 detail: impl Into<String>,
189) -> HandleResponse {
190 Problem::new(status, code, detail)
191 .into_response()
192 .expect("serializing a string-only problem cannot fail")
193}
194
195#[must_use]
197pub fn text(status: StatusCode, body: impl Into<String>) -> HandleResponse {
198 with_content_type(status, TEXT_CONTENT_TYPE, body.into().into_bytes())
199}
200
201#[must_use]
203pub fn empty(status: StatusCode) -> HandleResponse {
204 HandleResponse {
205 body: Vec::new().into(),
206 headers: Vec::new(),
207 status: i64::from(status.as_u16()),
208 }
209}
210
211impl HandleResponse {
212 pub fn with_header(
214 mut self,
215 name: &HeaderName,
216 value: &HeaderValue,
217 ) -> Result<Self, ResponseBuildError> {
218 self.headers.push(HandleResponseHeadersItem {
219 name: name.as_str().to_owned(),
220 value: value
221 .to_str()
222 .map_err(ResponseBuildError::NonTextHeaderValue)?
223 .to_owned(),
224 });
225 Ok(self)
226 }
227}
228
229fn with_content_type(
230 status: StatusCode,
231 content_type: &'static str,
232 body: Vec<u8>,
233) -> HandleResponse {
234 HandleResponse {
235 body: body.into(),
236 headers: vec![HandleResponseHeadersItem {
237 name: header::CONTENT_TYPE.as_str().to_owned(),
238 value: content_type.to_owned(),
239 }],
240 status: i64::from(status.as_u16()),
241 }
242}