1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4#[cfg(feature = "derive_builder")]
5use derive_builder::Builder;
6
7#[derive(Default, Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[cfg_attr(feature = "derive_builder", derive(Builder))]
25#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
26#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
27pub struct DhcpOverrides {
28 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
33 #[cfg_attr(feature = "serde", serde(default))]
34 #[cfg_attr(
35 feature = "serde",
36 serde(deserialize_with = "crate::bool::string_or_bool_option")
37 )]
38 pub use_dns: Option<bool>,
39 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
44 #[cfg_attr(feature = "serde", serde(default))]
45 #[cfg_attr(
46 feature = "serde",
47 serde(deserialize_with = "crate::bool::string_or_bool_option")
48 )]
49 pub use_ntp: Option<bool>,
50 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
54 #[cfg_attr(feature = "serde", serde(default))]
55 #[cfg_attr(
56 feature = "serde",
57 serde(deserialize_with = "crate::bool::string_or_bool_option")
58 )]
59 pub send_hostname: Option<bool>,
60 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
64 #[cfg_attr(feature = "serde", serde(default))]
65 #[cfg_attr(
66 feature = "serde",
67 serde(deserialize_with = "crate::bool::string_or_bool_option")
68 )]
69 pub use_hostname: Option<bool>,
70 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
75 #[cfg_attr(feature = "serde", serde(default))]
76 #[cfg_attr(
77 feature = "serde",
78 serde(deserialize_with = "crate::bool::string_or_bool_option")
79 )]
80 pub use_mtu: Option<bool>,
81 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
85 pub hostname: Option<String>,
86 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
94 #[cfg_attr(feature = "serde", serde(default))]
95 #[cfg_attr(
96 feature = "serde",
97 serde(deserialize_with = "crate::bool::string_or_bool_option")
98 )]
99 pub use_routes: Option<bool>,
100 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
105 pub route_metric: Option<u16>,
106 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
114 pub use_domains: Option<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
119#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
120pub enum Ipv6AddressGeneration {
121 #[cfg_attr(feature = "serde", serde(rename = "eui64"))]
122 Eui64,
123 #[cfg_attr(feature = "serde", serde(rename = "stable-privacy"))]
124 StablePrivacy,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "serde", serde(untagged))]
130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
131pub enum AddressMapping {
132 Simple(String),
133 Complex(std::collections::HashMap<String, AddressProperties>),
134}
135
136#[derive(Default, Debug, Clone, PartialEq, Eq)]
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
138#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
139#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
140pub struct AddressProperties {
141 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
144 pub label: Option<String>,
145
146 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
151 pub lifetime: Option<PreferredLifetime>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
156#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
157pub enum PreferredLifetime {
158 #[cfg_attr(feature = "serde", serde(rename = "forever"))]
159 Forever,
160 #[cfg_attr(feature = "serde", serde(rename = "0"))]
161 Zero,
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_dhcp_overrides_defaults() {
170 let overrides = DhcpOverrides::default();
171 assert_eq!(overrides.use_dns, None);
172 assert_eq!(overrides.use_ntp, None);
173 assert_eq!(overrides.send_hostname, None);
174 assert_eq!(overrides.use_hostname, None);
175 assert_eq!(overrides.use_mtu, None);
176 assert_eq!(overrides.hostname, None);
177 assert_eq!(overrides.use_routes, None);
178 assert_eq!(overrides.route_metric, None);
179 assert_eq!(overrides.use_domains, None);
180 }
181
182 #[test]
183 #[cfg(feature = "serde")]
184 fn test_dhcp_overrides_serialize() {
185 let overrides = DhcpOverrides {
186 use_dns: Some(false),
187 use_ntp: Some(true),
188 send_hostname: Some(true),
189 use_hostname: Some(false),
190 use_mtu: Some(true),
191 hostname: Some("test-host".to_string()),
192 use_routes: Some(false),
193 route_metric: Some(100),
194 use_domains: Some("route".to_string()),
195 };
196
197 let yaml = serde_yaml::to_string(&overrides).unwrap();
198 assert!(yaml.contains("use-dns: false"));
199 assert!(yaml.contains("use-ntp: true"));
200 assert!(yaml.contains("send-hostname: true"));
201 assert!(yaml.contains("use-hostname: false"));
202 assert!(yaml.contains("use-mtu: true"));
203 assert!(yaml.contains("hostname: test-host"));
204 assert!(yaml.contains("use-routes: false"));
205 assert!(yaml.contains("route-metric: 100"));
206 assert!(yaml.contains("use-domains: route"));
207 }
208
209 #[test]
210 #[cfg(feature = "serde")]
211 fn test_dhcp_overrides_deserialize() {
212 let yaml = r#"
213use-dns: false
214use-ntp: true
215send-hostname: true
216use-hostname: false
217use-mtu: true
218hostname: test-host
219use-routes: false
220route-metric: 100
221use-domains: route
222"#;
223
224 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
225 assert_eq!(overrides.use_dns, Some(false));
226 assert_eq!(overrides.use_ntp, Some(true));
227 assert_eq!(overrides.send_hostname, Some(true));
228 assert_eq!(overrides.use_hostname, Some(false));
229 assert_eq!(overrides.use_mtu, Some(true));
230 assert_eq!(overrides.hostname, Some("test-host".to_string()));
231 assert_eq!(overrides.use_routes, Some(false));
232 assert_eq!(overrides.route_metric, Some(100));
233 assert_eq!(overrides.use_domains, Some("route".to_string()));
234 }
235
236 #[test]
237 #[cfg(feature = "serde")]
238 fn test_dhcp_overrides_skip_none_serialization() {
239 let overrides = DhcpOverrides {
240 use_dns: Some(false),
241 use_ntp: None,
242 ..Default::default()
243 };
244
245 let yaml = serde_yaml::to_string(&overrides).unwrap();
246 assert!(yaml.contains("use-dns: false"));
247 assert!(!yaml.contains("use-ntp"));
248 assert!(!yaml.contains("send-hostname"));
249 }
250
251 #[test]
252 #[cfg(feature = "serde")]
253 fn test_dhcp_overrides_bool_as_string() {
254 let yaml = r#"
255use-dns: "false"
256use-ntp: "true"
257"#;
258
259 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
260 assert_eq!(overrides.use_dns, Some(false));
261 assert_eq!(overrides.use_ntp, Some(true));
262 }
263
264 #[test]
265 #[cfg(feature = "serde")]
266 fn test_ipv6_address_generation_serialize() {
267 let eui64 = Ipv6AddressGeneration::Eui64;
268 let stable = Ipv6AddressGeneration::StablePrivacy;
269
270 let eui64_yaml = serde_yaml::to_string(&eui64).unwrap();
271 let stable_yaml = serde_yaml::to_string(&stable).unwrap();
272
273 assert!(eui64_yaml.contains("eui64"));
274 assert!(stable_yaml.contains("stable-privacy"));
275 }
276
277 #[test]
278 #[cfg(feature = "serde")]
279 fn test_ipv6_address_generation_deserialize() {
280 let eui64: Ipv6AddressGeneration = serde_yaml::from_str("eui64").unwrap();
281 let stable: Ipv6AddressGeneration = serde_yaml::from_str("stable-privacy").unwrap();
282
283 assert_eq!(eui64, Ipv6AddressGeneration::Eui64);
284 assert_eq!(stable, Ipv6AddressGeneration::StablePrivacy);
285 }
286
287 #[test]
288 #[cfg(feature = "serde")]
289 fn test_address_mapping_simple() {
290 let simple = AddressMapping::Simple("192.168.1.10/24".to_string());
291
292 let yaml = serde_yaml::to_string(&simple).unwrap();
293 assert_eq!(yaml.trim(), "192.168.1.10/24");
294
295 let deserialized: AddressMapping = serde_yaml::from_str("192.168.1.10/24").unwrap();
296 assert_eq!(deserialized, simple);
297 }
298
299 #[test]
300 #[cfg(feature = "serde")]
301 fn test_address_mapping_complex() {
302 let mut map = std::collections::HashMap::new();
303 map.insert(
304 "192.168.1.10/24".to_string(),
305 AddressProperties {
306 label: Some("my-label".to_string()),
307 lifetime: Some(PreferredLifetime::Forever),
308 },
309 );
310 let complex = AddressMapping::Complex(map);
311
312 let yaml = serde_yaml::to_string(&complex).unwrap();
313 assert!(yaml.contains("192.168.1.10/24"));
314 assert!(yaml.contains("label: my-label"));
315 assert!(yaml.contains("lifetime: forever"));
316 }
317
318 #[test]
319 #[cfg(feature = "serde")]
320 fn test_address_mapping_complex_deserialize() {
321 let yaml = r#"
322192.168.1.10/24:
323 label: my-label
324 lifetime: forever
325"#;
326
327 let mapping: AddressMapping = serde_yaml::from_str(yaml).unwrap();
328
329 if let AddressMapping::Complex(map) = mapping {
330 let props = map.get("192.168.1.10/24").unwrap();
331 assert_eq!(props.label, Some("my-label".to_string()));
332 assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
333 } else {
334 panic!("Expected Complex variant");
335 }
336 }
337
338 #[test]
339 fn test_address_properties_defaults() {
340 let props = AddressProperties::default();
341 assert_eq!(props.label, None);
342 assert_eq!(props.lifetime, None);
343 }
344
345 #[test]
346 #[cfg(feature = "serde")]
347 fn test_address_properties_lifetime_forever() {
348 let yaml = r#"
349label: test
350lifetime: forever
351"#;
352
353 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
354 assert_eq!(props.label, Some("test".to_string()));
355 assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
356 }
357
358 #[test]
359 #[cfg(feature = "serde")]
360 fn test_address_properties_lifetime_zero() {
361 let yaml = r#"
362label: test
363lifetime: "0"
364"#;
365
366 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
367 assert_eq!(props.label, Some("test".to_string()));
368 assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
369 }
370
371 #[test]
372 #[cfg(feature = "serde")]
373 fn test_address_properties_skip_none() {
374 let props = AddressProperties {
375 label: Some("test".to_string()),
376 lifetime: None,
377 };
378
379 let yaml = serde_yaml::to_string(&props).unwrap();
380 assert!(yaml.contains("label: test"));
381 assert!(!yaml.contains("lifetime"));
382 }
383
384 #[test]
385 #[cfg(feature = "serde")]
386 fn test_address_properties_label_only() {
387 let yaml = r#"
388label: management
389"#;
390
391 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
392 assert_eq!(props.label, Some("management".to_string()));
393 assert_eq!(props.lifetime, None);
394 }
395
396 #[test]
397 #[cfg(feature = "serde")]
398 fn test_use_domains_boolean() {
399 let yaml = r#"
400use-domains: true
401"#;
402
403 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
404 assert_eq!(overrides.use_domains, Some("true".to_string()));
405 }
406
407 #[test]
408 #[cfg(feature = "serde")]
409 fn test_use_domains_route() {
410 let yaml = r#"
411use-domains: route
412"#;
413
414 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
415 assert_eq!(overrides.use_domains, Some("route".to_string()));
416 }
417
418 #[test]
419 #[cfg(feature = "serde")]
420 fn test_address_mapping_list() {
421 let yaml = r#"
422- 192.168.1.10/24
423- 192.168.1.11/24:
424 label: backup
425 lifetime: "0"
426- 10.0.0.1/8
427"#;
428
429 let mappings: Vec<AddressMapping> = serde_yaml::from_str(yaml).unwrap();
430 assert_eq!(mappings.len(), 3);
431
432 assert!(matches!(mappings[0], AddressMapping::Simple(_)));
434
435 if let AddressMapping::Complex(map) = &mappings[1] {
437 let props = map.get("192.168.1.11/24").unwrap();
438 assert_eq!(props.label, Some("backup".to_string()));
439 assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
440 } else {
441 panic!("Expected Complex variant");
442 }
443
444 assert!(matches!(mappings[2], AddressMapping::Simple(_)));
446 }
447
448 #[test]
449 fn test_ipv6_address_generation_equality() {
450 assert_eq!(Ipv6AddressGeneration::Eui64, Ipv6AddressGeneration::Eui64);
451 assert_eq!(
452 Ipv6AddressGeneration::StablePrivacy,
453 Ipv6AddressGeneration::StablePrivacy
454 );
455 assert_ne!(
456 Ipv6AddressGeneration::Eui64,
457 Ipv6AddressGeneration::StablePrivacy
458 );
459 }
460
461 #[test]
462 #[cfg(feature = "serde")]
463 fn test_route_metric_range() {
464 let yaml = r#"
465route-metric: 65535
466"#;
467
468 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
469 assert_eq!(overrides.route_metric, Some(65535));
470 }
471
472 #[test]
473 #[cfg(feature = "serde")]
474 fn test_empty_dhcp_overrides() {
475 let yaml = "{}";
476 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
477
478 assert_eq!(overrides.use_dns, None);
479 assert_eq!(overrides.use_ntp, None);
480 assert_eq!(overrides.route_metric, None);
481 }
482}