Skip to main content

lrwf_core/
problem.rs

1//! RFC 7807 / RFC 9457 Problem Details for HTTP APIs.
2//!
3//! Standard error response format for machine-readable errors.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ProblemDetails {
10    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
11    pub typ: Option<String>,
12    pub title: String,
13    pub status: u16,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub detail: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub instance: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub extensions: Option<HashMap<String, serde_json::Value>>,
20}
21
22impl ProblemDetails {
23    pub fn new(status: u16, title: impl Into<String>) -> Self {
24        Self {
25            typ: None,
26            title: title.into(),
27            status,
28            detail: None,
29            instance: None,
30            extensions: None,
31        }
32    }
33
34    pub fn not_found(resource: impl Into<String>, id: impl std::fmt::Display) -> Self {
35        let r = resource.into();
36        let is = format!("{}", id);
37        Self {
38            typ: Some("https://httpstatuses.com/404".into()),
39            title: "Resource Not Found".into(),
40            status: 404,
41            detail: Some(format!("{} '{}' not found", r, is)),
42            instance: Some(format!("/{}s/{}", r.to_lowercase(), is)),
43            extensions: None,
44        }
45    }
46
47    pub fn validation(errors: Vec<FieldError>) -> Self {
48        let mut ext = HashMap::new();
49        ext.insert(
50            "errors".into(),
51            serde_json::to_value(errors).unwrap_or_default(),
52        );
53        Self {
54            typ: Some("https://httpstatuses.com/400".into()),
55            title: "Validation Failed".into(),
56            status: 400,
57            detail: Some("One or more validation errors occurred".into()),
58            instance: None,
59            extensions: Some(ext),
60        }
61    }
62
63    pub fn with_detail(mut self, d: impl Into<String>) -> Self {
64        self.detail = Some(d.into());
65        self
66    }
67
68    pub fn with_instance(mut self, i: impl Into<String>) -> Self {
69        self.instance = Some(i.into());
70        self
71    }
72
73    pub fn to_error(self) -> crate::error::Error {
74        let title = self.title.clone();
75        let fallback = serde_json::to_string(&self).unwrap_or(title);
76        crate::error::Error::Http(fallback)
77    }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct FieldError {
82    pub field: String,
83    pub message: String,
84}
85
86impl FieldError {
87    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
88        Self {
89            field: field.into(),
90            message: message.into(),
91        }
92    }
93}