ddns_a/network/
adapter.rs

1//! Core network types for adapter representation.
2
3use std::fmt;
4use std::net::{Ipv4Addr, Ipv6Addr};
5
6use serde::{Deserialize, Serialize};
7
8/// IP version to monitor (explicit specification required, no default).
9///
10/// # Design Decision
11///
12/// This enum requires explicit configuration to avoid hidden behavior.
13/// Users must consciously choose which IP version(s) to monitor.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum IpVersion {
16    /// Monitor IPv4 addresses only.
17    V4,
18    /// Monitor IPv6 addresses only.
19    V6,
20    /// Monitor both IPv4 and IPv6 addresses.
21    Both,
22}
23
24impl IpVersion {
25    /// Returns true if this version includes IPv4.
26    #[must_use]
27    pub const fn includes_v4(self) -> bool {
28        matches!(self, Self::V4 | Self::Both)
29    }
30
31    /// Returns true if this version includes IPv6.
32    #[must_use]
33    pub const fn includes_v6(self) -> bool {
34        matches!(self, Self::V6 | Self::Both)
35    }
36}
37
38impl fmt::Display for IpVersion {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::V4 => write!(f, "IPv4"),
42            Self::V6 => write!(f, "IPv6"),
43            Self::Both => write!(f, "Both"),
44        }
45    }
46}
47
48/// Network adapter type classification.
49///
50/// Used for logging, filtering, and debugging. The core logic does not
51/// depend on specific values, allowing platform-specific implementations.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum AdapterKind {
54    /// Physical Ethernet adapter.
55    Ethernet,
56    /// Wireless (Wi-Fi) adapter.
57    Wireless,
58    /// Loopback adapter (localhost).
59    Loopback,
60    /// Virtual adapter (`VMware`, `VirtualBox`, `Hyper-V`, WSL, etc.).
61    Virtual,
62    /// Unknown or other adapter type, preserving the original type code for debugging.
63    Other(u32),
64}
65
66impl AdapterKind {
67    /// Returns true if this is a virtual adapter.
68    #[must_use]
69    pub const fn is_virtual(&self) -> bool {
70        matches!(self, Self::Virtual)
71    }
72
73    /// Returns true if this is a loopback adapter.
74    #[must_use]
75    pub const fn is_loopback(&self) -> bool {
76        matches!(self, Self::Loopback)
77    }
78}
79
80/// A snapshot of a single network adapter's addresses at a point in time.
81///
82/// # Equality
83///
84/// Two snapshots are equal if they have the same name, kind, and addresses.
85/// Address order matters for equality comparison.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct AdapterSnapshot {
88    /// The friendly name of the adapter (e.g., "Ethernet", "Wi-Fi").
89    pub name: String,
90    /// The type of adapter.
91    pub kind: AdapterKind,
92    /// All IPv4 addresses assigned to this adapter.
93    pub ipv4_addresses: Vec<Ipv4Addr>,
94    /// All IPv6 addresses assigned to this adapter.
95    pub ipv6_addresses: Vec<Ipv6Addr>,
96}
97
98impl AdapterSnapshot {
99    /// Creates a new adapter snapshot.
100    #[must_use]
101    pub fn new(
102        name: impl Into<String>,
103        kind: AdapterKind,
104        ipv4_addresses: Vec<Ipv4Addr>,
105        ipv6_addresses: Vec<Ipv6Addr>,
106    ) -> Self {
107        Self {
108            name: name.into(),
109            kind,
110            ipv4_addresses,
111            ipv6_addresses,
112        }
113    }
114
115    /// Returns true if this adapter has any addresses (IPv4 or IPv6).
116    #[must_use]
117    pub fn has_addresses(&self) -> bool {
118        !self.ipv4_addresses.is_empty() || !self.ipv6_addresses.is_empty()
119    }
120
121    /// Returns the total number of addresses (IPv4 + IPv6).
122    #[must_use]
123    pub fn address_count(&self) -> usize {
124        self.ipv4_addresses.len() + self.ipv6_addresses.len()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    mod ip_version {
133        use super::*;
134
135        #[test]
136        fn v4_includes_only_v4() {
137            assert!(IpVersion::V4.includes_v4());
138            assert!(!IpVersion::V4.includes_v6());
139        }
140
141        #[test]
142        fn v6_includes_only_v6() {
143            assert!(!IpVersion::V6.includes_v4());
144            assert!(IpVersion::V6.includes_v6());
145        }
146
147        #[test]
148        fn both_includes_both() {
149            assert!(IpVersion::Both.includes_v4());
150            assert!(IpVersion::Both.includes_v6());
151        }
152
153        #[test]
154        fn display_formats_correctly() {
155            assert_eq!(format!("{}", IpVersion::V4), "IPv4");
156            assert_eq!(format!("{}", IpVersion::V6), "IPv6");
157            assert_eq!(format!("{}", IpVersion::Both), "Both");
158        }
159    }
160
161    mod adapter_kind {
162        use super::*;
163
164        #[test]
165        fn virtual_is_virtual() {
166            assert!(AdapterKind::Virtual.is_virtual());
167            assert!(!AdapterKind::Ethernet.is_virtual());
168            assert!(!AdapterKind::Wireless.is_virtual());
169            assert!(!AdapterKind::Loopback.is_virtual());
170            assert!(!AdapterKind::Other(999).is_virtual());
171        }
172
173        #[test]
174        fn loopback_is_loopback() {
175            assert!(AdapterKind::Loopback.is_loopback());
176            assert!(!AdapterKind::Ethernet.is_loopback());
177            assert!(!AdapterKind::Virtual.is_loopback());
178        }
179
180        #[test]
181        fn other_preserves_type_code() {
182            let kind = AdapterKind::Other(42);
183            assert_eq!(kind, AdapterKind::Other(42));
184            assert_ne!(kind, AdapterKind::Other(99));
185        }
186    }
187
188    mod adapter_snapshot {
189        use super::*;
190
191        fn make_snapshot() -> AdapterSnapshot {
192            AdapterSnapshot::new(
193                "eth0",
194                AdapterKind::Ethernet,
195                vec!["192.168.1.1".parse().unwrap()],
196                vec!["fe80::1".parse().unwrap()],
197            )
198        }
199
200        #[test]
201        fn new_creates_snapshot_with_correct_fields() {
202            let snapshot = make_snapshot();
203
204            assert_eq!(snapshot.name, "eth0");
205            assert_eq!(snapshot.kind, AdapterKind::Ethernet);
206            assert_eq!(snapshot.ipv4_addresses.len(), 1);
207            assert_eq!(snapshot.ipv6_addresses.len(), 1);
208        }
209
210        #[test]
211        fn has_addresses_true_with_ipv4() {
212            let snapshot = AdapterSnapshot::new(
213                "eth0",
214                AdapterKind::Ethernet,
215                vec!["192.168.1.1".parse().unwrap()],
216                vec![],
217            );
218            assert!(snapshot.has_addresses());
219        }
220
221        #[test]
222        fn has_addresses_true_with_ipv6() {
223            let snapshot = AdapterSnapshot::new(
224                "eth0",
225                AdapterKind::Ethernet,
226                vec![],
227                vec!["fe80::1".parse().unwrap()],
228            );
229            assert!(snapshot.has_addresses());
230        }
231
232        #[test]
233        fn has_addresses_false_when_empty() {
234            let snapshot = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
235            assert!(!snapshot.has_addresses());
236        }
237
238        #[test]
239        fn address_count_sums_both_types() {
240            let snapshot = AdapterSnapshot::new(
241                "eth0",
242                AdapterKind::Ethernet,
243                vec![
244                    "192.168.1.1".parse().unwrap(),
245                    "192.168.1.2".parse().unwrap(),
246                ],
247                vec!["fe80::1".parse().unwrap()],
248            );
249            assert_eq!(snapshot.address_count(), 3);
250        }
251
252        #[test]
253        fn address_count_zero_when_empty() {
254            let snapshot = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
255            assert_eq!(snapshot.address_count(), 0);
256        }
257
258        #[test]
259        fn equality_requires_same_name() {
260            let snapshot1 = make_snapshot();
261            let mut snapshot2 = make_snapshot();
262            snapshot2.name = "eth1".to_string();
263
264            assert_ne!(snapshot1, snapshot2);
265        }
266
267        #[test]
268        fn equality_requires_same_kind() {
269            let snapshot1 = make_snapshot();
270            let mut snapshot2 = make_snapshot();
271            snapshot2.kind = AdapterKind::Wireless;
272
273            assert_ne!(snapshot1, snapshot2);
274        }
275
276        #[test]
277        fn equality_requires_same_addresses() {
278            let snapshot1 = make_snapshot();
279            let mut snapshot2 = make_snapshot();
280            snapshot2.ipv4_addresses.push("10.0.0.1".parse().unwrap());
281
282            assert_ne!(snapshot1, snapshot2);
283        }
284    }
285}