wp_error/
http_respond.rs

1//! Helpers to build consistent error responses (JSON/text) without
2//! coupling to any specific web framework.
3use crate::{SysErrorCode, http_status_for_reason};
4use serde::Serialize;
5use std::fmt::Display;
6
7#[derive(Debug, Serialize)]
8pub struct ErrorResponse<'a> {
9    pub status: u16,
10    pub sys_code: u16,
11    pub tag: &'a str,
12    pub message: String,
13}
14
15pub fn build_error_response<'a, R: SysErrorCode + Display>(r: &'a R) -> ErrorResponse<'a> {
16    ErrorResponse {
17        status: http_status_for_reason(r),
18        sys_code: r.sys_code(),
19        tag: r.sys_tag(),
20        message: r.to_string(),
21    }
22}
23
24pub fn error_response_json<R: SysErrorCode + Display>(r: &R) -> (u16, String) {
25    let er = build_error_response(r);
26    (
27        er.status,
28        serde_json::to_string(&er).unwrap_or_else(|_| {
29            format!(
30                "{{\"status\":{},\"sys_code\":{},\"tag\":\"{}\",\"message\":\"{}\"}}",
31                er.status,
32                er.sys_code,
33                er.tag,
34                er.message.replace('"', "'")
35            )
36        }),
37    )
38}
39
40pub fn error_response_text<R: SysErrorCode + Display>(r: &R) -> (u16, String) {
41    let er = build_error_response(r);
42    (
43        er.status,
44        format!("[{}] {}: {}", er.tag, er.sys_code, er.message),
45    )
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use std::fmt;
52
53    struct TestError {
54        code: u16,
55        tag: &'static str,
56        msg: String,
57    }
58
59    impl SysErrorCode for TestError {
60        fn sys_code(&self) -> u16 {
61            self.code
62        }
63        fn sys_tag(&self) -> &'static str {
64            self.tag
65        }
66    }
67
68    impl fmt::Display for TestError {
69        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70            write!(f, "{}", self.msg)
71        }
72    }
73
74    #[test]
75    fn test_build_error_response() {
76        let err = TestError {
77            code: 42201,
78            tag: "test.tag",
79            msg: "test message".into(),
80        };
81        let resp = build_error_response(&err);
82        assert_eq!(resp.status, 422);
83        assert_eq!(resp.sys_code, 42201);
84        assert_eq!(resp.tag, "test.tag");
85        assert_eq!(resp.message, "test message");
86    }
87
88    #[test]
89    fn test_error_response_json() {
90        let err = TestError {
91            code: 40401,
92            tag: "conf.core",
93            msg: "not found".into(),
94        };
95        let (status, json) = error_response_json(&err);
96        assert_eq!(status, 404);
97        assert!(json.contains("\"status\":404"));
98        assert!(json.contains("\"sys_code\":40401"));
99        assert!(json.contains("\"tag\":\"conf.core\""));
100        assert!(json.contains("\"message\":\"not found\""));
101    }
102
103    #[test]
104    fn test_error_response_json_with_quotes() {
105        let err = TestError {
106            code: 50001,
107            tag: "test",
108            msg: "error with \"quotes\"".into(),
109        };
110        let (status, json) = error_response_json(&err);
111        assert_eq!(status, 500);
112        // Should properly escape quotes in JSON
113        assert!(json.contains("error with"));
114    }
115
116    #[test]
117    fn test_error_response_text() {
118        let err = TestError {
119            code: 42211,
120            tag: "parse.oml",
121            msg: "syntax error".into(),
122        };
123        let (status, text) = error_response_text(&err);
124        assert_eq!(status, 422);
125        assert_eq!(text, "[parse.oml] 42211: syntax error");
126    }
127
128    #[test]
129    fn test_error_response_text_format() {
130        let err = TestError {
131            code: 50201,
132            tag: "source",
133            msg: "supplier failed".into(),
134        };
135        let (status, text) = error_response_text(&err);
136        assert_eq!(status, 502);
137        assert!(text.starts_with("[source]"));
138        assert!(text.contains("50201"));
139        assert!(text.contains("supplier failed"));
140    }
141
142    #[test]
143    fn test_error_response_serialize() {
144        let resp = ErrorResponse {
145            status: 404,
146            sys_code: 40401,
147            tag: "test",
148            message: "not found".into(),
149        };
150        let json = serde_json::to_string(&resp).unwrap();
151        assert!(json.contains("\"status\":404"));
152        assert!(json.contains("\"sys_code\":40401"));
153    }
154}