problem-spec 0.2.0

problem spec(https://www.rfc-editor.org/rfc/rfc7807) lib in rust
Documentation
use crate::status::Status;
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize)]
pub enum ProblemType {
    ValidationError,
    NotFound,
    InternalServerError,
    Custom(String),
}

impl ProblemType {
    fn as_str(&self) -> String {
        match self {
            ProblemType::ValidationError => {
                "https://example.com/problems/validation-error".to_string()
            }
            ProblemType::NotFound => "https://example.com/problems/not-found".to_string(),
            ProblemType::InternalServerError => {
                "https://example.com/problems/internal-server-error".to_string()
            }
            ProblemType::Custom(s) => s.clone(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ExtraKv {
    inner: HashMap<String, String>,
}

impl ExtraKv {
    fn new() -> Self {
        ExtraKv {
            inner: HashMap::new(),
        }
    }

    fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    fn insert(mut self, k: &str, v: &str) -> Self {
        self.inner.insert(k.to_string(), v.to_string());
        self
    }
}

#[derive(Debug, Serialize, Deserialize, Builder)]
pub struct Problem {
    #[serde(rename = "type")]
    #[serde(default = "default_problem_type")]
    pub problem_type: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<Status>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance: Option<String>,

    #[serde(flatten)]
    #[serde(skip_serializing_if = "ExtraKv::is_empty")]
    pub extra: ExtraKv,
}

fn default_problem_type() -> String {
    "about:blank".to_string()
}

impl Problem {
    pub fn new() -> Self {
        Problem {
            problem_type: default_problem_type(),
            title: None,
            status: None,
            detail: None,
            instance: None,
            extra: ExtraKv::new(),
        }
    }

    // pub fn with_extra<T: Serialize>(self, key: &str, value: T) -> Self {
    //     if let Some(mut extra) = self.extra {
    //         extra.insert(key.to_string(), serde_json::to_value(value).unwrap());
    //     }
    //     self
    // }

    pub fn problem_type(mut self, problem_type: ProblemType) -> Self {
        self.problem_type = problem_type.as_str();
        self
    }
}

#[cfg(test)]
mod test {
    use crate::problem_spec::{ExtraKv, Problem, ProblemType};
    use std::collections::HashMap;

    #[test]
    fn test_ok() {
        // let id = ProblemGroup::::default().name("test").display_name("this is a problem!!").build().unwrap();
        // let b = ProblemSpec::builder ::default().id(&id).details("happen").contextual_label("fund-app").build().unwrap();
        //
        //
        // println!("{:?}", b);
        // println!("{:?}", serde_json::to_string(&b).unwrap());
        let extra = ExtraKv::new();

        let problem_spec = Problem::builder()
            .problem_type(ProblemType::ValidationError.as_str())
            .title("happen".to_string())
            .detail("fund".to_string())
            .extra(extra)
            .build();

        println!("{:?}", serde_json::to_string(&problem_spec).unwrap());
    }
}