nautilus_network/
error.rs1use std::{fmt::Display, io};
19
20use thiserror::Error;
21
22#[derive(Error, Debug)]
24pub enum SendError {
25 #[error("send failed: client closed or disconnecting")]
27 Closed,
28 #[error("send failed: timeout waiting for active state")]
30 Timeout,
31 #[error("send failed: broken pipe ({0})")]
33 BrokenPipe(String),
34}
35
36pub type NetworkConfigResult<T> = Result<T, NetworkConfigError>;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum NetworkConfigError {
42 Invalid { field: String, reason: String },
44 Multiple { errors: Vec<Self> },
46}
47
48impl NetworkConfigError {
49 pub fn invalid(field: impl Into<String>, reason: impl Into<String>) -> Self {
51 Self::Invalid {
52 field: field.into(),
53 reason: reason.into(),
54 }
55 }
56
57 pub(crate) fn collect(mut errors: Vec<Self>) -> NetworkConfigResult<()> {
62 match errors.len() {
63 0 => Ok(()),
64 1 => Err(errors.remove(0)),
65 _ => Err(Self::Multiple { errors }),
66 }
67 }
68}
69
70impl Display for NetworkConfigError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::Invalid { field, reason } => write!(f, "invalid {field}: {reason}"),
74 Self::Multiple { errors } => {
75 for (index, error) in errors.iter().enumerate() {
76 if index > 0 {
77 write!(f, "; ")?;
78 }
79 write!(f, "{error}")?;
80 }
81 Ok(())
82 }
83 }
84 }
85}
86
87impl std::error::Error for NetworkConfigError {}
88
89pub(crate) fn is_connection_drop_io_error(err: &io::Error) -> bool {
90 matches!(
91 err.kind(),
92 io::ErrorKind::BrokenPipe
93 | io::ErrorKind::ConnectionAborted
94 | io::ErrorKind::ConnectionReset
95 | io::ErrorKind::NotConnected
96 | io::ErrorKind::TimedOut
97 | io::ErrorKind::UnexpectedEof
98 )
99}
100
101#[cfg(test)]
102mod tests {
103 use rstest::rstest;
104
105 use super::*;
106
107 #[rstest]
108 #[case(io::ErrorKind::BrokenPipe, true)]
109 #[case(io::ErrorKind::ConnectionAborted, true)]
110 #[case(io::ErrorKind::ConnectionReset, true)]
111 #[case(io::ErrorKind::NotConnected, true)]
112 #[case(io::ErrorKind::TimedOut, true)]
113 #[case(io::ErrorKind::UnexpectedEof, true)]
114 #[case(io::ErrorKind::InvalidInput, false)]
115 #[case(io::ErrorKind::PermissionDenied, false)]
116 fn connection_drop_io_error_classification(
117 #[case] kind: io::ErrorKind,
118 #[case] expected: bool,
119 ) {
120 let err = io::Error::from(kind);
121
122 assert_eq!(is_connection_drop_io_error(&err), expected);
123 }
124
125 #[rstest]
126 fn test_invalid_display() {
127 let err = NetworkConfigError::invalid("url", "must not be empty");
128
129 assert_eq!(err.to_string(), "invalid url: must not be empty");
130 }
131
132 #[rstest]
133 fn test_multiple_display_joins_errors() {
134 let err = NetworkConfigError::Multiple {
135 errors: vec![
136 NetworkConfigError::invalid("url", "must not be empty"),
137 NetworkConfigError::invalid("idle_timeout_ms", "must be positive, was 0"),
138 ],
139 };
140
141 assert_eq!(
142 err.to_string(),
143 "invalid url: must not be empty; invalid idle_timeout_ms: must be positive, was 0"
144 );
145 }
146
147 #[rstest]
148 fn test_collect_returns_bare_error_for_single() {
149 let errors = vec![NetworkConfigError::invalid("url", "must not be empty")];
150
151 let result = NetworkConfigError::collect(errors);
152
153 assert!(matches!(result, Err(NetworkConfigError::Invalid { field, .. }) if field == "url"));
154 }
155}