atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
27256acf27800b63fd950b95ecc55b2a2486c1c5
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/problem.rsRust91 lines2.5 KB
1
use axum::{
2
    Json,
3
    http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
4
    response::{IntoResponse, Response},
5
};
6
use serde_json::{Map, Value, json};
7
8
pub const PROBLEM_BASE: &str = "https://jelq.dev/problems/";
9
10
/// An RFC 9457 problem document.
11
pub struct Problem(Box<Inner>);
12
13
struct Inner {
14
    status: StatusCode,
15
    slug: &'static str,
16
    detail: String,
17
    extensions: Map<String, Value>,
18
    headers: HeaderMap,
19
}
20
21
impl Problem {
22
    pub fn new(status: StatusCode, slug: &'static str, detail: impl Into<String>) -> Self {
23
        Self(Box::new(Inner {
24
            status,
25
            slug,
26
            detail: detail.into(),
27
            extensions: Map::new(),
28
            headers: HeaderMap::new(),
29
        }))
30
    }
31
32
    pub fn slug(&self) -> &'static str {
33
        self.0.slug
34
    }
35
36
    pub fn status(&self) -> StatusCode {
37
        self.0.status
38
    }
39
40
    pub fn with(mut self, key: &str, value: impl Into<Value>) -> Self {
41
        self.0.extensions.insert(key.into(), value.into());
42
        self
43
    }
44
45
    pub fn document(&self) -> Map<String, Value> {
46
        let problem = &self.0;
47
        let mut body = Map::new();
48
        body.insert(
49
            "type".into(),
50
            json!(format!("{PROBLEM_BASE}{}", problem.slug)),
51
        );
52
        body.insert("title".into(), json!(reason(problem.status)));
53
        body.insert("status".into(), json!(problem.status.as_u16()));
54
        body.insert("detail".into(), json!(problem.detail));
55
        body.extend(problem.extensions.clone());
56
        body
57
    }
58
59
    pub fn header(mut self, name: HeaderName, value: impl AsRef<str>) -> Self {
60
        if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
61
            self.0.headers.append(name, value);
62
        }
63
        self
64
    }
65
}
66
67
// The http crate still uses the RFC 7231 names for these.
68
pub fn reason(status: StatusCode) -> &'static str {
69
    match status.as_u16() {
70
        413 => "Content Too Large",
71
        422 => "Unprocessable Content",
72
        _ => status.canonical_reason().unwrap_or("Unknown"),
73
    }
74
}
75
76
impl IntoResponse for Problem {
77
    fn into_response(self) -> Response {
78
        let body = self.document();
79
        let problem = *self.0;
80
        let mut response = (problem.status, Json(body)).into_response();
81
        let headers = response.headers_mut();
82
        headers.insert(
83
            header::CONTENT_TYPE,
84
            HeaderValue::from_static("application/problem+json"),
85
        );
86
        for (name, value) in &problem.headers {
87
            headers.append(name, value.clone());
88
        }
89
        response
90
    }
91
}