use std::collections::BTreeMap;
use thiserror::Error;
pub const MIN_MAPPED_STATUS: u16 = 400;
pub const MAX_MAPPED_STATUS: u16 = 599;
pub const DEFAULT_BUSINESS_STATUS: u16 = 500;
const DEFAULT_KIND_STATUSES: [(&str, u16); 5] = [
("invalid-argument", 400),
("conflict", 409),
("not-found", 404),
("forbidden", 403),
("failed", 500),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MappingTable {
Codes,
Kinds,
}
impl MappingTable {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Codes => "codes",
Self::Kinds => "kinds",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum HttpErrorMappingError {
#[error("blank `{table}` key in HTTP error mapping")]
BlankKey {
table: &'static str,
},
#[error("HTTP error mapping `{table}` value {status} is outside 400-599")]
StatusOutOfDomain {
table: &'static str,
status: u16,
},
}
#[must_use]
pub const fn is_mapped_status(status: u16) -> bool {
status >= MIN_MAPPED_STATUS && status <= MAX_MAPPED_STATUS
}
pub fn validate_key(table: MappingTable, key: &str) -> Result<(), HttpErrorMappingError> {
if key.trim().is_empty() {
return Err(HttpErrorMappingError::BlankKey {
table: table.name(),
});
}
Ok(())
}
pub fn validate_status(table: MappingTable, status: u16) -> Result<(), HttpErrorMappingError> {
if is_mapped_status(status) {
Ok(())
} else {
Err(HttpErrorMappingError::StatusOutOfDomain {
table: table.name(),
status,
})
}
}
#[must_use]
pub fn default_kind_status(kind: &str) -> Option<u16> {
DEFAULT_KIND_STATUSES
.iter()
.find_map(|(name, status)| (*name == kind).then_some(*status))
}
#[must_use]
pub fn business_status(mapping: Option<&HttpErrorMapping>, code: &str, kind: &str) -> u16 {
mapping
.and_then(|mapping| mapping.explicit_status(code, kind))
.or_else(|| default_kind_status(kind))
.unwrap_or(DEFAULT_BUSINESS_STATUS)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HttpErrorMapping {
pub codes: BTreeMap<String, u16>,
pub kinds: BTreeMap<String, u16>,
}
impl HttpErrorMapping {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>, status: u16) -> Self {
self.codes.insert(code.into(), status);
self
}
#[must_use]
pub fn with_kind(mut self, kind: impl Into<String>, status: u16) -> Self {
self.kinds.insert(kind.into(), status);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.codes.is_empty() && self.kinds.is_empty()
}
pub fn validate(&self) -> Result<(), HttpErrorMappingError> {
for (code, status) in &self.codes {
validate_key(MappingTable::Codes, code)?;
validate_status(MappingTable::Codes, *status)?;
}
for (kind, status) in &self.kinds {
validate_key(MappingTable::Kinds, kind)?;
validate_status(MappingTable::Kinds, *status)?;
}
Ok(())
}
#[must_use]
pub fn explicit_status(&self, code: &str, kind: &str) -> Option<u16> {
self.codes
.get(code)
.copied()
.filter(|status| is_mapped_status(*status))
.or_else(|| {
self.kinds
.get(kind)
.copied()
.filter(|status| is_mapped_status(*status))
})
}
}