axum_service_errors/
lib.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt::{Display, Formatter};
4use std::sync::OnceLock;
5
6use axum::{
7    http::StatusCode,
8    response::{IntoResponse, Response},
9};
10use serde::{Deserialize, Serialize};
11
12/// A parameter value that can be nested and supports various data types.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(untagged)]
15pub enum ParameterValue {
16    String(String),
17    Integer(i64),
18    Float(f64),
19    Boolean(bool),
20    Array(Vec<ParameterValue>),
21    Object(HashMap<String, ParameterValue>),
22    Null,
23}
24
25impl From<String> for ParameterValue {
26    fn from(value: String) -> Self {
27        ParameterValue::String(value)
28    }
29}
30
31impl From<&str> for ParameterValue {
32    fn from(value: &str) -> Self {
33        ParameterValue::String(value.to_string())
34    }
35}
36
37impl From<i32> for ParameterValue {
38    fn from(value: i32) -> Self {
39        ParameterValue::Integer(value as i64)
40    }
41}
42
43impl From<i64> for ParameterValue {
44    fn from(value: i64) -> Self {
45        ParameterValue::Integer(value)
46    }
47}
48
49impl From<f32> for ParameterValue {
50    fn from(value: f32) -> Self {
51        ParameterValue::Float(value as f64)
52    }
53}
54
55impl From<f64> for ParameterValue {
56    fn from(value: f64) -> Self {
57        ParameterValue::Float(value)
58    }
59}
60
61impl From<bool> for ParameterValue {
62    fn from(value: bool) -> Self {
63        ParameterValue::Boolean(value)
64    }
65}
66
67impl From<Vec<ParameterValue>> for ParameterValue {
68    fn from(value: Vec<ParameterValue>) -> Self {
69        ParameterValue::Array(value)
70    }
71}
72
73impl From<HashMap<String, ParameterValue>> for ParameterValue {
74    fn from(value: HashMap<String, ParameterValue>) -> Self {
75        ParameterValue::Object(value)
76    }
77}
78
79impl From<Vec<String>> for ParameterValue {
80    fn from(value: Vec<String>) -> Self {
81        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
82    }
83}
84
85impl From<Vec<&str>> for ParameterValue {
86    fn from(value: Vec<&str>) -> Self {
87        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
88    }
89}
90
91impl From<Vec<i32>> for ParameterValue {
92    fn from(value: Vec<i32>) -> Self {
93        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
94    }
95}
96
97impl From<Vec<i64>> for ParameterValue {
98    fn from(value: Vec<i64>) -> Self {
99        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
100    }
101}
102
103impl From<Vec<f64>> for ParameterValue {
104    fn from(value: Vec<f64>) -> Self {
105        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
106    }
107}
108
109impl From<Vec<bool>> for ParameterValue {
110    fn from(value: Vec<bool>) -> Self {
111        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
112    }
113}
114
115// More ergonomic array creation from various slice types
116impl<T, const N: usize> From<[T; N]> for ParameterValue
117where
118    T: Into<ParameterValue>,
119{
120    fn from(value: [T; N]) -> Self {
121        ParameterValue::Array(value.into_iter().map(|v| v.into()).collect())
122    }
123}
124
125impl<T> From<&[T]> for ParameterValue
126where
127    T: Clone + Into<ParameterValue>,
128{
129    fn from(value: &[T]) -> Self {
130        ParameterValue::Array(value.iter().cloned().map(|v| v.into()).collect())
131    }
132}
133
134// Ergonomic object creation from key-value pairs
135impl<K, V, const N: usize> From<[(K, V); N]> for ParameterValue
136where
137    K: Into<String>,
138    V: Into<ParameterValue>,
139{
140    fn from(value: [(K, V); N]) -> Self {
141        let map = value.into_iter()
142            .map(|(k, v)| (k.into(), v.into()))
143            .collect();
144        ParameterValue::Object(map)
145    }
146}
147
148impl<K, V> From<&[(K, V)]> for ParameterValue
149where
150    K: Clone + Into<String>,
151    V: Clone + Into<ParameterValue>,
152{
153    fn from(value: &[(K, V)]) -> Self {
154        let map = value.iter()
155            .map(|(k, v)| (k.clone().into(), v.clone().into()))
156            .collect();
157        ParameterValue::Object(map)
158    }
159}
160
161impl<K, V> From<Vec<(K, V)>> for ParameterValue
162where
163    K: Into<String>,
164    V: Into<ParameterValue>,
165{
166    fn from(value: Vec<(K, V)>) -> Self {
167        let map = value.into_iter()
168            .map(|(k, v)| (k.into(), v.into()))
169            .collect();
170        ParameterValue::Object(map)
171    }
172}
173
174
175// Convenience functions to create objects from heterogeneous key-value pairs
176impl ParameterValue {
177    /// Create an object from a collection of key-value pairs where values can be of different types.
178    pub fn object_from<I>(items: I) -> Self
179    where
180        I: IntoIterator<Item = (String, ParameterValue)>,
181    {
182        ParameterValue::Object(items.into_iter().collect())
183    }
184
185    /// Create an object using a builder pattern for mixed types
186    pub fn object_builder() -> ObjectBuilder {
187        ObjectBuilder::new()
188    }
189
190    /// Create an array using a builder pattern for mixed types  
191    pub fn array_builder() -> ArrayBuilder {
192        ArrayBuilder::new()
193    }
194
195    /// Create an object by calling a closure with a builder
196    pub fn build_object<F>(f: F) -> Self
197    where
198        F: FnOnce(&mut ObjectBuilder) -> &mut ObjectBuilder,
199    {
200        let mut builder = ObjectBuilder::new();
201        f(&mut builder);
202        builder.build()
203    }
204
205    /// Create an array by calling a closure with a builder
206    pub fn build_array<F>(f: F) -> Self  
207    where
208        F: FnOnce(&mut ArrayBuilder) -> &mut ArrayBuilder,
209    {
210        let mut builder = ArrayBuilder::new();
211        f(&mut builder);
212        builder.build()
213    }
214}
215
216/// Builder for creating ParameterValue objects with mixed types
217pub struct ObjectBuilder {
218    map: HashMap<String, ParameterValue>,
219}
220
221impl ObjectBuilder {
222    pub fn new() -> Self {
223        Self {
224            map: HashMap::new(),
225        }
226    }
227
228    pub fn field(mut self, key: impl Into<String>, value: impl Into<ParameterValue>) -> Self {
229        self.map.insert(key.into(), value.into());
230        self
231    }
232
233    pub fn field_mut(&mut self, key: impl Into<String>, value: impl Into<ParameterValue>) -> &mut Self {
234        self.map.insert(key.into(), value.into());
235        self
236    }
237
238    pub fn build(self) -> ParameterValue {
239        ParameterValue::Object(self.map)
240    }
241}
242
243/// Builder for creating ParameterValue arrays with mixed types
244pub struct ArrayBuilder {
245    items: Vec<ParameterValue>,
246}
247
248impl ArrayBuilder {
249    pub fn new() -> Self {
250        Self {
251            items: Vec::new(),
252        }
253    }
254
255    pub fn push(mut self, value: impl Into<ParameterValue>) -> Self {
256        self.items.push(value.into());
257        self
258    }
259
260    pub fn push_mut(&mut self, value: impl Into<ParameterValue>) -> &mut Self {
261        self.items.push(value.into());
262        self
263    }
264
265    pub fn build(self) -> ParameterValue {
266        ParameterValue::Array(self.items)
267    }
268}
269
270/// Macro to create ParameterValue objects with mixed types easily
271#[macro_export]
272macro_rules! param_object {
273    ($($key:expr => $value:expr),* $(,)?) => {
274        $crate::ParameterValue::object_from([
275            $(($key.to_string(), $crate::ParameterValue::from($value))),*
276        ])
277    };
278}
279
280/// Macro to create ParameterValue arrays with mixed types easily  
281#[macro_export]
282macro_rules! param_array {
283    ($($value:expr),* $(,)?) => {
284        $crate::ParameterValue::Array(vec![
285            $($crate::ParameterValue::from($value)),*
286        ])
287    };
288}
289
290impl Display for ParameterValue {
291    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
292        match self {
293            ParameterValue::String(s) => write!(f, "{}", s),
294            ParameterValue::Integer(i) => write!(f, "{}", i),
295            ParameterValue::Float(float) => write!(f, "{}", float),
296            ParameterValue::Boolean(b) => write!(f, "{}", b),
297            ParameterValue::Array(arr) => {
298                write!(f, "[")?;
299                for (i, item) in arr.iter().enumerate() {
300                    if i > 0 {
301                        write!(f, ", ")?;
302                    }
303                    write!(f, "{}", item)?;
304                }
305                write!(f, "]")
306            }
307            ParameterValue::Object(obj) => {
308                write!(f, "{{")?;
309                for (i, (key, value)) in obj.iter().enumerate() {
310                    if i > 0 {
311                        write!(f, ", ")?;
312                    }
313                    write!(f, "{}: {}", key, value)?;
314                }
315                write!(f, "}}")
316            }
317            ParameterValue::Null => write!(f, "null"),
318        }
319    }
320}
321
322impl ParameterValue {
323    /// Create a new array parameter value.
324    pub fn array(items: Vec<impl Into<ParameterValue>>) -> Self {
325        ParameterValue::Array(items.into_iter().map(|v| v.into()).collect())
326    }
327
328    /// Create a new object parameter value.
329    pub fn object(map: impl Into<HashMap<String, ParameterValue>>) -> Self {
330        ParameterValue::Object(map.into())
331    }
332}
333
334/// A trait for building custom response formats from ServiceError data.
335pub trait ResponseBuilder: std::fmt::Debug + Send + Sync {
336    /// Build a response body and content-type from the error data.
337    fn build(&self, error: &ServiceError) -> (String, &'static str);
338}
339
340/// Global default response builder storage.
341static DEFAULT_RESPONSE_BUILDER: OnceLock<Box<dyn ResponseBuilder>> = OnceLock::new();
342
343/// Set the global default response builder for all ServiceError instances.
344/// This should be called once at application startup.
345pub fn set_default_response_builder(builder: impl ResponseBuilder + 'static) {
346    DEFAULT_RESPONSE_BUILDER.set(Box::new(builder)).ok();
347}
348
349/// Get the global default response builder, if one has been set.
350fn get_default_response_builder() -> Option<&'static Box<dyn ResponseBuilder>> {
351    DEFAULT_RESPONSE_BUILDER.get()
352}
353
354/// A `ServiceError` represents a specific error within the software.
355#[derive(Debug, Serialize, Deserialize)]
356pub struct ServiceError<'a> {
357    /// An internal error code that represents a specific error within the
358    /// system.
359    pub code: u32,
360    /// A capitalized error name that represents the error type.
361    #[serde(borrow)]
362    pub name: Cow<'a, str>,
363    /// The respective HTTP status code that should be returned to the client.
364    #[serde(skip)]
365    pub http_status: u16,
366    /// A human-readable error message that describes the error in more detail.
367    #[serde(borrow)]
368    pub message: Cow<'a, str>,
369    /// Arguments for message formatting
370    #[serde(skip)]
371    pub arguments: Vec<String>,
372    /// Optional parameters as key-value pairs
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub parameters: Option<HashMap<String, ParameterValue>>,
375    /// Custom response builder for formatting output
376    #[serde(skip)]
377    response_builder: Option<Box<dyn ResponseBuilder>>,
378}
379
380impl<'a> Clone for ServiceError<'a> {
381    fn clone(&self) -> Self {
382        Self {
383            code: self.code,
384            name: self.name.clone(),
385            http_status: self.http_status,
386            message: self.message.clone(),
387            arguments: self.arguments.clone(),
388            parameters: self.parameters.clone(),
389            response_builder: None, // Cannot clone trait objects
390        }
391    }
392}
393
394impl<'a> ServiceError<'a> {
395    /// Create a new [`ServiceError`] instance.
396    pub const fn new(code: u32, name: &'a str, status: u16, message: &'a str) -> Self {
397        Self {
398            code,
399            name: Cow::Borrowed(name),
400            http_status: status,
401            message: Cow::Borrowed(message),
402            arguments: Vec::new(),
403            parameters: None,
404            response_builder: None,
405        }
406    }
407
408    /// Add an argument for message formatting.
409    pub fn bind(mut self, value: impl ToString) -> Self {
410        self.arguments.push(value.to_string());
411        self
412    }
413
414    /// Add an optional parameter.
415    pub fn parameter(mut self, key: impl ToString, value: impl Into<ParameterValue>) -> Self {
416        let parameters = self.parameters.get_or_insert_with(HashMap::new);
417        parameters.insert(key.to_string(), value.into());
418        self
419    }
420
421    /// Add multiple parameters at once.
422    pub fn parameters<K, V, I>(mut self, params: I) -> Self
423    where
424        K: Into<String>,
425        V: Into<ParameterValue>,
426        I: IntoIterator<Item = (K, V)>,
427    {
428        let parameters = self.parameters.get_or_insert_with(HashMap::new);
429        for (key, value) in params {
430            parameters.insert(key.into(), value.into());
431        }
432        self
433    }
434
435    /// Set a custom response builder for formatting the response.
436    pub fn with_response_builder(mut self, builder: impl ResponseBuilder + 'static) -> Self {
437        self.response_builder = Some(Box::new(builder));
438        self
439    }
440
441    /// Format the message with provided arguments.
442    fn format_message(&self) -> String {
443        let mut formatted = self.message.to_string();
444        for (i, arg) in self.arguments.iter().enumerate() {
445            let placeholder = format!("{{{i}}}");
446            formatted = formatted.replace(&placeholder, arg);
447        }
448        formatted
449    }
450}
451
452impl<'a> IntoResponse for ServiceError<'a> {
453    fn into_response(self) -> Response {
454        let status_code =
455            StatusCode::from_u16(self.http_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
456
457        let (body, content_type) = if let Some(builder) = &self.response_builder {
458            // Use instance-specific builder
459            builder.build(&self)
460        } else if let Some(default_builder) = get_default_response_builder() {
461            // Use global default builder
462            default_builder.build(&self)
463        } else {
464            // Fallback to plain text format
465            let text = if let Some(ref params) = self.parameters {
466                let param_display: Vec<String> = params
467                    .iter()
468                    .map(|(k, v)| format!("{}: {}", k, v))
469                    .collect();
470                format!(
471                    "Error {}: {} - {} (Parameters: {{{}}})",
472                    self.code,
473                    self.name,
474                    self.format_message(),
475                    param_display.join(", ")
476                )
477            } else {
478                format!(
479                    "Error {}: {} - {}",
480                    self.code,
481                    self.name,
482                    self.format_message()
483                )
484            };
485            (text, "text/plain")
486        };
487
488        (status_code, [("content-type", content_type)], body).into_response()
489    }
490}
491
492/// A simple JSON response builder that serializes the ServiceError as JSON.
493#[cfg(feature = "json")]
494#[derive(Debug, Clone)]
495pub struct JsonResponseBuilder;
496
497#[cfg(feature = "json")]
498impl JsonResponseBuilder {
499    pub fn new() -> Self {
500        Self
501    }
502}
503
504#[cfg(feature = "json")]
505impl ResponseBuilder for JsonResponseBuilder {
506    fn build(&self, error: &ServiceError) -> (String, &'static str) {
507        let response_body = JsonResponseBody {
508            code: error.code,
509            name: error.name.clone(),
510            message: error.format_message(),
511            parameters: error.parameters.clone(),
512        };
513
514        let json = serde_json::to_string(&response_body).unwrap_or_else(|_| {
515            format!("{{\"error\":\"Failed to serialize error {}\"}}", error.code)
516        });
517
518        (json, "application/json")
519    }
520}
521
522#[cfg(feature = "json")]
523#[derive(Debug, Clone, Serialize)]
524struct JsonResponseBody<'a> {
525    code: u32,
526    #[serde(borrow)]
527    name: Cow<'a, str>,
528    message: String,
529    #[serde(skip_serializing_if = "Option::is_none")]
530    parameters: Option<HashMap<String, ParameterValue>>,
531}
532
533/// A simple plain text response builder.
534#[derive(Debug, Clone)]
535pub struct PlainTextResponseBuilder;
536
537impl Default for PlainTextResponseBuilder {
538    fn default() -> Self {
539        Self::new()
540    }
541}
542
543impl PlainTextResponseBuilder {
544    pub fn new() -> Self {
545        Self
546    }
547}
548
549impl ResponseBuilder for PlainTextResponseBuilder {
550    fn build(&self, error: &ServiceError) -> (String, &'static str) {
551        let text = if let Some(ref params) = error.parameters {
552            let param_display: Vec<String> = params
553                .iter()
554                .map(|(k, v)| format!("{}: {}", k, v))
555                .collect();
556            format!(
557                "Error {}: {} - {} (Parameters: {{{}}})",
558                error.code,
559                error.name,
560                error.format_message(),
561                param_display.join(", ")
562            )
563        } else {
564            format!(
565                "Error {}: {} - {}",
566                error.code,
567                error.name,
568                error.format_message()
569            )
570        };
571        (text, "text/plain")
572    }
573}