use std::error::Error;
use std::fmt;
use std::io;
use crate::diagnostics;
const PROTOCOL_HINT: &str =
"check the request and daemon state, then retry after correcting the request";
const TRANSPORT_HINT: &str = "verify the rmux daemon is running and the endpoint is reachable";
#[derive(Debug)]
#[non_exhaustive]
pub enum RmuxError {
#[non_exhaustive]
Unsupported {
feature: String,
hint: String,
},
#[non_exhaustive]
Protocol {
source: rmux_proto::RmuxError,
},
#[non_exhaustive]
Transport {
operation: String,
source: io::Error,
},
#[non_exhaustive]
Collect {
source: CollectError,
},
}
impl RmuxError {
#[must_use]
pub fn unsupported(feature: impl Into<String>, hint: impl Into<String>) -> Self {
Self::Unsupported {
feature: feature.into(),
hint: hint.into(),
}
}
#[must_use]
pub fn protocol(error: rmux_proto::RmuxError) -> Self {
match error {
rmux_proto::RmuxError::UnsupportedWireVersion {
got,
minimum,
maximum,
} => Self::unsupported(
diagnostics::FEATURE_PROTOCOL_WIRE_VERSION,
format!(
"upgrade the rmux daemon or use an SDK that supports wire version {got} \
(supported range {minimum}..={maximum})"
),
),
rmux_proto::RmuxError::UnsupportedCapability { feature, supported } => {
let hint = diagnostics::unsupported_capability_hint(&feature, &supported);
Self::unsupported(feature, hint)
}
rmux_proto::RmuxError::UnknownCommand(command) => {
let feature = diagnostics::command_feature_id(&command);
Self::unsupported(
feature,
format!(
"upgrade the rmux daemon or use a command advertised by the negotiated \
command inventory before sending `{command}`"
),
)
}
source => Self::Protocol { source },
}
}
#[must_use]
pub fn transport(operation: impl Into<String>, source: io::Error) -> Self {
Self::Transport {
operation: operation.into(),
source,
}
}
#[must_use]
pub fn collect(source: CollectError) -> Self {
Self::Collect { source }
}
#[must_use]
pub fn hint(&self) -> Option<&str> {
match self {
Self::Unsupported { hint, .. } => Some(hint),
Self::Protocol { .. } => Some(PROTOCOL_HINT),
Self::Transport { .. } => Some(TRANSPORT_HINT),
Self::Collect { .. } => None,
}
}
#[must_use]
pub fn feature(&self) -> Option<&str> {
match self {
Self::Unsupported { feature, .. } => Some(feature),
Self::Protocol { .. } | Self::Transport { .. } | Self::Collect { .. } => None,
}
}
}
impl From<rmux_proto::RmuxError> for RmuxError {
fn from(error: rmux_proto::RmuxError) -> Self {
Self::protocol(error)
}
}
impl From<rmux_proto::ErrorResponse> for RmuxError {
fn from(response: rmux_proto::ErrorResponse) -> Self {
Self::protocol(response.error)
}
}
impl From<io::Error> for RmuxError {
fn from(error: io::Error) -> Self {
Self::transport("communicate with rmux daemon", error)
}
}
impl From<CollectError> for RmuxError {
fn from(error: CollectError) -> Self {
Self::collect(error)
}
}
#[derive(Debug, Default)]
pub struct CollectError {
errors: Vec<RmuxError>,
}
impl CollectError {
#[must_use]
pub fn new(errors: Vec<RmuxError>) -> Self {
Self { errors }
}
#[must_use]
pub fn errors(&self) -> &[RmuxError] {
&self.errors
}
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn push(&mut self, error: RmuxError) {
self.errors.push(error);
}
#[must_use]
pub fn into_errors(self) -> Vec<RmuxError> {
self.errors
}
}
impl From<Vec<RmuxError>> for CollectError {
fn from(errors: Vec<RmuxError>) -> Self {
Self::new(errors)
}
}
impl FromIterator<RmuxError> for CollectError {
fn from_iter<T: IntoIterator<Item = RmuxError>>(iter: T) -> Self {
Self::new(iter.into_iter().collect())
}
}
impl Extend<RmuxError> for CollectError {
fn extend<T: IntoIterator<Item = RmuxError>>(&mut self, iter: T) {
self.errors.extend(iter);
}
}
impl fmt::Display for CollectError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.errors.as_slice() {
[] => write!(formatter, "no SDK diagnostics were collected"),
[error] => {
writeln!(formatter, "1 SDK diagnostic collected:")?;
write_numbered_error(formatter, 1, error)
}
errors => {
writeln!(formatter, "{} SDK diagnostics collected:", errors.len())?;
for (index, error) in errors.iter().enumerate() {
if index > 0 {
writeln!(formatter)?;
}
write_numbered_error(formatter, index + 1, error)?;
}
Ok(())
}
}
}
}
impl Error for CollectError {}
impl fmt::Display for RmuxError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported { feature, hint } => {
write!(formatter, "unsupported feature `{feature}`\nhint: {hint}")
}
Self::Protocol { source } => {
write!(
formatter,
"rmux protocol error: {source}\nhint: {PROTOCOL_HINT}"
)
}
Self::Transport { operation, source } => {
write!(
formatter,
"rmux transport error while {operation}: {source}\nhint: {TRANSPORT_HINT}"
)
}
Self::Collect { source } => source.fmt(formatter),
}
}
}
impl Error for RmuxError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Unsupported { .. } => None,
Self::Protocol { source } => Some(source),
Self::Transport { source, .. } => Some(source),
Self::Collect { source } => Some(source),
}
}
}
pub type Result<T> = core::result::Result<T, RmuxError>;
trait NonCloneGuard {}
impl<T: Clone> NonCloneGuard for T {}
impl NonCloneGuard for RmuxError {}
impl NonCloneGuard for CollectError {}
const _: fn() = sdk_errors_remain_non_clone;
#[allow(dead_code)]
fn sdk_errors_remain_non_clone() {
fn assert_non_clone_guard<T: NonCloneGuard>() {}
assert_non_clone_guard::<RmuxError>();
assert_non_clone_guard::<CollectError>();
}
fn write_numbered_error(
formatter: &mut fmt::Formatter<'_>,
index: usize,
error: &RmuxError,
) -> fmt::Result {
let rendered = error.to_string();
let mut lines = rendered.lines();
let Some(first) = lines.next() else {
return write!(formatter, "{index}. <empty SDK diagnostic>");
};
write!(formatter, "{index}. {first}")?;
for line in lines {
write!(formatter, "\n {line}")?;
}
Ok(())
}