use crate::engine::{ErrorClass, HandlerError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
CallerInput,
Unavailable,
RuntimeUnavailable,
Adapter,
InputSize,
OutputSize,
Permit,
Timeout,
Run,
}
impl Category {
pub const ALL: [Category; 9] = [
Category::CallerInput,
Category::Unavailable,
Category::RuntimeUnavailable,
Category::Adapter,
Category::InputSize,
Category::OutputSize,
Category::Permit,
Category::Timeout,
Category::Run,
];
pub fn as_str(self) -> &'static str {
match self {
Self::CallerInput => "caller_input",
Self::Unavailable => "unavailable",
Self::RuntimeUnavailable => "runtime_unavailable",
Self::Adapter => "adapter",
Self::InputSize => "input_size",
Self::OutputSize => "output_size",
Self::Permit => "permit",
Self::Timeout => "timeout",
Self::Run => "run",
}
}
pub fn class(self) -> ErrorClass {
match self {
Self::CallerInput | Self::Adapter => ErrorClass::CallerInput,
Self::Unavailable | Self::RuntimeUnavailable | Self::Run => ErrorClass::Backend,
Self::InputSize | Self::OutputSize | Self::Permit => ErrorClass::Limit,
Self::Timeout => ErrorClass::Timeout,
}
}
}
#[derive(Debug)]
pub struct Failure {
pub category: Category,
pub message: String,
pub detail: Option<String>,
}
impl Failure {
pub fn new(category: Category, message: impl Into<String>) -> Self {
Self {
category,
message: message.into(),
detail: None,
}
}
pub fn with_detail(mut self, detail: impl std::fmt::Display) -> Self {
self.detail = Some(detail.to_string());
self
}
pub fn into_handler_error(self) -> HandlerError {
let mut e = HandlerError::new(self.category.class(), self.message);
if let Some(detail) = self.detail {
e = e.with_detail(detail);
}
e
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_category_has_a_class_and_a_stable_label() {
let mut labels: Vec<&str> = Category::ALL.iter().map(|c| c.as_str()).collect();
labels.sort_unstable();
labels.dedup();
assert_eq!(labels.len(), Category::ALL.len(), "labels must be distinct");
for c in Category::ALL {
assert_eq!(
c.class().is_retryable(),
c == Category::Timeout,
"{c:?}: only a timeout is retryable"
);
}
let expected = [
(
Category::CallerInput,
"caller_input",
ErrorClass::CallerInput,
),
(Category::Unavailable, "unavailable", ErrorClass::Backend),
(
Category::RuntimeUnavailable,
"runtime_unavailable",
ErrorClass::Backend,
),
(Category::Adapter, "adapter", ErrorClass::CallerInput),
(Category::InputSize, "input_size", ErrorClass::Limit),
(Category::OutputSize, "output_size", ErrorClass::Limit),
(Category::Permit, "permit", ErrorClass::Limit),
(Category::Timeout, "timeout", ErrorClass::Timeout),
(Category::Run, "run", ErrorClass::Backend),
];
for (category, label, class) in expected {
assert_eq!(category.as_str(), label);
assert_eq!(category.class(), class, "{label}");
}
}
#[test]
fn a_failure_keeps_its_detail_out_of_the_message() {
let f = Failure::new(Category::Run, "the model failed to run")
.with_detail("tract: shape mismatch at node 12");
let e = f.into_handler_error();
assert_eq!(e.class, ErrorClass::Backend);
assert_eq!(e.msg, "the model failed to run");
assert_eq!(
e.detail.as_deref(),
Some("tract: shape mismatch at node 12")
);
}
}