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::{EndpointHandleInvocationError, HandleResponse, HandleResponseHeadersItem};
10
11const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8";
12const PROBLEM_CONTENT_TYPE: &str = "application/problem+json; charset=utf-8";
13const TEXT_CONTENT_TYPE: &str = "text/plain; charset=utf-8";
14
15#[derive(Debug)]
17pub enum ResponseBuildError {
18 Json(serde_json::Error),
20 NonTextHeaderValue(http::header::ToStrError),
22}
23
24impl fmt::Display for ResponseBuildError {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Json(error) => write!(formatter, "response JSON serialization failed: {error}"),
28 Self::NonTextHeaderValue(error) => {
29 write!(formatter, "response header is not valid text: {error}")
30 }
31 }
32 }
33}
34
35impl Error for ResponseBuildError {}
36
37impl From<serde_json::Error> for ResponseBuildError {
38 fn from(error: serde_json::Error) -> Self {
39 Self::Json(error)
40 }
41}
42
43impl From<ResponseBuildError> for EndpointHandleInvocationError {
44 fn from(error: ResponseBuildError) -> Self {
45 Self::Runtime(RuntimeFailure::Internal {
46 detail: error.to_string(),
47 })
48 }
49}
50
51pub fn json(
53 status: StatusCode,
54 body: &impl Serialize,
55) -> Result<HandleResponse, ResponseBuildError> {
56 Ok(with_content_type(
57 status,
58 JSON_CONTENT_TYPE,
59 serde_json::to_vec(body)?,
60 ))
61}
62
63pub fn problem(
65 status: StatusCode,
66 code: impl Into<String>,
67 detail: impl Into<String>,
68) -> HandleResponse {
69 #[derive(Serialize)]
70 struct Problem {
71 r#type: &'static str,
72 title: String,
73 status: u16,
74 detail: String,
75 code: String,
76 }
77
78 let code = code.into();
79 let problem = Problem {
80 r#type: "about:blank",
81 title: status.canonical_reason().unwrap_or("HTTP error").to_owned(),
82 status: status.as_u16(),
83 detail: detail.into(),
84 code,
85 };
86 with_content_type(
87 status,
88 PROBLEM_CONTENT_TYPE,
89 serde_json::to_vec(&problem).expect("serializing a string-only problem cannot fail"),
90 )
91}
92
93#[must_use]
95pub fn text(status: StatusCode, body: impl Into<String>) -> HandleResponse {
96 with_content_type(status, TEXT_CONTENT_TYPE, body.into().into_bytes())
97}
98
99#[must_use]
101pub fn empty(status: StatusCode) -> HandleResponse {
102 HandleResponse {
103 body: Vec::new().into(),
104 headers: Vec::new(),
105 status: i64::from(status.as_u16()),
106 }
107}
108
109impl HandleResponse {
110 pub fn with_header(
112 mut self,
113 name: &HeaderName,
114 value: &HeaderValue,
115 ) -> Result<Self, ResponseBuildError> {
116 self.headers.push(HandleResponseHeadersItem {
117 name: name.as_str().to_owned(),
118 value: value
119 .to_str()
120 .map_err(ResponseBuildError::NonTextHeaderValue)?
121 .to_owned(),
122 });
123 Ok(self)
124 }
125}
126
127fn with_content_type(
128 status: StatusCode,
129 content_type: &'static str,
130 body: Vec<u8>,
131) -> HandleResponse {
132 HandleResponse {
133 body: body.into(),
134 headers: vec![HandleResponseHeadersItem {
135 name: header::CONTENT_TYPE.as_str().to_owned(),
136 value: content_type.to_owned(),
137 }],
138 status: i64::from(status.as_u16()),
139 }
140}