Skip to main content

winterbaume_core/protocol/
json.rs

1//! JSON protocol utilities for awsJson1.0/1.1 and REST-JSON services.
2
3use http::header::HeaderName;
4
5use crate::service::MockResponse;
6
7const X_AMZN_ERRORTYPE: HeaderName = HeaderName::from_static("x-amzn-errortype");
8
9/// Create an awsJson1.0/1.1 error response with `__type` and `message` fields.
10///
11/// Used by services like KMS, DynamoDB, SQS, SSM, Logs, etc.
12pub fn json_error_response(status: u16, error_type: &str, message: &str) -> MockResponse {
13    let body = format!(
14        r#"{{"__type":"{}","message":"{}"}}"#,
15        escape_json_string(error_type),
16        escape_json_string(message),
17    );
18    MockResponse::json(status, body)
19}
20
21/// Create a REST-JSON error response with `x-amzn-errortype` header.
22///
23/// Used by services like SES v2, Lambda, EKS, AppConfig, etc.
24/// The error type is communicated via the `x-amzn-errortype` header,
25/// and the body contains `Type` and `Message` fields.
26pub fn rest_json_error(status: u16, code: &str, message: &str) -> MockResponse {
27    let body = format!(
28        r#"{{"Type":"User","Message":"{}"}}"#,
29        escape_json_string(message),
30    );
31    let mut resp = MockResponse::rest_json(status, body);
32    resp.headers.insert(X_AMZN_ERRORTYPE, code.parse().unwrap());
33    resp
34}
35
36/// Check whether a top-level JSON object key is present in the raw request
37/// body. Used by handlers that need to distinguish "field absent" from
38/// "field present with default-equipped struct value", which the typed wire
39/// model collapses when fields are non-Option with `#[serde(default)]`.
40///
41/// Parses the body as JSON and looks for `key` in the top-level object.
42/// Returns `false` for non-object bodies, malformed JSON, or empty bodies.
43pub fn body_has_top_level_field(body: &[u8], key: &str) -> bool {
44    serde_json::from_slice::<serde_json::Value>(body)
45        .ok()
46        .and_then(|v| v.as_object().map(|o| o.contains_key(key)))
47        .unwrap_or(false)
48}
49
50/// Minimal JSON string escaping for error messages.
51fn escape_json_string(s: &str) -> String {
52    s.replace('\\', "\\\\")
53        .replace('"', "\\\"")
54        .replace('\n', "\\n")
55        .replace('\r', "\\r")
56        .replace('\t', "\\t")
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_json_error_response() {
65        let resp = json_error_response(400, "ValidationException", "Missing 'KeyId'");
66        assert_eq!(resp.status, 400);
67        let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
68        assert_eq!(body["__type"], "ValidationException");
69        assert_eq!(body["message"], "Missing 'KeyId'");
70    }
71
72    #[test]
73    fn test_rest_json_error() {
74        let resp = rest_json_error(404, "NotFoundException", "Resource not found");
75        assert_eq!(resp.status, 404);
76        assert_eq!(
77            resp.headers.get("x-amzn-errortype").unwrap(),
78            "NotFoundException"
79        );
80        let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
81        assert_eq!(body["Message"], "Resource not found");
82    }
83
84    #[test]
85    fn test_escape_json_string() {
86        assert_eq!(escape_json_string(r#"say "hi""#), r#"say \"hi\""#);
87        assert_eq!(escape_json_string("line\nnew"), "line\\nnew");
88    }
89}