Skip to main content

nautilus_network/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Network error types.
17
18use std::{fmt::Display, io};
19
20use thiserror::Error;
21
22/// Error type for send operations in network clients.
23#[derive(Error, Debug)]
24pub enum SendError {
25    /// The client has been closed or is disconnecting.
26    #[error("send failed: client closed or disconnecting")]
27    Closed,
28    /// Timed out waiting for the client to become active.
29    #[error("send failed: timeout waiting for active state")]
30    Timeout,
31    /// Failed to send because the writer channel is closed.
32    #[error("send failed: broken pipe ({0})")]
33    BrokenPipe(String),
34}
35
36/// Result type for client configuration validation.
37pub type NetworkConfigResult<T> = Result<T, NetworkConfigError>;
38
39/// A validation error for a network client configuration.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum NetworkConfigError {
42    /// A field value is empty or outside its accepted range.
43    Invalid { field: String, reason: String },
44    /// Multiple validation errors were collected.
45    Multiple { errors: Vec<Self> },
46}
47
48impl NetworkConfigError {
49    /// Creates a [`NetworkConfigError::Invalid`] for `field` with the given `reason`.
50    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    /// Converts collected errors into a single result.
58    ///
59    /// Returns `Ok(())` when `errors` is empty, the sole error when one was collected, or a
60    /// [`NetworkConfigError::Multiple`] otherwise.
61    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}