mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-18 22:03:01 +02:00
281 lines
9.9 KiB
Rust
281 lines
9.9 KiB
Rust
use crate::utils::sanitize_sensitive_info;
|
|
use std::fmt::{Display, Formatter};
|
|
|
|
#[macro_export]
|
|
macro_rules! get_errors_notify_message {
|
|
($errors:expr, $size:expr) => {
|
|
if $errors.is_empty() {
|
|
None
|
|
} else {
|
|
let text = $errors
|
|
.iter()
|
|
.filter(|err| err.is_notify())
|
|
.map(|err| err.message())
|
|
.collect::<Vec<&str>>()
|
|
.join("\r\n");
|
|
|
|
let max_size = $size;
|
|
if max_size > 0 && text.chars().count() > max_size {
|
|
let truncated: String = text.chars().take(max_size).collect();
|
|
Some(format!("{}...", truncated))
|
|
} else if text.is_empty() {
|
|
None
|
|
} else {
|
|
Some(text)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
pub use get_errors_notify_message;
|
|
|
|
// #[macro_export]
|
|
// macro_rules! handle_tuliprox_error_result {
|
|
// ($result: expr) => {
|
|
// if let Err(err) = $result {
|
|
// return Err($crate::error::TuliproxError::Config(err.to_string()));
|
|
// }
|
|
// };
|
|
// }
|
|
// pub use handle_tuliprox_error_result;
|
|
|
|
/// Declares every error category once: the [`ErrorKind`] variant, its display
|
|
/// label, and the matching constructor on [`TuliproxError`].
|
|
macro_rules! error_kinds {
|
|
($($variant:ident => $label:literal),+ $(,)?) => {
|
|
/// The category of a [`TuliproxError`].
|
|
///
|
|
/// Fieldless and `Copy`, so a category can be compared, matched, stored
|
|
/// and returned without touching the message.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
|
pub enum ErrorKind {
|
|
$($variant,)+
|
|
}
|
|
|
|
impl ErrorKind {
|
|
/// The prefix this category is displayed with.
|
|
#[must_use]
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
$(Self::$variant => $label,)+
|
|
}
|
|
}
|
|
}
|
|
|
|
// Constructors named after the categories, so the 755 existing
|
|
// `TuliproxError::Config(msg)` call sites keep compiling verbatim: a
|
|
// tuple-variant constructor and an associated function are invoked with
|
|
// identical syntax. The non-snake-case names are the deliberate price of
|
|
// splitting the type without touching every call site at once.
|
|
#[allow(non_snake_case)]
|
|
impl TuliproxError {
|
|
$(
|
|
#[doc = concat!("Build a `", $label, "`.")]
|
|
#[inline]
|
|
#[must_use]
|
|
pub fn $variant(message: impl Into<Box<str>>) -> Self {
|
|
Self::new(ErrorKind::$variant, message)
|
|
}
|
|
)+
|
|
}
|
|
};
|
|
}
|
|
|
|
error_kinds! {
|
|
Errors => "errors",
|
|
ApiXtream => "api xtream error",
|
|
Config => "config error",
|
|
ConfigApp => "config app error",
|
|
ConfigCache => "config cache error",
|
|
ConfigBase => "config base error",
|
|
ConfigApiProxy => "config API proxy error",
|
|
ConfigEpg => "config EPG error",
|
|
ConfigHdhomerun => "config HDHomeRun error",
|
|
ConfigInput => "config input error",
|
|
ConfigIpCheck => "config IP check error",
|
|
ConfigLibrary => "config library error",
|
|
ConfigMetadataUpdate => "config metadata update error",
|
|
ConfigPanelApi => "config panel API error",
|
|
ConfigProxy => "config proxy error",
|
|
ConfigProxyType => "config proxy type error",
|
|
ConfigProxyUserStatus => "config proxy user status error",
|
|
ConfigQosAggregation => "config QoS aggregation error",
|
|
ConfigRateLimit => "config rate limit error",
|
|
ConfigReverseProxy => "config reverse proxy error",
|
|
ConfigSort => "config sort error",
|
|
ConfigSource => "config source error",
|
|
ConfigStream => "config stream error",
|
|
ConfigStreamHistory => "config stream history error",
|
|
ConfigRecording => "config recording error",
|
|
ConfigVideoDownload => "config video download error",
|
|
ConfigTarget => "config target error",
|
|
TargetBouquet => "target bouquet error",
|
|
ChannelGroup => "channel group error",
|
|
ConfigWebUi => "config web UI error",
|
|
RepositoryEpg => "repository EPG error",
|
|
RepositoryXtream => "repository XTream error",
|
|
RepositoryM3u => "repository M3U error",
|
|
RepositoryStalker => "repository Stalker error",
|
|
RepositoryLibrary => "repository Library error",
|
|
RepositoryStorage => "repository Storage error",
|
|
RepositoryNetwork => "repository Network error",
|
|
RepositoryPlaylist => "repository Playlist error",
|
|
RepositoryTrakt => "repository Trakt error",
|
|
FilterParse => "filter parse error",
|
|
Mapper => "mapper error",
|
|
RegexCompile => "regex compile error",
|
|
UrlParse => "url parse error",
|
|
Io => "io error",
|
|
Crypto => "crypto error",
|
|
Server => "server error",
|
|
Probe => "probe error",
|
|
Task => "task error",
|
|
Parse => "parse error",
|
|
ProxyUser => "proxy user error",
|
|
Repository => "repository error",
|
|
Download => "download error",
|
|
ProviderConnection => "provider connection error",
|
|
}
|
|
|
|
impl ErrorKind {
|
|
/// Whether an error of this category should reach the user as a
|
|
/// notification rather than only the log.
|
|
#[must_use]
|
|
pub const fn is_notify(self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::RepositoryEpg
|
|
| Self::RepositoryXtream
|
|
| Self::RepositoryM3u
|
|
| Self::RepositoryStalker
|
|
| Self::RepositoryLibrary
|
|
| Self::RepositoryStorage
|
|
| Self::RepositoryNetwork
|
|
| Self::RepositoryPlaylist
|
|
| Self::RepositoryTrakt
|
|
| Self::Repository
|
|
| Self::Download
|
|
| Self::ProviderConnection
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A categorised error with a message.
|
|
///
|
|
/// This used to be a 50-variant enum in which every variant carried exactly one
|
|
/// `String` and nothing else -- so it was never really an enum of errors, but a
|
|
/// category tag beside a message. Encoding it as an enum cost a 50-arm
|
|
/// `message()` whose only job was to return the payload, plus a second 50-name
|
|
/// list in `is_notify()` that had to be kept in sync by hand. Splitting the two
|
|
/// makes the category `Copy` and comparable, moves classification onto
|
|
/// [`ErrorKind`], and leaves one field access where the match used to be.
|
|
#[derive(Debug)]
|
|
pub struct TuliproxError {
|
|
kind: ErrorKind,
|
|
message: Box<str>,
|
|
}
|
|
|
|
impl TuliproxError {
|
|
#[inline]
|
|
#[must_use]
|
|
pub fn new(kind: ErrorKind, message: impl Into<Box<str>>) -> Self { Self { kind, message: message.into() } }
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn kind(&self) -> ErrorKind { self.kind }
|
|
|
|
/// The message, without the category prefix that [`Display`] adds.
|
|
#[inline]
|
|
#[must_use]
|
|
pub fn message(&self) -> &str { &self.message }
|
|
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn is_notify(&self) -> bool { self.kind.is_notify() }
|
|
}
|
|
|
|
impl Display for TuliproxError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.kind.label(), self.message) }
|
|
}
|
|
|
|
impl std::error::Error for TuliproxError {}
|
|
|
|
pub fn to_io_error<E>(err: E) -> std::io::Error
|
|
where
|
|
E: std::error::Error,
|
|
{
|
|
std::io::Error::other(sanitize_sensitive_info(&err.to_string()))
|
|
}
|
|
|
|
pub fn str_to_io_error(err: &str) -> std::io::Error { std::io::Error::other(sanitize_sensitive_info(err)) }
|
|
|
|
pub fn string_to_io_error(err: impl AsRef<str>) -> std::io::Error {
|
|
std::io::Error::other(sanitize_sensitive_info(err.as_ref()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{ErrorKind, TuliproxError};
|
|
|
|
#[test]
|
|
fn display_keeps_the_label_colon_message_shape_the_enum_produced() {
|
|
assert_eq!(TuliproxError::Config("bad").to_string(), "config error: bad");
|
|
assert_eq!(TuliproxError::Errors("a\nb").to_string(), "errors: a\nb");
|
|
assert_eq!(TuliproxError::ConfigWebUi("nope").to_string(), "config web UI error: nope");
|
|
assert_eq!(TuliproxError::RepositoryM3u("gone").to_string(), "repository M3U error: gone");
|
|
}
|
|
|
|
#[test]
|
|
fn message_returns_the_payload_without_the_label() {
|
|
let err = TuliproxError::ConfigEpg(String::from("boom"));
|
|
assert_eq!(err.message(), "boom");
|
|
assert_eq!(err.kind(), ErrorKind::ConfigEpg);
|
|
}
|
|
|
|
#[test]
|
|
fn constructors_accept_both_str_and_string() {
|
|
// The old String payload forced `.to_string()` at call sites that only
|
|
// had a &str; `impl Into<Box<str>>` accepts either.
|
|
assert_eq!(TuliproxError::Io("x").message(), "x");
|
|
assert_eq!(TuliproxError::Io(String::from("y")).message(), "y");
|
|
assert_eq!(TuliproxError::Io(format!("z{}", 1)).message(), "z1");
|
|
}
|
|
|
|
#[test]
|
|
fn only_the_repository_download_and_provider_categories_notify() {
|
|
for kind in [
|
|
ErrorKind::Repository,
|
|
ErrorKind::RepositoryEpg,
|
|
ErrorKind::RepositoryXtream,
|
|
ErrorKind::RepositoryM3u,
|
|
ErrorKind::RepositoryStalker,
|
|
ErrorKind::RepositoryLibrary,
|
|
ErrorKind::RepositoryStorage,
|
|
ErrorKind::RepositoryNetwork,
|
|
ErrorKind::RepositoryPlaylist,
|
|
ErrorKind::RepositoryTrakt,
|
|
ErrorKind::Download,
|
|
ErrorKind::ProviderConnection,
|
|
] {
|
|
assert!(kind.is_notify(), "{kind:?} should notify");
|
|
}
|
|
for kind in [ErrorKind::Config, ErrorKind::Errors, ErrorKind::Io, ErrorKind::Server, ErrorKind::ApiXtream] {
|
|
assert!(!kind.is_notify(), "{kind:?} should not notify");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn kind_is_copy_and_the_error_is_not_larger_than_the_enum_it_replaced() {
|
|
// The 50-variant enum was a String (24 bytes) plus a discriminant.
|
|
// kind + Box<str> is one word smaller.
|
|
let kind = ErrorKind::Config;
|
|
let copied = kind;
|
|
assert_eq!(kind, copied);
|
|
assert!(
|
|
size_of::<TuliproxError>() <= size_of::<String>() + size_of::<usize>(),
|
|
"unexpected size: {}",
|
|
size_of::<TuliproxError>()
|
|
);
|
|
}
|
|
}
|