use std::sync::Arc;
use bytes::Bytes;
use http::HeaderValue;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ErrorResponseContext<'a> {
pub code: &'a str,
pub message: &'a str,
pub status: u16,
}
impl<'a> ErrorResponseContext<'a> {
pub const fn new(code: &'a str, message: &'a str, status: u16) -> Self {
Self { code, message, status }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FormattedErrorResponse {
pub body: Bytes,
pub content_type: HeaderValue,
}
impl FormattedErrorResponse {
pub fn new(body: impl Into<Bytes>, content_type: HeaderValue) -> Self {
Self {
body: body.into(),
content_type,
}
}
}
pub trait ErrorResponseFormatter: Send + Sync {
fn format(&self, context: &ErrorResponseContext<'_>) -> FormattedErrorResponse;
}
#[derive(Clone)]
pub struct ErrorResponseFormatterHandle {
formatter: Arc<dyn ErrorResponseFormatter>,
}
impl ErrorResponseFormatterHandle {
pub fn new(formatter: impl ErrorResponseFormatter + 'static) -> Self {
Self {
formatter: Arc::new(formatter),
}
}
pub fn format(&self, context: &ErrorResponseContext<'_>) -> FormattedErrorResponse {
self.formatter.format(context)
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn context_constructor_sets_fields() {
let context = ErrorResponseContext::new("upstream_error", "Upstream error", 502);
assert_eq!(context.code, "upstream_error");
assert_eq!(context.message, "Upstream error");
assert_eq!(context.status, 502);
}
#[test]
fn formatter_handle_delegates_to_external_formatter() {
let formatter = ErrorResponseFormatterHandle::new(TestFormatter);
let context = ErrorResponseContext::new("upstream_error", "Upstream error", 502);
let response = formatter.format(&context);
assert_eq!(
response.body,
Bytes::from_static(br#"{"code":"upstream_error","status":502}"#)
);
assert_eq!(
response.content_type,
HeaderValue::from_static("application/vnd.praxis.test+json")
);
}
struct TestFormatter;
impl ErrorResponseFormatter for TestFormatter {
fn format(&self, context: &ErrorResponseContext<'_>) -> FormattedErrorResponse {
FormattedErrorResponse::new(
format!(r#"{{"code":"{}","status":{}}}"#, context.code, context.status),
HeaderValue::from_static("application/vnd.praxis.test+json"),
)
}
}
}