Skip to main content

rustack_apigatewayv2_http/
response.rs

1//! API Gateway v2 response serialization and error formatting.
2//!
3//! API Gateway v2 uses a JSON body with a lowercase `message` field:
4//! `{"message": "..."}`
5
6use bytes::Bytes;
7use rustack_apigatewayv2_model::error::ApiGatewayV2Error;
8use serde::Serialize;
9
10/// Content type for API Gateway v2 JSON responses.
11pub const CONTENT_TYPE: &str = "application/json";
12
13/// Convert an [`ApiGatewayV2Error`] into a complete HTTP error response.
14///
15/// The response includes:
16/// - HTTP status code from the error
17/// - JSON body with `message` field
18///
19/// # Errors
20///
21/// Returns `ApiGatewayV2Error` if JSON serialization or response construction fails.
22pub fn error_to_response(
23    error: &ApiGatewayV2Error,
24) -> Result<http::Response<Bytes>, ApiGatewayV2Error> {
25    let body_obj = serde_json::json!({
26        "message": error.message,
27    });
28    let body_bytes = serde_json::to_vec(&body_obj).map_err(|e| {
29        ApiGatewayV2Error::internal_error(format!("Failed to serialize error response: {e}"))
30    })?;
31
32    http::Response::builder()
33        .status(error.status_code)
34        .header("content-type", CONTENT_TYPE)
35        .body(Bytes::from(body_bytes))
36        .map_err(|e| {
37            ApiGatewayV2Error::internal_error(format!("Failed to build error response: {e}"))
38        })
39}
40
41/// Build a JSON success response with the given status code.
42///
43/// # Errors
44///
45/// Returns `ApiGatewayV2Error` if serialization fails.
46pub fn json_response(
47    status: u16,
48    body: &impl Serialize,
49) -> Result<http::Response<Bytes>, ApiGatewayV2Error> {
50    let body_bytes = serde_json::to_vec(body).map_err(|e| {
51        ApiGatewayV2Error::internal_error(format!("Failed to serialize response: {e}"))
52    })?;
53    http::Response::builder()
54        .status(status)
55        .header("content-type", CONTENT_TYPE)
56        .body(Bytes::from(body_bytes))
57        .map_err(|e| ApiGatewayV2Error::internal_error(format!("Failed to build response: {e}")))
58}
59
60/// Build an empty response with the given status code.
61///
62/// # Errors
63///
64/// Returns `ApiGatewayV2Error` if response construction fails.
65pub fn empty_response(status: u16) -> Result<http::Response<Bytes>, ApiGatewayV2Error> {
66    http::Response::builder()
67        .status(status)
68        .body(Bytes::new())
69        .map_err(|e| {
70            ApiGatewayV2Error::internal_error(format!("Failed to build empty response: {e}"))
71        })
72}
73
74#[cfg(test)]
75mod tests {
76    use rustack_apigatewayv2_model::error::ApiGatewayV2ErrorCode;
77
78    use super::*;
79
80    #[test]
81    fn test_should_format_error_with_lowercase_message() {
82        let err = ApiGatewayV2Error::with_message(
83            ApiGatewayV2ErrorCode::NotFoundException,
84            "API not found: abc123",
85        );
86        let resp = error_to_response(&err).expect("should build");
87        assert_eq!(resp.status(), http::StatusCode::NOT_FOUND);
88        assert_eq!(
89            resp.headers()
90                .get("content-type")
91                .expect("has content-type"),
92            CONTENT_TYPE,
93        );
94
95        let parsed: serde_json::Value =
96            serde_json::from_slice(resp.body()).expect("valid JSON body");
97        assert_eq!(parsed["message"], "API not found: abc123");
98        // Must NOT have Type field.
99        assert!(parsed.get("Type").is_none());
100    }
101
102    #[test]
103    fn test_should_format_bad_request_error() {
104        let err = ApiGatewayV2Error::with_message(
105            ApiGatewayV2ErrorCode::BadRequestException,
106            "Invalid input",
107        );
108        let resp = error_to_response(&err).expect("should build");
109        assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
110
111        let parsed: serde_json::Value =
112            serde_json::from_slice(resp.body()).expect("valid JSON body");
113        assert_eq!(parsed["message"], "Invalid input");
114    }
115
116    #[test]
117    fn test_should_build_json_success_response() {
118        let body = serde_json::json!({"apiId": "abc123", "name": "my-api"});
119        let resp = json_response(201, &body).expect("should build");
120        assert_eq!(resp.status().as_u16(), 201);
121        assert_eq!(
122            resp.headers()
123                .get("content-type")
124                .expect("has content-type"),
125            CONTENT_TYPE,
126        );
127        assert!(!resp.body().is_empty());
128    }
129
130    #[test]
131    fn test_should_build_empty_response() {
132        let resp = empty_response(204).expect("should build");
133        assert_eq!(resp.status().as_u16(), 204);
134        assert!(resp.body().is_empty());
135    }
136
137    #[test]
138    fn test_should_use_application_json_content_type() {
139        assert_eq!(CONTENT_TYPE, "application/json");
140    }
141
142    #[test]
143    fn test_should_map_all_error_codes_to_correct_status() {
144        let cases = [
145            (ApiGatewayV2ErrorCode::BadRequestException, 400),
146            (ApiGatewayV2ErrorCode::AccessDeniedException, 403),
147            (ApiGatewayV2ErrorCode::NotFoundException, 404),
148            (ApiGatewayV2ErrorCode::ConflictException, 409),
149            (ApiGatewayV2ErrorCode::TooManyRequestsException, 500),
150            (ApiGatewayV2ErrorCode::UnknownOperation, 500),
151        ];
152        for (code, expected_status) in cases {
153            let err = ApiGatewayV2Error::with_message(code, "test");
154            let resp = error_to_response(&err).expect("should build");
155            assert_eq!(
156                resp.status().as_u16(),
157                expected_status,
158                "wrong status for {code:?}",
159            );
160        }
161    }
162
163    #[test]
164    fn test_should_not_include_type_field_in_error() {
165        let err =
166            ApiGatewayV2Error::with_message(ApiGatewayV2ErrorCode::AccessDeniedException, "denied");
167        let resp = error_to_response(&err).expect("should build");
168        let parsed: serde_json::Value = serde_json::from_slice(resp.body()).expect("valid JSON");
169        assert!(parsed.get("Type").is_none());
170        assert!(parsed.get("__type").is_none());
171        assert_eq!(parsed["message"], "denied");
172    }
173}