Skip to main content

modkit_canonical_errors/
problem.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::CanonicalError;
4
5// ---------------------------------------------------------------------------
6// Problem (RFC 9457)
7// ---------------------------------------------------------------------------
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Problem {
11    #[serde(rename = "type")]
12    pub problem_type: String,
13    pub title: String,
14    pub status: u16,
15    pub detail: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub instance: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub trace_id: Option<String>,
20    pub context: serde_json::Value,
21}
22
23impl Problem {
24    /// Convert a `CanonicalError` to a `Problem`.
25    ///
26    /// # Errors
27    ///
28    /// Returns `serde_json::Error` if the error-category context type
29    /// fails to serialize.  Built-in context types are plain structs and
30    /// should never fail, but this keeps the failure visible rather than
31    /// silently producing an empty `"context": {}`.
32    pub fn from_error(err: &CanonicalError) -> Result<Self, serde_json::Error> {
33        let problem_type = format!("gts://{}", err.gts_type());
34        let title = err.title().to_owned();
35        let status = err.status_code();
36        let detail = err.detail().to_owned();
37
38        let mut context = serialize_context(err)?;
39
40        if let Some(rt) = err.resource_type() {
41            context["resource_type"] = serde_json::Value::String(rt.to_owned());
42        }
43
44        if let Some(rn) = err.resource_name() {
45            context["resource_name"] = serde_json::Value::String(rn.to_owned());
46        }
47
48        Ok(Problem {
49            problem_type,
50            title,
51            status,
52            detail,
53            instance: None,
54            trace_id: None,
55            context,
56        })
57    }
58
59    /// Convert a `CanonicalError` to a `Problem`, including the internal
60    /// diagnostic string in the `context` for `Internal` and `Unknown`
61    /// variants.
62    ///
63    /// **This method MUST NOT be used in production.** It exists so that
64    /// development and test environments can surface the real error cause
65    /// in the wire response for easier debugging.
66    ///
67    /// In production, use [`from_error`](Self::from_error) instead — it
68    /// never leaks the diagnostic string.
69    ///
70    /// # Errors
71    ///
72    /// Returns `serde_json::Error` if the context fails to serialize.
73    pub fn from_error_debug(err: &CanonicalError) -> Result<Self, serde_json::Error> {
74        let mut problem = Self::from_error(err)?;
75
76        if let Some(diag) = err.diagnostic() {
77            problem.context["description"] = serde_json::Value::String(diag.to_owned());
78        }
79
80        Ok(problem)
81    }
82
83    /// Set the `trace_id` field, returning `self` for chaining.
84    #[must_use]
85    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
86        self.trace_id = Some(trace_id.into());
87        self
88    }
89
90    /// Set the `instance` field, returning `self` for chaining.
91    #[must_use]
92    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
93        self.instance = Some(instance.into());
94        self
95    }
96}
97
98fn serialize_context(err: &CanonicalError) -> Result<serde_json::Value, serde_json::Error> {
99    match err {
100        CanonicalError::Cancelled { ctx, .. } => serde_json::to_value(ctx),
101        CanonicalError::Unknown { ctx, .. } => serde_json::to_value(ctx),
102        CanonicalError::InvalidArgument { ctx, .. } => serde_json::to_value(ctx),
103        CanonicalError::DeadlineExceeded { ctx, .. } => serde_json::to_value(ctx),
104        CanonicalError::NotFound { ctx, .. } => serde_json::to_value(ctx),
105        CanonicalError::AlreadyExists { ctx, .. } => serde_json::to_value(ctx),
106        CanonicalError::PermissionDenied { ctx, .. } => serde_json::to_value(ctx),
107        CanonicalError::ResourceExhausted { ctx, .. } => serde_json::to_value(ctx),
108        CanonicalError::FailedPrecondition { ctx, .. } => serde_json::to_value(ctx),
109        CanonicalError::Aborted { ctx, .. } => serde_json::to_value(ctx),
110        CanonicalError::OutOfRange { ctx, .. } => serde_json::to_value(ctx),
111        CanonicalError::Unimplemented { ctx, .. } => serde_json::to_value(ctx),
112        CanonicalError::Internal { ctx, .. } => serde_json::to_value(ctx),
113        CanonicalError::ServiceUnavailable { ctx, .. } => serde_json::to_value(ctx),
114        CanonicalError::DataLoss { ctx, .. } => serde_json::to_value(ctx),
115        CanonicalError::Unauthenticated { ctx, .. } => serde_json::to_value(ctx),
116    }
117}
118
119impl From<CanonicalError> for Problem {
120    fn from(err: CanonicalError) -> Self {
121        match Problem::from_error(&err) {
122            Ok(p) => p,
123            Err(ser_err) => Problem {
124                problem_type: format!("gts://{}", err.gts_type()),
125                title: err.title().to_owned(),
126                status: err.status_code(),
127                detail: err.detail().to_owned(),
128                instance: None,
129                trace_id: None,
130                context: serde_json::Value::String(ser_err.to_string()),
131            },
132        }
133    }
134}