use std::fmt;
use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
#[derive(Debug, Clone)]
pub enum Payload<C, S> {
Canonical(C),
Flat(S),
Structured(S),
}
#[derive(Debug, Clone)]
pub enum Representation<T> {
Full(T),
Identifier(super::generated::common::Identifier),
}
impl<T: serde::Serialize> serde::Serialize for Representation<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Full(full) => full.serialize(serializer),
Self::Identifier(identifier) => identifier.serialize(serializer),
}
}
}
impl<'de, T: serde::de::DeserializeOwned> serde::Deserialize<'de> for Representation<T> {
#[expect(
clippy::disallowed_types,
reason = "a `oneOf` body is read twice, as the full and as the identifier form, so the \
wire value is buffered once (#1694)"
)]
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match serde_json::from_value::<T>(value.clone()) {
Ok(full) => Ok(Self::Full(full)),
Err(full) => {
serde_json::from_value(value)
.map(Self::Identifier)
.map_err(|identifier| {
serde::de::Error::custom(format!(
"neither the full representation ({full}) nor the identifier form \
({identifier})"
))
})
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("bad request: {0}")]
BadRequest(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
#[error("forbidden: {0}")]
Forbidden(String),
#[error("not found: {0}")]
NotFound(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("precondition failed: {0}")]
PreconditionFailed(String),
#[error("unprocessable entity: {0}")]
Unprocessable(String),
#[error("{} validation error(s)", .0.len())]
ValidationFailed(Vec<ValidationError>),
#[error("unsupported media type: {0}")]
UnsupportedMediaType(String),
#[error("not acceptable: {0}")]
NotAcceptable(String),
#[error("not implemented")]
NotImplemented,
#[error("service unavailable: {0}")]
ServiceUnavailable(String),
#[error("internal error: {0}")]
Internal(String),
}
impl ApiError {
#[must_use]
pub fn status(&self) -> StatusCode {
match self {
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
ApiError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
ApiError::Forbidden(_) => StatusCode::FORBIDDEN,
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::Conflict(_) => StatusCode::CONFLICT,
ApiError::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
ApiError::Unprocessable(_) | ApiError::ValidationFailed(_) => {
StatusCode::UNPROCESSABLE_ENTITY
}
ApiError::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
ApiError::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
ApiError::NotImplemented => StatusCode::NOT_IMPLEMENTED,
ApiError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum HeaderError {
#[error("{name} is not a legal header name")]
Name {
name: String,
#[source]
source: http::header::InvalidHeaderName,
},
#[error("the value for the {name} header is not a legal header value")]
Value {
name: String,
#[source]
source: http::header::InvalidHeaderValue,
},
}
fn header_field(name: &str, value: &str) -> Result<(HeaderName, HeaderValue), HeaderError> {
let header = HeaderName::from_bytes(name.as_bytes()).map_err(|source| HeaderError::Name {
name: name.to_owned(),
source,
})?;
let value = HeaderValue::from_str(value).map_err(|source| HeaderError::Value {
name: name.to_owned(),
source,
})?;
Ok((header, value))
}
pub trait ResponseHeaders {
fn into_header_map(self) -> Result<HeaderMap, HeaderError>;
}
pub(crate) fn set_header(
map: &mut HeaderMap,
name: &str,
value: Option<String>,
) -> Result<(), HeaderError> {
if let Some(value) = value {
let (name, value) = header_field(name, &value)?;
map.insert(name, value);
}
Ok(())
}
pub(crate) fn append_headers(
map: &mut HeaderMap,
name: &str,
values: Vec<String>,
) -> Result<(), HeaderError> {
for value in values {
let (name, value) = header_field(name, &value)?;
map.append(name, value);
}
Ok(())
}
pub(crate) fn merge_headers(headers: &mut HeaderMap, fields: HeaderMap) {
for name in fields.keys() {
headers.remove(name);
}
let mut current: Option<HeaderName> = None;
for (name, value) in fields {
if let Some(name) = name {
current = Some(name);
}
if let Some(name) = current.as_ref() {
headers.append(name.clone(), value);
}
}
}
#[derive(Debug)]
pub struct Refusal {
error: ApiError,
headers: HeaderMap,
}
impl Refusal {
#[must_use]
pub fn new(error: ApiError) -> Self {
Self {
error,
headers: HeaderMap::new(),
}
}
#[must_use]
pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.headers.append(name, value);
self
}
pub fn try_with_header(self, name: &str, value: &str) -> Result<Self, HeaderError> {
let (name, value) = header_field(name, value)?;
Ok(self.with_header(name, value))
}
pub fn with_headers(mut self, headers: impl ResponseHeaders) -> Result<Self, HeaderError> {
merge_headers(&mut self.headers, headers.into_header_map()?);
Ok(self)
}
#[must_use]
pub fn error(&self) -> &ApiError {
&self.error
}
#[must_use]
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
#[must_use]
pub fn status(&self) -> StatusCode {
self.error.status()
}
#[must_use]
pub fn into_parts(self) -> (ApiError, HeaderMap) {
(self.error, self.headers)
}
}
impl From<ApiError> for Refusal {
fn from(error: ApiError) -> Self {
Self::new(error)
}
}
impl From<HeaderError> for Refusal {
fn from(_error: HeaderError) -> Self {
Self::new(ApiError::Internal(
"the answer carries a header that is not legal on the wire".to_owned(),
))
}
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.error, f)
}
}
impl std::error::Error for Refusal {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(&self.error)
}
}
#[cfg(feature = "rest-server")]
pub(crate) fn error_response(
status: StatusCode,
message: String,
validation_errors: Vec<String>,
) -> axum::response::Response {
use axum::response::IntoResponse as _;
let body = super::generated::common::Error {
message,
validation_errors,
additional_properties: std::collections::BTreeMap::new(),
};
match serde_json::to_vec(&body) {
Ok(bytes) => (
status,
[(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
)],
bytes,
)
.into_response(),
Err(_unserializable) => status.into_response(),
}
}
#[cfg(feature = "rest-server")]
impl axum::response::IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let validation_errors = match &self {
ApiError::ValidationFailed(errors) => errors
.iter()
.map(|error| format!("{}: {}", error.path, error.message))
.collect(),
_ => Vec::new(),
};
error_response(self.status(), self.to_string(), validation_errors)
}
}
#[cfg(feature = "rest-server")]
impl axum::response::IntoResponse for Refusal {
fn into_response(self) -> axum::response::Response {
let mut response = self.error.into_response();
merge_headers(response.headers_mut(), self.headers);
response
}
}