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<crate::UseDomains>,
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 #[cfg(feature = "serde")]
168 use crate::UseDomains;
169
170 #[test]
171 fn test_dhcp_overrides_defaults() {
172 let overrides = DhcpOverrides::default();
173 assert_eq!(overrides.use_dns, None);
174 assert_eq!(overrides.use_ntp, None);
175 assert_eq!(overrides.send_hostname, None);
176 assert_eq!(overrides.use_hostname, None);
177 assert_eq!(overrides.use_mtu, None);
178 assert_eq!(overrides.hostname, None);
179 assert_eq!(overrides.use_routes, None);
180 assert_eq!(overrides.route_metric, None);
181 assert_eq!(overrides.use_domains, None);
182 }
183
184 #[test]
185 #[cfg(feature = "serde")]
186 fn test_dhcp_overrides_serialize() {
187 let overrides = DhcpOverrides {
188 use_dns: Some(false),
189 use_ntp: Some(true),
190 send_hostname: Some(true),
191 use_hostname: Some(false),
192 use_mtu: Some(true),
193 hostname: Some("test-host".to_string()),
194 use_routes: Some(false),
195 route_metric: Some(100),
196 use_domains: Some(UseDomains::Route),
197 };
198
199 let yaml = serde_yaml::to_string(&overrides).unwrap();
200 assert!(yaml.contains("use-dns: false"));
201 assert!(yaml.contains("use-ntp: true"));
202 assert!(yaml.contains("send-hostname: true"));
203 assert!(yaml.contains("use-hostname: false"));
204 assert!(yaml.contains("use-mtu: true"));
205 assert!(yaml.contains("hostname: test-host"));
206 assert!(yaml.contains("use-routes: false"));
207 assert!(yaml.contains("route-metric: 100"));
208 assert!(yaml.contains("use-domains: route"));
209 }
210
211 #[test]
212 #[cfg(feature = "serde")]
213 fn test_dhcp_overrides_deserialize() {
214 let yaml = r#"
215use-dns: false
216use-ntp: true
217send-hostname: true
218use-hostname: false
219use-mtu: true
220hostname: test-host
221use-routes: false
222route-metric: 100
223use-domains: route
224"#;
225
226 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
227 assert_eq!(overrides.use_dns, Some(false));
228 assert_eq!(overrides.use_ntp, Some(true));
229 assert_eq!(overrides.send_hostname, Some(true));
230 assert_eq!(overrides.use_hostname, Some(false));
231 assert_eq!(overrides.use_mtu, Some(true));
232 assert_eq!(overrides.hostname, Some("test-host".to_string()));
233 assert_eq!(overrides.use_routes, Some(false));
234 assert_eq!(overrides.route_metric, Some(100));
235 assert_eq!(overrides.use_domains, Some(UseDomains::Route));
236 }
237
238 #[test]
239 #[cfg(feature = "serde")]
240 fn test_dhcp_overrides_skip_none_serialization() {
241 let overrides = DhcpOverrides {
242 use_dns: Some(false),
243 use_ntp: None,
244 ..Default::default()
245 };
246
247 let yaml = serde_yaml::to_string(&overrides).unwrap();
248 assert!(yaml.contains("use-dns: false"));
249 assert!(!yaml.contains("use-ntp"));
250 assert!(!yaml.contains("send-hostname"));
251 }
252
253 #[test]
254 #[cfg(feature = "serde")]
255 fn test_dhcp_overrides_bool_as_string() {
256 let yaml = r#"
257use-dns: "false"
258use-ntp: "true"
259"#;
260
261 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
262 assert_eq!(overrides.use_dns, Some(false));
263 assert_eq!(overrides.use_ntp, Some(true));
264 }
265
266 #[test]
267 #[cfg(feature = "serde")]
268 fn test_ipv6_address_generation_serialize() {
269 let eui64 = Ipv6AddressGeneration::Eui64;
270 let stable = Ipv6AddressGeneration::StablePrivacy;
271
272 let eui64_yaml = serde_yaml::to_string(&eui64).unwrap();
273 let stable_yaml = serde_yaml::to_string(&stable).unwrap();
274
275 assert!(eui64_yaml.contains("eui64"));
276 assert!(stable_yaml.contains("stable-privacy"));
277 }
278
279 #[test]
280 #[cfg(feature = "serde")]
281 fn test_ipv6_address_generation_deserialize() {
282 let eui64: Ipv6AddressGeneration = serde_yaml::from_str("eui64").unwrap();
283 let stable: Ipv6AddressGeneration = serde_yaml::from_str("stable-privacy").unwrap();
284
285 assert_eq!(eui64, Ipv6AddressGeneration::Eui64);
286 assert_eq!(stable, Ipv6AddressGeneration::StablePrivacy);
287 }
288
289 #[test]
290 #[cfg(feature = "serde")]
291 fn test_address_mapping_simple() {
292 let simple = AddressMapping::Simple("192.168.1.10/24".to_string());
293
294 let yaml = serde_yaml::to_string(&simple).unwrap();
295 assert_eq!(yaml.trim(), "192.168.1.10/24");
296
297 let deserialized: AddressMapping = serde_yaml::from_str("192.168.1.10/24").unwrap();
298 assert_eq!(deserialized, simple);
299 }
300
301 #[test]
302 #[cfg(feature = "serde")]
303 fn test_address_mapping_complex() {
304 let mut map = std::collections::HashMap::new();
305 map.insert(
306 "192.168.1.10/24".to_string(),
307 AddressProperties {
308 label: Some("my-label".to_string()),
309 lifetime: Some(PreferredLifetime::Forever),
310 },
311 );
312 let complex = AddressMapping::Complex(map);
313
314 let yaml = serde_yaml::to_string(&complex).unwrap();
315 assert!(yaml.contains("192.168.1.10/24"));
316 assert!(yaml.contains("label: my-label"));
317 assert!(yaml.contains("lifetime: forever"));
318 }
319
320 #[test]
321 #[cfg(feature = "serde")]
322 fn test_address_mapping_complex_deserialize() {
323 let yaml = r#"
324192.168.1.10/24:
325 label: my-label
326 lifetime: forever
327"#;
328
329 let mapping: AddressMapping = serde_yaml::from_str(yaml).unwrap();
330
331 if let AddressMapping::Complex(map) = mapping {
332 let props = map.get("192.168.1.10/24").unwrap();
333 assert_eq!(props.label, Some("my-label".to_string()));
334 assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
335 } else {
336 panic!("Expected Complex variant");
337 }
338 }
339
340 #[test]
341 fn test_address_properties_defaults() {
342 let props = AddressProperties::default();
343 assert_eq!(props.label, None);
344 assert_eq!(props.lifetime, None);
345 }
346
347 #[test]
348 #[cfg(feature = "serde")]
349 fn test_address_properties_lifetime_forever() {
350 let yaml = r#"
351label: test
352lifetime: forever
353"#;
354
355 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
356 assert_eq!(props.label, Some("test".to_string()));
357 assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
358 }
359
360 #[test]
361 #[cfg(feature = "serde")]
362 fn test_address_properties_lifetime_zero() {
363 let yaml = r#"
364label: test
365lifetime: "0"
366"#;
367
368 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
369 assert_eq!(props.label, Some("test".to_string()));
370 assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
371 }
372
373 #[test]
374 #[cfg(feature = "serde")]
375 fn test_address_properties_skip_none() {
376 let props = AddressProperties {
377 label: Some("test".to_string()),
378 lifetime: None,
379 };
380
381 let yaml = serde_yaml::to_string(&props).unwrap();
382 assert!(yaml.contains("label: test"));
383 assert!(!yaml.contains("lifetime"));
384 }
385
386 #[test]
387 #[cfg(feature = "serde")]
388 fn test_address_properties_label_only() {
389 let yaml = r#"
390label: management
391"#;
392
393 let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
394 assert_eq!(props.label, Some("management".to_string()));
395 assert_eq!(props.lifetime, None);
396 }
397
398 #[test]
399 #[cfg(feature = "serde")]
400 fn test_use_domains_boolean() {
401 let yaml = r#"
402use-domains: true
403"#;
404
405 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
406 assert_eq!(overrides.use_domains, Some(UseDomains::Boolean(true)));
407 }
408
409 #[test]
410 #[cfg(feature = "serde")]
411 fn test_use_domains_boolean_quoted() {
412 let yaml = r#"
413use-domains: "false"
414"#;
415
416 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
417 assert_eq!(overrides.use_domains, Some(UseDomains::Boolean(false)));
418 }
419
420 #[test]
421 #[cfg(feature = "serde")]
422 fn test_use_domains_route() {
423 let yaml = r#"
424use-domains: route
425"#;
426
427 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
428 assert_eq!(overrides.use_domains, Some(UseDomains::Route));
429 }
430
431 #[test]
432 #[cfg(feature = "serde")]
433 fn test_use_domains_serialize_roundtrip() {
434 let boolean = DhcpOverrides {
435 use_domains: Some(UseDomains::Boolean(true)),
436 ..Default::default()
437 };
438 let yaml = serde_yaml::to_string(&boolean).unwrap();
439 assert!(yaml.contains("use-domains: true"));
440 assert_eq!(
441 serde_yaml::from_str::<DhcpOverrides>(&yaml).unwrap(),
442 boolean
443 );
444
445 let route = DhcpOverrides {
446 use_domains: Some(UseDomains::Route),
447 ..Default::default()
448 };
449 let yaml = serde_yaml::to_string(&route).unwrap();
450 assert!(yaml.contains("use-domains: route"));
451 assert_eq!(serde_yaml::from_str::<DhcpOverrides>(&yaml).unwrap(), route);
452 }
453
454 #[test]
455 #[cfg(feature = "serde")]
456 fn test_address_mapping_list() {
457 let yaml = r#"
458- 192.168.1.10/24
459- 192.168.1.11/24:
460 label: backup
461 lifetime: "0"
462- 10.0.0.1/8
463"#;
464
465 let mappings: Vec<AddressMapping> = serde_yaml::from_str(yaml).unwrap();
466 assert_eq!(mappings.len(), 3);
467
468 assert!(matches!(mappings[0], AddressMapping::Simple(_)));
470
471 if let AddressMapping::Complex(map) = &mappings[1] {
473 let props = map.get("192.168.1.11/24").unwrap();
474 assert_eq!(props.label, Some("backup".to_string()));
475 assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
476 } else {
477 panic!("Expected Complex variant");
478 }
479
480 assert!(matches!(mappings[2], AddressMapping::Simple(_)));
482 }
483
484 #[test]
485 fn test_ipv6_address_generation_equality() {
486 assert_eq!(Ipv6AddressGeneration::Eui64, Ipv6AddressGeneration::Eui64);
487 assert_eq!(
488 Ipv6AddressGeneration::StablePrivacy,
489 Ipv6AddressGeneration::StablePrivacy
490 );
491 assert_ne!(
492 Ipv6AddressGeneration::Eui64,
493 Ipv6AddressGeneration::StablePrivacy
494 );
495 }
496
497 #[test]
498 #[cfg(feature = "serde")]
499 fn test_route_metric_range() {
500 let yaml = r#"
501route-metric: 65535
502"#;
503
504 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
505 assert_eq!(overrides.route_metric, Some(65535));
506 }
507
508 #[test]
509 #[cfg(feature = "serde")]
510 fn test_empty_dhcp_overrides() {
511 let yaml = "{}";
512 let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
513
514 assert_eq!(overrides.use_dns, None);
515 assert_eq!(overrides.use_ntp, None);
516 assert_eq!(overrides.route_metric, None);
517 }
518}