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