1use crate::interface::{InterfaceId, InterfaceType, IpAddr};
14use serde::{Deserialize, Serialize};
15
16#[non_exhaustive]
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum ConnectionState {
29 Disconnected,
31 Connecting,
33 Authenticating,
35 Configuring,
37 Connected,
39 Disconnecting,
41 Failed,
43}
44
45impl ConnectionState {
46 pub const fn is_connected(&self) -> bool {
48 matches!(self, Self::Connected)
49 }
50
51 pub const fn is_transitioning(&self) -> bool {
53 matches!(
54 self,
55 Self::Connecting | Self::Authenticating | Self::Configuring | Self::Disconnecting
56 )
57 }
58
59 pub const fn is_failed(&self) -> bool {
61 matches!(self, Self::Failed)
62 }
63
64 pub const fn label(&self) -> &'static str {
66 match self {
67 Self::Disconnected => "Disconnected",
68 Self::Connecting => "Connecting",
69 Self::Authenticating => "Authenticating",
70 Self::Configuring => "Configuring",
71 Self::Connected => "Connected",
72 Self::Disconnecting => "Disconnecting",
73 Self::Failed => "Failed",
74 }
75 }
76}
77
78#[non_exhaustive]
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub enum FailureReason {
84 NetworkNotFound,
86 AuthenticationFailed,
88 ConfigurationFailed,
90 Timeout,
92 SignalLost,
94 ManualDisconnect,
96 HardwareError,
98 Other(String),
100}
101
102impl FailureReason {
103 pub fn is_retryable(&self) -> bool {
105 matches!(
106 self,
107 Self::Timeout | Self::SignalLost | Self::ConfigurationFailed
108 )
109 }
110
111 pub fn label(&self) -> &str {
113 match self {
114 Self::NetworkNotFound => "Network not found",
115 Self::AuthenticationFailed => "Authentication failed",
116 Self::ConfigurationFailed => "Configuration failed",
117 Self::Timeout => "Connection timed out",
118 Self::SignalLost => "Signal lost",
119 Self::ManualDisconnect => "Disconnected",
120 Self::HardwareError => "Hardware error",
121 Self::Other(msg) => msg.as_str(),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Connection {
131 pub interface_id: InterfaceId,
133 pub interface_type: InterfaceType,
135 pub network_name: String,
137 state: ConnectionState,
139 pub address: Option<IpAddr>,
141 pub gateway: Option<IpAddr>,
143 pub dns_servers: Vec<IpAddr>,
145 pub last_failure: Option<FailureReason>,
147 pub retry_count: u32,
149 pub max_retries: u32,
151}
152
153impl Connection {
154 pub fn new(
156 interface_id: InterfaceId,
157 interface_type: InterfaceType,
158 network_name: impl Into<String>,
159 ) -> Self {
160 Self {
161 interface_id,
162 interface_type,
163 network_name: network_name.into(),
164 state: ConnectionState::Disconnected,
165 address: None,
166 gateway: None,
167 dns_servers: Vec::new(),
168 last_failure: None,
169 retry_count: 0,
170 max_retries: 3,
171 }
172 }
173
174 pub fn state(&self) -> ConnectionState {
176 self.state
177 }
178
179 pub fn is_connected(&self) -> bool {
181 self.state.is_connected()
182 }
183
184 pub fn is_transitioning(&self) -> bool {
186 self.state.is_transitioning()
187 }
188
189 pub fn connect(&mut self) -> Result<(), ConnectionError> {
193 match self.state {
194 ConnectionState::Disconnected | ConnectionState::Failed => {
195 self.state = ConnectionState::Connecting;
196 self.last_failure = None;
197 Ok(())
198 }
199 ConnectionState::Connecting
200 | ConnectionState::Authenticating
201 | ConnectionState::Configuring
202 | ConnectionState::Connected
203 | ConnectionState::Disconnecting => Err(ConnectionError::InvalidTransition {
204 from: self.state,
205 to: ConnectionState::Connecting,
206 }),
207 }
208 }
209
210 pub fn begin_auth(&mut self) -> Result<(), ConnectionError> {
212 if self.state == ConnectionState::Connecting {
213 self.state = ConnectionState::Authenticating;
214 Ok(())
215 } else {
216 Err(ConnectionError::InvalidTransition {
217 from: self.state,
218 to: ConnectionState::Authenticating,
219 })
220 }
221 }
222
223 pub fn begin_config(&mut self) -> Result<(), ConnectionError> {
225 if self.state == ConnectionState::Authenticating {
226 self.state = ConnectionState::Configuring;
227 Ok(())
228 } else {
229 Err(ConnectionError::InvalidTransition {
230 from: self.state,
231 to: ConnectionState::Configuring,
232 })
233 }
234 }
235
236 pub fn establish(
238 &mut self,
239 address: IpAddr,
240 gateway: Option<IpAddr>,
241 dns: Vec<IpAddr>,
242 ) -> Result<(), ConnectionError> {
243 if self.state == ConnectionState::Configuring {
244 self.address = Some(address);
245 self.gateway = gateway;
246 self.dns_servers = dns;
247 self.state = ConnectionState::Connected;
248 self.retry_count = 0;
249 Ok(())
250 } else {
251 Err(ConnectionError::InvalidTransition {
252 from: self.state,
253 to: ConnectionState::Connected,
254 })
255 }
256 }
257
258 pub fn begin_disconnect(&mut self) -> Result<(), ConnectionError> {
260 if self.state == ConnectionState::Connected {
261 self.state = ConnectionState::Disconnecting;
262 Ok(())
263 } else {
264 Err(ConnectionError::InvalidTransition {
265 from: self.state,
266 to: ConnectionState::Disconnecting,
267 })
268 }
269 }
270
271 pub fn complete_disconnect(&mut self) {
273 self.state = ConnectionState::Disconnected;
274 self.address = None;
275 self.gateway = None;
276 self.dns_servers.clear();
277 }
278
279 pub fn fail(&mut self, reason: FailureReason) {
281 self.last_failure = Some(reason);
282 self.state = ConnectionState::Failed;
283 self.retry_count = self.retry_count.saturating_add(1);
284 self.address = None;
285 self.gateway = None;
286 self.dns_servers.clear();
287 }
288
289 pub fn should_retry(&self) -> bool {
291 self.state == ConnectionState::Failed
292 && self.retry_count < self.max_retries
293 && self
294 .last_failure
295 .as_ref()
296 .is_some_and(FailureReason::is_retryable)
297 }
298
299 pub fn force_disconnect(&mut self) {
301 self.state = ConnectionState::Disconnected;
302 self.address = None;
303 self.gateway = None;
304 self.dns_servers.clear();
305 self.retry_count = 0;
306 }
307
308 pub fn summary(&self) -> String {
310 let addr_str = self
311 .address
312 .as_ref()
313 .map_or("no address".to_string(), IpAddr::to_string_repr);
314 format!(
315 "{} ({}) [{}] {}",
316 self.network_name,
317 self.interface_type.label(),
318 self.state.label(),
319 addr_str,
320 )
321 }
322}
323
324#[non_exhaustive]
328#[derive(Debug, Clone, nexcore_error::Error)]
329pub enum ConnectionError {
330 #[error("invalid transition: {from:?} → {to:?}")]
332 InvalidTransition {
333 from: ConnectionState,
334 to: ConnectionState,
335 },
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn make_conn() -> Connection {
343 Connection::new(
344 InterfaceId::new("wlan0"),
345 InterfaceType::WiFi,
346 "HomeNetwork",
347 )
348 }
349
350 #[test]
351 fn new_connection_disconnected() {
352 let c = make_conn();
353 assert_eq!(c.state(), ConnectionState::Disconnected);
354 assert!(!c.is_connected());
355 assert!(!c.is_transitioning());
356 }
357
358 #[test]
359 fn full_connection_lifecycle() {
360 let mut c = make_conn();
361
362 assert!(c.connect().is_ok());
364 assert_eq!(c.state(), ConnectionState::Connecting);
365 assert!(c.is_transitioning());
366
367 assert!(c.begin_auth().is_ok());
369 assert_eq!(c.state(), ConnectionState::Authenticating);
370
371 assert!(c.begin_config().is_ok());
373 assert_eq!(c.state(), ConnectionState::Configuring);
374
375 assert!(
377 c.establish(
378 IpAddr::v4(192, 168, 1, 100),
379 Some(IpAddr::v4(192, 168, 1, 1)),
380 vec![IpAddr::v4(8, 8, 8, 8)],
381 )
382 .is_ok()
383 );
384 assert_eq!(c.state(), ConnectionState::Connected);
385 assert!(c.is_connected());
386 assert!(c.address.is_some());
387
388 assert!(c.begin_disconnect().is_ok());
390 assert_eq!(c.state(), ConnectionState::Disconnecting);
391 c.complete_disconnect();
392 assert_eq!(c.state(), ConnectionState::Disconnected);
393 assert!(c.address.is_none());
394 }
395
396 #[test]
397 fn invalid_transition_blocked() {
398 let mut c = make_conn();
399 assert!(c.begin_auth().is_err());
401 assert!(c.establish(IpAddr::v4(0, 0, 0, 0), None, vec![]).is_err());
403 assert!(c.begin_disconnect().is_err());
405 }
406
407 #[test]
408 fn connection_failure() {
409 let mut c = make_conn();
410 assert!(c.connect().is_ok());
411 c.fail(FailureReason::Timeout);
412 assert_eq!(c.state(), ConnectionState::Failed);
413 assert!(c.state().is_failed());
414 assert_eq!(c.retry_count, 1);
415 }
416
417 #[test]
418 fn retry_on_retryable_failure() {
419 let mut c = make_conn();
420 assert!(c.connect().is_ok());
421 c.fail(FailureReason::Timeout);
422 assert!(c.should_retry());
423 }
424
425 #[test]
426 fn no_retry_on_auth_failure() {
427 let mut c = make_conn();
428 assert!(c.connect().is_ok());
429 c.fail(FailureReason::AuthenticationFailed);
430 assert!(!c.should_retry());
431 }
432
433 #[test]
434 fn max_retries_exceeded() {
435 let mut c = make_conn();
436 c.max_retries = 2;
437 for _ in 0..3 {
438 assert!(c.connect().is_ok());
439 c.fail(FailureReason::Timeout);
440 }
441 assert!(!c.should_retry()); }
443
444 #[test]
445 fn force_disconnect() {
446 let mut c = make_conn();
447 assert!(c.connect().is_ok());
448 assert!(c.begin_auth().is_ok());
449 c.force_disconnect();
450 assert_eq!(c.state(), ConnectionState::Disconnected);
451 assert_eq!(c.retry_count, 0);
452 }
453
454 #[test]
455 fn reconnect_after_failure() {
456 let mut c = make_conn();
457 assert!(c.connect().is_ok());
458 c.fail(FailureReason::SignalLost);
459 assert!(c.connect().is_ok()); }
461
462 #[test]
463 fn connection_summary() {
464 let mut c = make_conn();
465 assert!(c.connect().is_ok());
466 assert!(c.begin_auth().is_ok());
467 assert!(c.begin_config().is_ok());
468 assert!(c.establish(IpAddr::v4(10, 0, 0, 5), None, vec![]).is_ok());
469 let s = c.summary();
470 assert!(s.contains("HomeNetwork"));
471 assert!(s.contains("WiFi"));
472 assert!(s.contains("Connected"));
473 assert!(s.contains("10.0.0.5"));
474 }
475
476 #[test]
477 fn failure_reason_labels() {
478 assert_eq!(FailureReason::Timeout.label(), "Connection timed out");
479 assert_eq!(
480 FailureReason::AuthenticationFailed.label(),
481 "Authentication failed"
482 );
483 }
484
485 #[test]
486 fn connection_state_labels() {
487 assert_eq!(ConnectionState::Connected.label(), "Connected");
488 assert_eq!(ConnectionState::Authenticating.label(), "Authenticating");
489 }
490}