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