haste_fhir_operation_error/
lib.rs1use std::{
5 backtrace::Backtrace,
6 borrow::Cow,
7 error::Error,
8 fmt::{self, Debug, Display},
9 panic::Location,
10 sync::atomic::{AtomicBool, Ordering},
11};
12
13use haste_fhir_model::r4::generated::{
14 resources::{OperationOutcome, OperationOutcomeIssue},
15 terminology::{BoundCode, IssueSeverity, IssueType},
16 types::FHIRString,
17};
18
19#[cfg(feature = "derive")]
20pub mod derive;
21
22#[cfg(feature = "axum")]
23pub mod axum;
24
25pub type Source = Box<dyn Error + Send + Sync + 'static>;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum StatusOverride {
31 UnprocessableEntity,
33}
34
35pub struct Hop {
38 message: Cow<'static, str>,
39 location: &'static Location<'static>,
40}
41
42impl Hop {
43 #[must_use]
44 pub fn message(&self) -> &str {
45 &self.message
46 }
47
48 #[must_use]
49 pub fn location(&self) -> &'static Location<'static> {
50 self.location
51 }
52}
53
54impl Debug for Hop {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{:?} at {}", self.message, self.location)
57 }
58}
59
60struct Inner {
61 outcome: OperationOutcome,
62 source: Option<Source>,
63 status_override: Option<StatusOverride>,
64 origin: &'static Location<'static>,
66 trail: Vec<Hop>,
69 backtrace: Option<Backtrace>,
72}
73
74pub struct OperationOutcomeError(Box<Inner>);
80
81static CAPTURE_BACKTRACES: AtomicBool = AtomicBool::new(false);
82
83fn create_operation_outcome(
84 severity: BoundCode<IssueSeverity>,
85 code: BoundCode<IssueType>,
86 diagnostic: String,
87) -> OperationOutcome {
88 OperationOutcome {
89 issue: vec![OperationOutcomeIssue {
90 severity,
91 code,
92 diagnostics: Some(Box::new(FHIRString {
93 value: Some(diagnostic),
94 ..Default::default()
95 })),
96 ..Default::default()
97 }],
98 ..Default::default()
99 }
100}
101
102fn status_for_code(code: &BoundCode<IssueType>) -> u16 {
105 match code.as_str() {
106 Some("invalid" | "structure" | "required" | "value" | "invariant") => 400,
108 Some("login" | "unknown" | "expired") => 401,
110 Some("forbidden" | "security" | "suppressed") => 403,
111 Some("not-found") => 404,
112 Some("business-rule") => 405,
114 Some("conflict") => 409,
115 Some("throttled") => 429,
116 Some("not-supported") => 501,
117 _ => 500,
118 }
119}
120
121impl OperationOutcomeError {
122 pub fn capture_backtraces(enabled: bool) {
126 CAPTURE_BACKTRACES.store(enabled, Ordering::Relaxed);
127 }
128
129 fn build(
130 source: Option<Source>,
131 outcome: OperationOutcome,
132 status_override: Option<StatusOverride>,
133 origin: &'static Location<'static>,
134 ) -> Self {
135 let mut inner = Box::new(Inner {
136 outcome,
137 source,
138 status_override,
139 origin,
140 trail: Vec::new(),
141 backtrace: None,
142 });
143 if CAPTURE_BACKTRACES.load(Ordering::Relaxed) && inner.http_status() >= 500 {
144 inner.backtrace = Some(Backtrace::force_capture());
145 }
146 OperationOutcomeError(inner)
147 }
148
149 #[must_use]
150 #[track_caller]
151 pub fn new(source: Option<Source>, outcome: OperationOutcome) -> Self {
152 Self::build(source, outcome, None, Location::caller())
153 }
154
155 #[must_use]
159 #[track_caller]
160 pub fn unprocessable(outcome: OperationOutcome) -> Self {
161 Self::build(
162 None,
163 outcome,
164 Some(StatusOverride::UnprocessableEntity),
165 Location::caller(),
166 )
167 }
168
169 #[must_use]
170 #[track_caller]
171 pub fn fatal(code: BoundCode<IssueType>, diagnostic: String) -> Self {
172 Self::build(
173 None,
174 create_operation_outcome(IssueSeverity::fatal(), code, diagnostic),
175 None,
176 Location::caller(),
177 )
178 }
179
180 #[must_use]
181 #[track_caller]
182 pub fn error(code: BoundCode<IssueType>, diagnostic: String) -> Self {
183 Self::build(
184 None,
185 create_operation_outcome(IssueSeverity::error(), code, diagnostic),
186 None,
187 Location::caller(),
188 )
189 }
190
191 #[must_use]
192 #[track_caller]
193 pub fn warning(code: BoundCode<IssueType>, diagnostic: String) -> Self {
194 Self::build(
195 None,
196 create_operation_outcome(IssueSeverity::warning(), code, diagnostic),
197 None,
198 Location::caller(),
199 )
200 }
201
202 #[must_use]
203 #[track_caller]
204 pub fn information(code: BoundCode<IssueType>, diagnostic: String) -> Self {
205 Self::build(
206 None,
207 create_operation_outcome(IssueSeverity::information(), code, diagnostic),
208 None,
209 Location::caller(),
210 )
211 }
212
213 #[must_use]
217 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
218 self.0.source = Some(Box::new(source));
219 self
220 }
221
222 #[must_use]
225 #[track_caller]
226 pub fn context(mut self, message: impl Into<Cow<'static, str>>) -> Self {
227 self.0.trail.push(Hop {
228 message: message.into(),
229 location: Location::caller(),
230 });
231 self
232 }
233
234 #[must_use]
235 pub fn outcome(&self) -> &OperationOutcome {
236 &self.0.outcome
237 }
238
239 pub fn push_issue(&mut self, issue: OperationOutcomeIssue) {
240 self.0.outcome.issue.push(issue);
241 }
242
243 #[must_use]
245 pub fn origin(&self) -> &'static Location<'static> {
246 self.0.origin
247 }
248
249 #[must_use]
251 pub fn trail(&self) -> &[Hop] {
252 &self.0.trail
253 }
254
255 #[must_use]
256 pub fn backtrace(&self) -> Option<&Backtrace> {
257 self.0.backtrace.as_ref()
258 }
259
260 pub fn causes(&self) -> impl Iterator<Item = &(dyn Error + 'static)> {
262 std::iter::successors(self.source(), |&error| error.source())
263 }
264
265 #[must_use]
268 pub fn http_status(&self) -> u16 {
269 self.0.http_status()
270 }
271
272 #[must_use]
274 pub fn is_server_error(&self) -> bool {
275 self.http_status() >= 500
276 }
277}
278
279impl Inner {
280 fn http_status(&self) -> u16 {
281 match self.status_override {
282 Some(StatusOverride::UnprocessableEntity) => 422,
283 None => self
284 .outcome
285 .issue
286 .first()
287 .map_or(500, |issue| status_for_code(&issue.code)),
288 }
289 }
290}
291
292pub trait ResultExt {
294 #[must_use]
295 fn context(self, message: impl Into<Cow<'static, str>>) -> Self;
296}
297
298impl<T> ResultExt for Result<T, OperationOutcomeError> {
299 #[track_caller]
300 fn context(self, message: impl Into<Cow<'static, str>>) -> Self {
301 match self {
302 Ok(value) => Ok(value),
303 Err(error) => Err(error.context(message)),
304 }
305 }
306}
307
308fn get_issue_diagnostics(issue: &OperationOutcomeIssue) -> Option<&str> {
309 issue.diagnostics.as_ref().and_then(|d| d.value.as_deref())
310}
311
312impl Display for OperationOutcomeError {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(
315 f,
316 "Operation Error: '{}'",
317 self.0
318 .outcome
319 .issue
320 .iter()
321 .filter_map(get_issue_diagnostics)
322 .collect::<Vec<_>>()
323 .join(", ")
324 )
325 }
326}
327
328struct IssueSummary<'a>(&'a OperationOutcomeIssue);
330
331impl Debug for IssueSummary<'_> {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 write!(
334 f,
335 "{} {}: {:?}",
336 self.0.severity.as_str().unwrap_or("?"),
337 self.0.code.as_str().unwrap_or("?"),
338 get_issue_diagnostics(self.0).unwrap_or("")
339 )
340 }
341}
342
343struct Issues<'a>(&'a [OperationOutcomeIssue]);
344
345impl Debug for Issues<'_> {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 f.debug_list()
348 .entries(self.0.iter().map(IssueSummary))
349 .finish()
350 }
351}
352
353struct Cause<'a>(&'a (dyn Error + 'static));
354
355impl Debug for Cause<'_> {
356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357 Display::fmt(self.0, f)
358 }
359}
360
361struct Causes<'a>(&'a OperationOutcomeError);
363
364impl Debug for Causes<'_> {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 f.debug_list().entries(self.0.causes().map(Cause)).finish()
367 }
368}
369
370impl Debug for OperationOutcomeError {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 let inner = &self.0;
373 let mut s = f.debug_struct("OperationOutcomeError");
374 s.field("status", &inner.http_status())
375 .field("issues", &Issues(&inner.outcome.issue))
376 .field("origin", &format_args!("{}", inner.origin));
377 if !inner.trail.is_empty() {
378 s.field("trail", &inner.trail);
379 }
380 if inner.source.is_some() {
381 s.field("causes", &Causes(self));
382 }
383 if let Some(backtrace) = &inner.backtrace {
384 s.field("backtrace", &format_args!("{backtrace}"));
385 }
386 s.finish()
387 }
388}
389
390impl Error for OperationOutcomeError {
391 fn source(&self) -> Option<&(dyn Error + 'static)> {
392 self.0
393 .source
394 .as_deref()
395 .map(|source| source as &(dyn Error + 'static))
396 }
397}