problem_spec/
problem_spec.rs

1use crate::status::Status;
2use bon::Builder;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Serialize, Deserialize)]
7pub enum ProblemType {
8    ValidationError,
9    NotFound,
10    InternalServerError,
11    Custom(String),
12}
13
14impl ProblemType {
15    fn as_str(&self) -> String {
16        match self {
17            ProblemType::ValidationError => {
18                "https://example.com/problems/validation-error".to_string()
19            }
20            ProblemType::NotFound => "https://example.com/problems/not-found".to_string(),
21            ProblemType::InternalServerError => {
22                "https://example.com/problems/internal-server-error".to_string()
23            }
24            ProblemType::Custom(s) => s.clone(),
25        }
26    }
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30pub struct ExtraKv {
31    inner: HashMap<String, String>,
32}
33
34impl ExtraKv {
35    fn new() -> Self {
36        ExtraKv {
37            inner: HashMap::new(),
38        }
39    }
40
41    fn is_empty(&self) -> bool {
42        self.inner.is_empty()
43    }
44
45    fn insert(mut self, k: &str, v: &str) -> Self {
46        self.inner.insert(k.to_string(), v.to_string());
47        self
48    }
49}
50
51#[derive(Debug, Serialize, Deserialize, Builder)]
52pub struct Problem {
53    #[serde(rename = "type")]
54    #[serde(default = "default_problem_type")]
55    pub problem_type: String,
56
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub title: Option<String>,
59
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub status: Option<Status>,
62
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub detail: Option<String>,
65
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub instance: Option<String>,
68
69    #[serde(flatten)]
70    #[serde(skip_serializing_if = "ExtraKv::is_empty")]
71    pub extra: ExtraKv,
72}
73
74fn default_problem_type() -> String {
75    "about:blank".to_string()
76}
77
78impl Problem {
79    pub fn new() -> Self {
80        Problem {
81            problem_type: default_problem_type(),
82            title: None,
83            status: None,
84            detail: None,
85            instance: None,
86            extra: ExtraKv::new(),
87        }
88    }
89
90    // pub fn with_extra<T: Serialize>(self, key: &str, value: T) -> Self {
91    //     if let Some(mut extra) = self.extra {
92    //         extra.insert(key.to_string(), serde_json::to_value(value).unwrap());
93    //     }
94    //     self
95    // }
96
97    pub fn problem_type(mut self, problem_type: ProblemType) -> Self {
98        self.problem_type = problem_type.as_str();
99        self
100    }
101}
102
103#[cfg(test)]
104mod test {
105    use crate::problem_spec::{ExtraKv, Problem, ProblemType};
106    use std::collections::HashMap;
107
108    #[test]
109    fn test_ok() {
110        // let id = ProblemGroup::::default().name("test").display_name("this is a problem!!").build().unwrap();
111        // let b = ProblemSpec::builder ::default().id(&id).details("happen").contextual_label("fund-app").build().unwrap();
112        //
113        //
114        // println!("{:?}", b);
115        // println!("{:?}", serde_json::to_string(&b).unwrap());
116        let extra = ExtraKv::new();
117
118        let problem_spec = Problem::builder()
119            .problem_type(ProblemType::ValidationError.as_str())
120            .title("happen".to_string())
121            .detail("fund".to_string())
122            .extra(extra)
123            .build();
124
125        println!("{:?}", serde_json::to_string(&problem_spec).unwrap());
126    }
127}