Skip to main content

sova_core/
problem.rs

1//! RFC 7807 Problem Details helpers (`application/problem+json`).
2
3use crate::request_id::current_request_id;
4use crate::response::Response;
5use serde::Serialize;
6use serde_json::{json, Map, Value};
7
8/// Build a Problem Details response.
9///
10/// `extensions` are merged into the JSON object (e.g. `errors`, `instance`).
11pub fn problem_response(
12    status: u16,
13    title: impl Into<String>,
14    detail: impl Into<String>,
15    extensions: impl IntoIterator<Item = (String, Value)>,
16) -> Response {
17    let title = title.into();
18    let detail = detail.into();
19    let mut body = Map::new();
20    body.insert("type".into(), json!("about:blank"));
21    body.insert("title".into(), json!(title));
22    body.insert("status".into(), json!(status));
23    body.insert("detail".into(), json!(detail));
24    if let Some(rid) = current_request_id() {
25        body.insert("request_id".into(), json!(rid));
26    }
27    for (k, v) in extensions {
28        body.insert(k, v);
29    }
30    Response::json(&Value::Object(body))
31        .status(status)
32        .header("content-type", "application/problem+json")
33}
34
35/// Map a framework [`crate::Error`] to Problem Details (API preset).
36pub fn error_to_problem(err: crate::Error) -> Response {
37    use crate::Error;
38    match err {
39        Error::Response(res) => *res,
40        Error::NotFound => problem_response(404, "Not Found", "Not Found", []),
41        Error::Unauthorized => problem_response(401, "Unauthorized", "Unauthorized", []),
42        Error::Forbidden => problem_response(403, "Forbidden", "Forbidden", []),
43        Error::BadRequest(msg) => problem_response(400, "Bad Request", msg, []),
44        Error::PayloadTooLarge => {
45            problem_response(413, "Payload Too Large", "Payload Too Large", [])
46        }
47        Error::MethodNotAllowed => {
48            problem_response(405, "Method Not Allowed", "Method Not Allowed", [])
49        }
50        Error::Internal(msg) => problem_response(500, "Internal Server Error", msg, []),
51        Error::Json(e) => problem_response(400, "Bad Request", format!("JSON error: {e}"), []),
52        Error::Io(e) => {
53            problem_response(500, "Internal Server Error", format!("IO error: {e}"), [])
54        }
55    }
56}
57
58/// Convenience for a field-error list (`errors` array).
59pub fn problem_with_errors<E: Serialize>(
60    status: u16,
61    title: impl Into<String>,
62    detail: impl Into<String>,
63    errors: &[E],
64) -> Response {
65    let errors = serde_json::to_value(errors).unwrap_or(json!([]));
66    problem_response(
67        status,
68        title,
69        detail,
70        [("errors".into(), errors)],
71    )
72}