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: timed out writing to transport, delivery undetermined")]
37 WriteTimeout,
38 #[error("send failed: connection changed before write")]
40 ConnectionChanged,
41 #[error("send failed: broken pipe ({0})")]
43 BrokenPipe(String),
44}
45
46pub type NetworkConfigResult<T> = Result<T, NetworkConfigError>;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum NetworkConfigError {
52 Invalid { field: String, reason: String },
54 Multiple { errors: Vec<Self> },
56}
57
58impl NetworkConfigError {
59 pub fn invalid(field: impl Into<String>, reason: impl Into<String>) -> Self {
61 Self::Invalid {
62 field: field.into(),
63 reason: reason.into(),
64 }
65 }
66
67 pub(crate) fn collect(mut errors: Vec<Self>) -> NetworkConfigResult<()> {
72 match errors.len() {
73 0 => Ok(()),
74 1 => Err(errors.remove(0)),
75 _ => Err(Self::Multiple { errors }),
76 }
77 }
78}
79
80impl Display for NetworkConfigError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::Invalid { field, reason } => write!(f, "invalid {field}: {reason}"),
84 Self::Multiple { errors } => {
85 for (index, error) in errors.iter().enumerate() {
86 if index > 0 {
87 write!(f, "; ")?;
88 }
89 write!(f, "{error}")?;
90 }
91 Ok(())
92 }
93 }
94 }
95}
96
97impl std::error::Error for NetworkConfigError {}
98
99pub(crate) fn is_connection_drop_io_error(err: &io::Error) -> bool {
100 matches!(
101 err.kind(),
102 io::ErrorKind::BrokenPipe
103 | io::ErrorKind::ConnectionAborted
104 | io::ErrorKind::ConnectionReset
105 | io::ErrorKind::NotConnected
106 | io::ErrorKind::TimedOut
107 | io::ErrorKind::UnexpectedEof
108 )
109}
110
111#[cfg(test)]
112mod tests {
113 use rstest::rstest;
114
115 use super::*;
116
117 #[rstest]
118 #[case(io::ErrorKind::BrokenPipe, true)]
119 #[case(io::ErrorKind::ConnectionAborted, true)]
120 #[case(io::ErrorKind::ConnectionReset, true)]
121 #[case(io::ErrorKind::NotConnected, true)]
122 #[case(io::ErrorKind::TimedOut, true)]
123 #[case(io::ErrorKind::UnexpectedEof, true)]
124 #[case(io::ErrorKind::InvalidInput, false)]
125 #[case(io::ErrorKind::PermissionDenied, false)]
126 fn connection_drop_io_error_classification(
127 #[case] kind: io::ErrorKind,
128 #[case] expected: bool,
129 ) {
130 let err = io::Error::from(kind);
131
132 assert_eq!(is_connection_drop_io_error(&err), expected);
133 }
134
135 #[rstest]
136 fn test_invalid_display() {
137 let err = NetworkConfigError::invalid("url", "must not be empty");
138
139 assert_eq!(err.to_string(), "invalid url: must not be empty");
140 }
141
142 #[rstest]
143 fn test_multiple_display_joins_errors() {
144 let err = NetworkConfigError::Multiple {
145 errors: vec![
146 NetworkConfigError::invalid("url", "must not be empty"),
147 NetworkConfigError::invalid("idle_timeout_ms", "must be positive, was 0"),
148 ],
149 };
150
151 assert_eq!(
152 err.to_string(),
153 "invalid url: must not be empty; invalid idle_timeout_ms: must be positive, was 0"
154 );
155 }
156
157 #[rstest]
158 fn test_collect_returns_bare_error_for_single() {
159 let errors = vec![NetworkConfigError::invalid("url", "must not be empty")];
160
161 let result = NetworkConfigError::collect(errors);
162
163 assert!(matches!(result, Err(NetworkConfigError::Invalid { field, .. }) if field == "url"));
164 }
165}