soma_application/
error.rs1use serde::Serialize;
2use soma_domain::errors::ServiceErrorKind;
3
4use crate::{PortError, ProviderError};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9pub struct ApplicationError {
10 pub code: String,
12 pub message: String,
14 pub retryable: bool,
16 pub remediation: String,
18 pub details: Box<ApplicationErrorDetails>,
20 #[serde(skip)]
22 private_diagnostics: Option<String>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
27#[serde(tag = "source", rename_all = "snake_case")]
28pub enum ApplicationErrorDetails {
29 Generic,
31 Provider {
33 schema_version: u32,
35 provider: String,
37 action: Option<String>,
39 provider_error_kind: String,
41 },
42 Service {
44 schema_version: u8,
46 service_error_kind: String,
48 field: Option<String>,
50 bad_value: Option<String>,
52 expected_pattern: Option<String>,
54 reason_kind: Option<String>,
56 available_actions: Vec<String>,
58 },
59}
60
61impl ApplicationError {
62 pub fn new(
64 code: impl Into<String>,
65 message: impl Into<String>,
66 retryable: bool,
67 remediation: impl Into<String>,
68 ) -> Self {
69 Self {
70 code: code.into(),
71 message: message.into(),
72 retryable,
73 remediation: remediation.into(),
74 details: Box::new(ApplicationErrorDetails::Generic),
75 private_diagnostics: None,
76 }
77 }
78
79 pub fn with_details(mut self, details: ApplicationErrorDetails) -> Self {
81 self.details = Box::new(details);
82 self
83 }
84
85 pub fn private_diagnostics(&self) -> Option<&str> {
87 self.private_diagnostics.as_deref()
88 }
89
90 pub(crate) fn service(error: &anyhow::Error) -> Self {
91 let classified = crate::classify_service_error(error);
92 Self {
93 code: classified.code,
94 message: classified.message,
95 retryable: classified.retryable,
96 remediation: classified.remediation,
97 details: Box::new(ApplicationErrorDetails::Service {
98 schema_version: classified.schema_version,
99 service_error_kind: classified.kind.as_str().to_owned(),
100 field: classified.field,
101 bad_value: classified.bad_value,
102 expected_pattern: classified.expected_pattern,
103 reason_kind: classified.reason_kind,
104 available_actions: classified
105 .available_actions
106 .into_iter()
107 .map(ToOwned::to_owned)
108 .collect(),
109 }),
110 private_diagnostics: None,
111 }
112 }
113
114 pub fn service_error_kind(&self) -> Option<&str> {
117 match self.details.as_ref() {
118 ApplicationErrorDetails::Service {
119 service_error_kind, ..
120 } => Some(service_error_kind),
121 ApplicationErrorDetails::Generic | ApplicationErrorDetails::Provider { .. } => None,
122 }
123 }
124
125 pub fn is_validation(&self) -> bool {
127 self.service_error_kind() == Some(ServiceErrorKind::Validation.as_str())
128 }
129
130 pub(crate) fn legacy(operation: &str, error: impl std::fmt::Display) -> Self {
131 let diagnostic = crate::provider_errors::redact_public(&error.to_string());
132 Self::new(
133 "legacy_operation_failed",
134 format!("{operation} failed: {diagnostic}"),
135 false,
136 "Check Soma service status and retry.",
137 )
138 }
139
140 pub(crate) fn not_found(kind: &str, name: &str) -> Self {
141 Self::new(
142 format!("{kind}_not_found"),
143 format!("unknown {kind} `{name}`"),
144 false,
145 format!("List available {kind}s and retry with a known name."),
146 )
147 }
148}
149
150impl From<ProviderError> for ApplicationError {
151 fn from(error: ProviderError) -> Self {
152 let private_diagnostics = error.private_diagnostics().map(ToOwned::to_owned);
153 Self {
154 code: error.code.to_string(),
155 message: error.message.to_string(),
156 retryable: error.retryable,
157 remediation: error.remediation.to_string(),
158 details: Box::new(ApplicationErrorDetails::Provider {
159 schema_version: error.schema_version,
160 provider: error.provider.to_string(),
161 action: error.action.map(Into::into),
162 provider_error_kind: error.kind.to_owned(),
163 }),
164 private_diagnostics,
165 }
166 }
167}
168
169impl From<PortError> for ApplicationError {
170 fn from(error: PortError) -> Self {
171 let message = crate::provider_errors::redact_public(&error.message);
172 Self::new(error.code, message, error.retryable, error.remediation)
173 }
174}
175
176impl std::fmt::Display for ApplicationError {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 write!(f, "{}: {}", self.code, self.message)
179 }
180}
181
182impl std::error::Error for ApplicationError {}