Skip to main content

lenso_capability_http_endpoint/
response.rs

1//! Typed response construction for authored HTTP Endpoint handlers.
2
3use 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/// An authoring-time failure while constructing an HTTP response.
16#[derive(Debug)]
17pub enum ResponseBuildError {
18    /// A typed response value could not be serialized as JSON.
19    Json(serde_json::Error),
20    /// The portable Endpoint contract cannot represent a binary header value.
21    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
51/// Serializes a typed value and returns a JSON response.
52pub 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
63/// Returns an RFC 9457-compatible problem response with a stable extension code.
64pub 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/// Returns a UTF-8 plain-text response.
94#[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/// Returns an empty response without a representation content type.
100#[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    /// Adds one validated HTTP header to an authored response.
111    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}