genja_core/inventory/models.rs
1use crate::CustomTreeMap;
2use genja_core_derive::{DerefMacro, DerefMutMacro};
3use schemars::{JsonSchema, schema_for};
4use serde::de::{Error, SeqAccess, Unexpected, Visitor};
5use serde::{Deserialize, Deserializer, Serialize};
6use std::fmt;
7
8pub trait BaseMethods {
9 fn schema() -> String
10 where
11 Self: Sized,
12 Self: JsonSchema,
13 {
14 let schema = schema_for!(Self);
15 serde_json::to_string_pretty(&schema).unwrap()
16 }
17}
18
19pub trait BaseBuilderHost {
20 type Output;
21
22 // Updates the hostname and returns the updated builder.
23 fn hostname<S>(self, hostname: S) -> Self
24 where
25 S: Into<String>;
26
27 /// Updates the port and returns the updated builder.
28 fn port(self, port: u16) -> Self;
29
30 /// Updates the username and returns the updated builder.
31 fn username<S>(self, username: S) -> Self
32 where
33 S: Into<String>;
34
35 /// Updates the password and returns the updated builder.
36 fn password<S>(self, password: S) -> Self
37 where
38 S: Into<String>;
39
40 /// Updates the platform and returns the updated builder.
41 fn platform<S>(self, platform: S) -> Self
42 where
43 S: Into<String>;
44
45 /// Updates the groups and returns the updated builder.
46 fn groups(self, groups: ParentGroups) -> Self;
47
48 /// Updates the data and returns the updated builder.
49 fn data(self, data: Data) -> Self;
50
51 /// Updates the connection options and returns the updated builder.
52 fn connection_options<S>(self, name: S, options: ConnectionOptions) -> Self
53 where
54 S: Into<String>;
55
56 /// Builds the struct from the updated builder and returns final struct object.
57 fn build(self) -> Self::Output;
58}
59
60// Required for the DerefMacro derive to satisfy the DerefTarget trait.
61pub trait DerefTarget {
62 type Target;
63}
64
65/// Connection-specific configuration options that can override base host settings.
66///
67/// This struct defines optional connection parameters that can be specified per connection plugin name
68/// (e.g., "ssh", "netconf", "http") to override the base connection settings defined at the host,
69/// group, or defaults level. Connection options are stored in a map keyed by connection plugin name
70/// and are applied during connection parameter resolution.
71///
72/// All fields are optional, allowing partial overrides. When resolving connection parameters,
73/// these options take precedence over base settings at the same hierarchy level (host, group, or defaults).
74///
75/// # Fields
76///
77/// * `hostname` - Optional hostname or IP address override for this connection plugin name.
78/// When specified, overrides the base hostname for connections of this type.
79///
80/// * `port` - Optional port number override for this connection plugin name.
81/// When specified, overrides the base port for connections of this type.
82///
83/// * `username` - Optional username override for authentication.
84/// When specified, overrides the base username for connections of this type.
85///
86/// * `password` - Optional password override for authentication.
87/// When specified, overrides the base password for connections of this type.
88///
89/// * `platform` - Optional platform identifier override.
90/// When specified, overrides the base platform for connections of this type.
91///
92/// * `extras` - Optional arbitrary JSON data for connection-specific configuration.
93/// Allows storing additional connection parameters that don't fit the standard fields.
94///
95/// # Examples
96///
97/// ```
98/// # use genja_core::inventory::ConnectionOptions;
99/// let options = ConnectionOptions::builder()
100/// .port(830)
101/// .username("netconf_user")
102/// .build();
103/// ```
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
105pub struct ConnectionOptions {
106 pub(crate) hostname: Option<String>,
107 pub(crate) port: Option<u16>,
108 pub(crate) username: Option<String>,
109 pub(crate) password: Option<String>,
110 pub(crate) platform: Option<String>,
111 pub(crate) extras: Option<Extras>,
112}
113
114impl Default for ConnectionOptions {
115 fn default() -> Self {
116 Self::builder().build()
117 }
118}
119
120impl ConnectionOptions {
121 pub fn builder() -> ConnectionOptionsBuilder {
122 ConnectionOptionsBuilder::new()
123 }
124
125 pub fn hostname(&self) -> Option<&str> {
126 self.hostname.as_deref()
127 }
128
129 pub fn port(&self) -> Option<u16> {
130 self.port
131 }
132
133 pub fn username(&self) -> Option<&str> {
134 self.username.as_deref()
135 }
136
137 pub fn password(&self) -> Option<&str> {
138 self.password.as_deref()
139 }
140
141 pub fn platform(&self) -> Option<&str> {
142 self.platform.as_deref()
143 }
144
145 pub fn extras(&self) -> Option<&Extras> {
146 self.extras.as_ref()
147 }
148
149 /// Converts this `ConnectionOptions` instance into a builder for modification.
150 ///
151 /// This method creates a new `ConnectionOptionsBuilder` initialized with all the current
152 /// values from this `ConnectionOptions` instance. This is useful when you need to create
153 /// a modified copy of existing connection options while preserving most of the original
154 /// configuration.
155 ///
156 /// # Returns
157 ///
158 /// Returns a `ConnectionOptionsBuilder` with all fields initialized to match the current
159 /// `ConnectionOptions` instance. The builder can then be used to modify specific fields
160 /// before calling `build()` to create a new `ConnectionOptions` instance.
161 ///
162 /// # Examples
163 ///
164 /// ```
165 /// # use genja_core::inventory::ConnectionOptions;
166 /// let options = ConnectionOptions::builder()
167 /// .port(830)
168 /// .username("netconf_user")
169 /// .build();
170 ///
171 /// let modified = options.to_builder()
172 /// .port(831)
173 /// .build();
174 ///
175 /// assert_eq!(modified.port(), Some(831));
176 /// ```
177 pub fn to_builder(&self) -> ConnectionOptionsBuilder {
178 ConnectionOptionsBuilder {
179 hostname: self.hostname.clone(),
180 port: self.port,
181 username: self.username.clone(),
182 password: self.password.clone(),
183 platform: self.platform.clone(),
184 extras: self.extras.clone(),
185 }
186 }
187}
188
189/// Builder for constructing `ConnectionOptions` instances.
190///
191/// This builder provides a fluent interface for creating connection options with optional
192/// field overrides. All fields start as `None` and can be set individually before calling
193/// `build()` to create the final `ConnectionOptions` instance.
194///
195/// The builder is typically created via `ConnectionOptions::builder()` or by converting
196/// an existing `ConnectionOptions` instance using `to_builder()`.
197///
198/// # Fields
199///
200/// * `hostname` - Optional hostname or IP address override for the connection plugin name.
201/// When set, this value will override the base hostname for connections of this type.
202///
203/// * `port` - Optional port number override for the connection plugin name.
204/// When set, this value will override the base port for connections of this type.
205///
206/// * `username` - Optional username override for authentication.
207/// When set, this value will override the base username for connections of this type.
208///
209/// * `password` - Optional password override for authentication.
210/// When set, this value will override the base password for connections of this type.
211///
212/// * `platform` - Optional platform identifier override.
213/// When set, this value will override the base platform for connections of this type.
214///
215/// * `extras` - Optional arbitrary JSON data for connection-specific configuration.
216/// Allows storing additional connection parameters that don't fit the standard fields.
217///
218/// # Examples
219///
220/// ```
221/// # use genja_core::inventory::ConnectionOptions;
222/// let options = ConnectionOptions::builder()
223/// .hostname("10.0.0.1")
224/// .port(830)
225/// .username("netconf_user")
226/// .build();
227///
228/// assert_eq!(options.hostname(), Some("10.0.0.1"));
229/// assert_eq!(options.port(), Some(830));
230/// ```
231pub struct ConnectionOptionsBuilder {
232 hostname: Option<String>,
233 port: Option<u16>,
234 username: Option<String>,
235 password: Option<String>,
236 platform: Option<String>,
237 extras: Option<Extras>,
238}
239
240impl ConnectionOptionsBuilder {
241 pub fn new() -> Self {
242 Self {
243 hostname: None,
244 port: None,
245 username: None,
246 password: None,
247 platform: None,
248 extras: None,
249 }
250 }
251
252 pub fn hostname<S>(mut self, hostname: S) -> Self
253 where
254 S: Into<String>,
255 {
256 self.hostname = Some(hostname.into());
257 self
258 }
259
260 pub fn port(mut self, port: u16) -> Self {
261 self.port = Some(port);
262 self
263 }
264
265 pub fn username<S>(mut self, username: S) -> Self
266 where
267 S: Into<String>,
268 {
269 self.username = Some(username.into());
270 self
271 }
272
273 pub fn password<S>(mut self, password: S) -> Self
274 where
275 S: Into<String>,
276 {
277 self.password = Some(password.into());
278 self
279 }
280
281 pub fn platform<S>(mut self, platform: S) -> Self
282 where
283 S: Into<String>,
284 {
285 self.platform = Some(platform.into());
286 self
287 }
288
289 pub fn extras(mut self, extras: Extras) -> Self {
290 self.extras = Some(extras);
291 self
292 }
293
294 pub fn build(self) -> ConnectionOptions {
295 ConnectionOptions {
296 hostname: self.hostname,
297 port: self.port,
298 username: self.username,
299 password: self.password,
300 platform: self.platform,
301 extras: self.extras,
302 }
303 }
304}
305
306impl Default for ConnectionOptionsBuilder {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312/// Fully resolved connection parameters for establishing a connection to a host.
313///
314/// This struct represents the final, merged connection configuration after applying
315/// defaults, group settings, host-specific settings, and connection-plugin-name-specific
316/// overrides. It contains all the information needed to establish a connection to
317/// a target host using a specific connection plugin name (e.g., SSH, NETCONF, HTTP).
318///
319/// The resolution process follows a hierarchical priority order where settings at
320/// higher levels (host-specific) override settings at lower levels (defaults).
321/// Connection-specific options can override base settings at each hierarchy level.
322///
323/// # Fields
324///
325/// * `hostname` - The resolved hostname or IP address for the connection.
326/// This field is always present and defaults to an empty string if not specified
327/// anywhere in the hierarchy. It represents the target address for the connection.
328///
329/// * `port` - Optional port number for the connection. If `None`, the connection
330/// implementation should use its default port. When specified, it indicates the
331/// TCP/UDP port to use for establishing the connection.
332///
333/// * `username` - Optional username for authentication. If `None`, the connection
334/// may use other authentication methods or fail if credentials are required.
335/// When specified, it provides the username for credential-based authentication.
336///
337/// * `password` - Optional password for authentication. If `None`, the connection
338/// may use other authentication methods (e.g., SSH keys) or fail if a password
339/// is required. When specified, it provides the password for authentication.
340///
341/// * `platform` - Optional platform identifier (e.g., "linux", "cisco_ios", "junos").
342/// This helps connection implementations apply platform-specific behavior, command
343/// syntax, or protocol variations. If `None`, the connection uses generic behavior.
344///
345/// * `extras` - Optional arbitrary JSON data for additional connection-specific
346/// configuration. This allows passing custom parameters that don't fit the standard
347/// fields, such as timeout values, retry settings, or protocol-specific options.
348///
349/// # Examples
350///
351/// ```
352/// # use genja_core::inventory::ResolvedConnectionParams;
353/// let params = ResolvedConnectionParams {
354/// hostname: "10.0.0.1".to_string(),
355/// port: Some(830),
356/// username: Some("admin".to_string()),
357/// password: Some("secret".to_string()),
358/// platform: Some("junos".to_string()),
359/// extras: None,
360/// };
361///
362/// assert_eq!(params.hostname, "10.0.0.1");
363/// assert_eq!(params.port, Some(830));
364/// ```
365#[derive(Debug, Clone, PartialEq)]
366pub struct ResolvedConnectionParams {
367 pub hostname: String,
368 pub port: Option<u16>,
369 pub username: Option<String>,
370 pub password: Option<String>,
371 pub platform: Option<String>,
372 pub extras: Option<Extras>,
373}
374
375impl DerefTarget for Extras {
376 type Target = serde_json::Value;
377}
378
379/// The DataExtra struct is a wrapper for serde_json::Value, any json data is accepted.
380#[derive(
381 Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, DerefMacro, DerefMutMacro,
382)]
383pub struct Extras(serde_json::Value);
384
385impl Extras {
386 pub fn new(value: serde_json::Value) -> Self {
387 Extras(value)
388 }
389}
390
391impl DerefTarget for ParentGroups {
392 type Target = Vec<String>;
393}
394
395/// The ParentGroups struct is a wrapped vector of strings.
396///
397/// It stores a list of strings representing the groups the host
398/// belongs to.
399///
400/// The ParentGroups struct implements Deref and DerefMut for easy
401/// access to the underlying vector.
402#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, DerefMacro, DerefMutMacro)]
403pub struct ParentGroups(pub(crate) Vec<String>);
404
405impl Default for ParentGroups {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411impl ParentGroups {
412 pub fn new() -> Self {
413 ParentGroups(Vec::new())
414 }
415}
416
417impl<'de> Deserialize<'de> for ParentGroups {
418 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419 where
420 D: Deserializer<'de>,
421 {
422 match deserializer.deserialize_seq(ParentGroupsVisitor) {
423 Ok(parent) => Ok(parent),
424 Err(err) => {
425 log::error!("{}", err);
426 let err_msg = "Groups should be an array of strings for use with `ParentGroups`";
427 log::error!("{err_msg}");
428 Err(D::Error::custom(err_msg))
429 }
430 }
431 }
432}
433
434struct ParentGroupsVisitor;
435
436impl<'de> Visitor<'de> for ParentGroupsVisitor {
437 type Value = ParentGroups;
438
439 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
440 formatter.write_str("a sequence of strings")
441 }
442 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
443 where
444 E: Error,
445 {
446 Err(Error::invalid_value(Unexpected::Str(s), &self))
447 }
448
449 /// This method is used to handle custom deserialization logic for
450 /// sequences. It returns a list of unique strings from the sequence.
451 ///
452 /// The vector implementation ensures that duplicate strings are not added to the
453 /// and preserves the order of the first occurrence of each string.
454 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
455 where
456 A: SeqAccess<'de>,
457 {
458 let mut groups = Vec::new();
459 while let Some(value) = seq.next_element()? {
460 if !groups.contains(&value) {
461 groups.push(value);
462 }
463 }
464
465 Ok(ParentGroups(groups.into_iter().collect()))
466 }
467}
468
469/// Defaults configuration for inventory.
470///
471/// Schema: same fields as `Group`, minus `groups` and `defaults`.
472/// This allows defaults to define connection details and data that apply broadly
473/// without nesting or self-references.
474#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
475pub struct Defaults {
476 pub(crate) hostname: Option<String>,
477 pub(crate) port: Option<u16>,
478 pub(crate) username: Option<String>,
479 pub(crate) password: Option<String>,
480 pub(crate) platform: Option<String>,
481 pub(crate) data: Option<Data>,
482 pub(crate) connection_options: Option<CustomTreeMap<ConnectionOptions>>,
483}
484
485impl DerefTarget for Data {
486 type Target = serde_json::Value;
487}
488
489impl Defaults {
490 pub fn builder() -> DefaultsBuilder {
491 DefaultsBuilder::new()
492 }
493
494 /// Converts this `Defaults` instance into a builder for modification.
495 ///
496 /// This method creates a new `DefaultsBuilder` initialized with all the current
497 /// values from this `Defaults` instance. This is useful when you need to create
498 /// a modified copy of existing defaults while preserving most of the original
499 /// configuration.
500 ///
501 /// # Returns
502 ///
503 /// Returns a `DefaultsBuilder` with all fields initialized to match the current
504 /// `Defaults` instance. The builder can then be used to modify specific fields
505 /// before calling `build()` to create a new `Defaults` instance.
506 ///
507 /// # Examples
508 ///
509 /// ```
510 /// # use genja_core::inventory::Defaults;
511 /// let defaults = Defaults::builder()
512 /// .username("admin")
513 /// .port(22)
514 /// .build();
515 ///
516 /// let modified = defaults.to_builder()
517 /// .port(2222)
518 /// .build();
519 ///
520 /// assert_eq!(modified.port(), Some(2222));
521 /// assert_eq!(modified.username(), Some("admin"));
522 /// ```
523 pub fn to_builder(&self) -> DefaultsBuilder {
524 let mut builder = Defaults::builder();
525 if let Some(hostname) = self.hostname.as_deref() {
526 builder = builder.hostname(hostname);
527 }
528 if let Some(port) = self.port {
529 builder = builder.port(port);
530 }
531 if let Some(username) = self.username.as_deref() {
532 builder = builder.username(username);
533 }
534 if let Some(password) = self.password.as_deref() {
535 builder = builder.password(password);
536 }
537 if let Some(platform) = self.platform.as_deref() {
538 builder = builder.platform(platform);
539 }
540 if let Some(data) = self.data.as_ref() {
541 builder = builder.data(data.clone());
542 }
543 if let Some(options_map) = self.connection_options.as_ref() {
544 for (name, options) in options_map.iter() {
545 builder = builder.connection_options(name.to_string(), options.clone());
546 }
547 }
548 builder
549 }
550
551 pub fn new() -> Self {
552 Defaults {
553 hostname: None,
554 port: None,
555 username: None,
556 password: None,
557 platform: None,
558 data: None,
559 connection_options: None,
560 }
561 }
562 /// Returns true if all fields are None or empty
563 pub fn is_empty(&self) -> bool {
564 self.hostname.is_none()
565 && self.port.is_none()
566 && self.username.is_none()
567 && self.password.is_none()
568 && self.platform.is_none()
569 && self.data.is_none()
570 && self.connection_options.is_none()
571 }
572
573 pub fn hostname(&self) -> Option<&str> {
574 self.hostname.as_deref()
575 }
576
577 pub fn port(&self) -> Option<u16> {
578 self.port
579 }
580
581 pub fn username(&self) -> Option<&str> {
582 self.username.as_deref()
583 }
584
585 pub fn password(&self) -> Option<&str> {
586 self.password.as_deref()
587 }
588
589 pub fn platform(&self) -> Option<&str> {
590 self.platform.as_deref()
591 }
592
593 pub fn data(&self) -> Option<&Data> {
594 self.data.as_ref()
595 }
596
597 pub fn connection_options(&self) -> Option<&CustomTreeMap<ConnectionOptions>> {
598 self.connection_options.as_ref()
599 }
600}
601
602/// Builder for constructing `Defaults` instances.
603///
604/// This builder provides a fluent interface for creating inventory defaults with optional
605/// configuration fields. All fields start as `None` and can be set individually using the
606/// builder methods before calling `build()` to create the final `Defaults` instance.
607///
608/// Defaults define base configuration values that apply to all hosts and groups in the
609/// inventory unless overridden at the group or host level. This allows for centralized
610/// management of common connection parameters and data.
611///
612/// Unlike `Host` and `Group`, defaults do not support `groups` membership.
613///
614/// # Fields
615///
616/// * `hostname` - Optional default hostname or IP address. Applied to hosts/groups that
617/// don't specify their own hostname.
618///
619/// * `port` - Optional default port number for connections. Applied to hosts/groups that
620/// don't specify their own port.
621///
622/// * `username` - Optional default username for authentication. Applied to hosts/groups
623/// that don't specify their own username.
624///
625/// * `password` - Optional default password for authentication. Applied to hosts/groups
626/// that don't specify their own password.
627///
628/// * `platform` - Optional default platform identifier (e.g., "linux", "cisco_ios").
629/// Applied to hosts/groups that don't specify their own platform.
630///
631/// * `data` - Optional arbitrary JSON data that applies to all hosts/groups by default.
632/// Can be overridden or merged at the group or host level.
633///
634/// * `connection_options` - Optional map of connection-specific overrides keyed by
635/// connection plugin name. Allows per-connection-plugin-name customization of default parameters.
636///
637/// # Examples
638///
639/// ```
640/// # use genja_core::inventory::Defaults;
641/// let defaults = Defaults::builder()
642/// .username("admin")
643/// .port(22)
644/// .platform("linux")
645/// .build();
646///
647/// assert_eq!(defaults.username(), Some("admin"));
648/// assert_eq!(defaults.port(), Some(22));
649/// ```
650pub struct DefaultsBuilder {
651 hostname: Option<String>,
652 port: Option<u16>,
653 username: Option<String>,
654 password: Option<String>,
655 platform: Option<String>,
656 data: Option<Data>,
657 connection_options: Option<CustomTreeMap<ConnectionOptions>>,
658}
659
660impl DefaultsBuilder {
661 pub fn new() -> Self {
662 Self {
663 hostname: None,
664 port: None,
665 username: None,
666 password: None,
667 platform: None,
668 data: None,
669 connection_options: None,
670 }
671 }
672
673 pub fn hostname<S>(mut self, hostname: S) -> Self
674 where
675 S: Into<String>,
676 {
677 self.hostname = Some(hostname.into());
678 self
679 }
680
681 pub fn port(mut self, port: u16) -> Self {
682 self.port = Some(port);
683 self
684 }
685
686 pub fn username<S>(mut self, username: S) -> Self
687 where
688 S: Into<String>,
689 {
690 self.username = Some(username.into());
691 self
692 }
693
694 pub fn password<S>(mut self, password: S) -> Self
695 where
696 S: Into<String>,
697 {
698 self.password = Some(password.into());
699 self
700 }
701
702 pub fn platform<S>(mut self, platform: S) -> Self
703 where
704 S: Into<String>,
705 {
706 self.platform = Some(platform.into());
707 self
708 }
709
710 pub fn data(mut self, data: Data) -> Self {
711 self.data = Some(data);
712 self
713 }
714
715 /// Adds or updates connection-specific options for defaults.
716 ///
717 /// # Parameters
718 ///
719 /// * `name` - A string-like value identifying the connection plugin name (e.g., "ssh", "netconf").
720 /// * `options` - A `ConnectionOptions` instance containing connection-specific configuration.
721 ///
722 /// # Returns
723 ///
724 /// Returns `Self` with the connection options updated, allowing for method chaining.
725 /// If no connection options map exists, one is created before inserting the new options.
726 pub fn connection_options<S>(mut self, name: S, options: ConnectionOptions) -> Self
727 where
728 S: Into<String>,
729 {
730 self.connection_options
731 .get_or_insert_with(CustomTreeMap::new)
732 .insert(name.into(), options);
733 self
734 }
735
736 pub fn build(self) -> Defaults {
737 Defaults {
738 hostname: self.hostname,
739 port: self.port,
740 username: self.username,
741 password: self.password,
742 platform: self.platform,
743 data: self.data,
744 connection_options: self.connection_options,
745 }
746 }
747}
748
749impl Default for DefaultsBuilder {
750 fn default() -> Self {
751 Self::new()
752 }
753}
754
755impl Default for Defaults {
756 fn default() -> Self {
757 Self::new()
758 }
759}
760/// The Data struct is a wrapper for serde_json::Value, any json data is accepted.
761#[derive(
762 Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, DerefMacro, DerefMutMacro,
763)]
764pub struct Data(pub(crate) serde_json::Value);
765
766impl Data {
767 pub fn new(data: serde_json::Value) -> Self {
768 Data(data)
769 }
770}
771
772/// Represents a single host in the inventory with connection parameters and metadata.
773///
774/// A `Host` defines the configuration for connecting to and managing a single network device
775/// or server. It contains optional connection parameters (hostname, port, credentials, platform),
776/// group membership information, arbitrary data, and connection-specific overrides.
777///
778/// Hosts are the fundamental unit of the inventory system. They can inherit configuration from
779/// groups and defaults through the inventory hierarchy, with host-level settings taking highest
780/// precedence during parameter resolution.
781///
782/// # Fields
783///
784/// * `hostname` - Optional hostname or IP address for the host. This is the primary identifier
785/// used for network connections. If not specified, it may be inherited from groups or defaults.
786///
787/// * `port` - Optional port number for connections. If not specified, defaults may be applied
788/// during connection parameter resolution or connection implementations may use their default ports.
789///
790/// * `username` - Optional username for authentication. Used for establishing connections to
791/// the host. Can be inherited from groups or defaults if not specified.
792///
793/// * `password` - Optional password for authentication. Used in conjunction with username for
794/// connection authentication. Can be inherited from groups or defaults if not specified.
795///
796/// * `platform` - Optional platform identifier (e.g., "linux", "cisco_ios", "junos"). Used to
797/// determine platform-specific behavior and connection handling. Can be inherited from groups
798/// or defaults if not specified.
799///
800/// * `groups` - Optional parent group names that this host belongs to. Groups provide inherited
801/// configuration through the inventory hierarchy. Multiple groups can be specified, and their
802/// configurations are merged in order.
803///
804/// * `data` - Optional arbitrary JSON data associated with the host. Allows storing custom
805/// metadata and configuration that doesn't fit standard fields. Can be merged with group
806/// and default data during resolution.
807///
808/// * `connection_options` - Optional map of connection-specific overrides keyed by connection
809/// type (e.g., "ssh", "netconf", "http"). Allows per-connection-plugin-name customization of
810/// connection parameters, overriding base host settings for specific connection plugin names.
811///
812/// # Deserialization
813///
814/// - Unknown fields are rejected via `#[serde(deny_unknown_fields)]` to catch configuration errors
815/// - All fields are optional, allowing minimal host definitions
816/// - Connection options accept arbitrary map keys for different connection plugin names
817///
818/// # Examples
819///
820/// ```
821/// # use genja_core::inventory::{Host, BaseBuilderHost};
822/// let host = Host::builder()
823/// .hostname("10.0.0.1")
824/// .port(22)
825/// .username("admin")
826/// .platform("linux")
827/// .build();
828///
829/// assert_eq!(host.hostname(), Some("10.0.0.1"));
830/// assert_eq!(host.port(), Some(22));
831/// ```
832#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
833#[serde(deny_unknown_fields)]
834pub struct Host {
835 pub(crate) hostname: Option<String>,
836 pub(crate) port: Option<u16>,
837 pub(crate) username: Option<String>,
838 pub(crate) password: Option<String>,
839 pub(crate) platform: Option<String>,
840 pub(crate) groups: Option<ParentGroups>,
841 pub(crate) data: Option<Data>,
842 pub(crate) connection_options: Option<CustomTreeMap<ConnectionOptions>>,
843}
844impl Host {
845 pub fn new() -> Host {
846 Host {
847 hostname: None,
848 port: None,
849 username: None,
850 password: None,
851 platform: None,
852 groups: None,
853 data: None,
854 connection_options: None,
855 }
856 }
857 pub fn builder() -> HostBuilder {
858 HostBuilder::new()
859 }
860
861 /// Converts this `Host` instance into a builder for modification.
862 ///
863 /// This method creates a new `HostBuilder` initialized with all the current
864 /// values from this `Host` instance. This is useful when you need to create
865 /// a modified copy of an existing host while preserving most of the original
866 /// configuration.
867 ///
868 /// # Returns
869 ///
870 /// Returns a `HostBuilder` with all fields initialized to match the current
871 /// `Host` instance. The builder can then be used to modify specific fields
872 /// before calling `build()` to create a new `Host` instance.
873 ///
874 /// # Examples
875 ///
876 /// ```
877 /// # use genja_core::inventory::{Host, BaseBuilderHost};
878 /// let host = Host::builder()
879 /// .hostname("10.0.0.1")
880 /// .port(22)
881 /// .username("admin")
882 /// .build();
883 ///
884 /// let modified = host.to_builder()
885 /// .port(2222)
886 /// .build();
887 ///
888 /// assert_eq!(modified.hostname(), Some("10.0.0.1"));
889 /// assert_eq!(modified.port(), Some(2222));
890 /// assert_eq!(modified.username(), Some("admin"));
891 /// ```
892 pub fn to_builder(&self) -> HostBuilder {
893 let mut builder = Host::builder();
894 if let Some(hostname) = self.hostname() {
895 builder = builder.hostname(hostname);
896 }
897 if let Some(port) = self.port() {
898 builder = builder.port(port);
899 }
900 if let Some(username) = self.username() {
901 builder = builder.username(username);
902 }
903 if let Some(password) = self.password() {
904 builder = builder.password(password);
905 }
906 if let Some(platform) = self.platform() {
907 builder = builder.platform(platform);
908 }
909 if let Some(groups) = self.groups() {
910 builder = builder.groups(groups.clone());
911 }
912 if let Some(data) = self.data() {
913 builder = builder.data(data.clone());
914 }
915 if let Some(options_map) = self.connection_options() {
916 for (name, options) in options_map.iter() {
917 builder = builder.connection_options(name.to_string(), options.clone());
918 }
919 }
920 builder
921 }
922
923 pub fn hostname(&self) -> Option<&str> {
924 self.hostname.as_deref()
925 }
926
927 pub fn port(&self) -> Option<u16> {
928 self.port
929 }
930
931 pub fn username(&self) -> Option<&str> {
932 self.username.as_deref()
933 }
934
935 pub fn password(&self) -> Option<&str> {
936 self.password.as_deref()
937 }
938
939 pub fn platform(&self) -> Option<&str> {
940 self.platform.as_deref()
941 }
942
943 pub fn groups(&self) -> Option<&ParentGroups> {
944 self.groups.as_ref()
945 }
946
947 pub fn data(&self) -> Option<&Data> {
948 self.data.as_ref()
949 }
950
951 pub fn connection_options(&self) -> Option<&CustomTreeMap<ConnectionOptions>> {
952 self.connection_options.as_ref()
953 }
954
955 /// Resolves connection parameters for a specific connection plugin name by merging host-level
956 /// settings with connection-specific overrides.
957 ///
958 /// This method uses only the fields on this `Host`. It does not apply defaults or group
959 /// inheritance. To include those, use `Inventory::resolve_connection_params` (see the second
960 /// example below).
961 ///
962 /// This method creates a complete set of connection parameters by starting with the host's
963 /// base connection fields (hostname, port, username, password, platform) and then applying
964 /// any connection-specific overrides from the `connection_options` map. Connection-specific
965 /// options take precedence over base host fields.
966 ///
967 /// # Parameters
968 ///
969 /// * `connection_type` - A string identifying the connection plugin name to resolve parameters for
970 /// (e.g., "ssh", "netconf", "http"). This is used as the key to lookup connection-specific
971 /// options in the host's `connection_options` map.
972 ///
973 /// # Returns
974 ///
975 /// Returns a `ResolvedConnectionParams` struct containing the fully resolved connection
976 /// parameters. If the host has connection-specific options for the given `connection_type`,
977 /// those values override the corresponding base host fields. Fields not specified in either
978 /// location will be `None` (except hostname, which defaults to an empty string if not set).
979 ///
980 /// # Examples
981 ///
982 /// ```
983 /// # use genja_core::inventory::{Host, ConnectionOptions, BaseBuilderHost};
984 /// let options = ConnectionOptions::builder().port(830).build();
985 ///
986 /// let host = Host::builder()
987 /// .hostname("10.0.0.1")
988 /// .port(22)
989 /// .connection_options("netconf", options)
990 /// .build();
991 ///
992 /// let params = host.resolve_connection_params("netconf");
993 /// assert_eq!(params.hostname, "10.0.0.1");
994 /// assert_eq!(params.port, Some(830)); // Connection-specific port overrides base port
995 /// ```
996 ///
997 /// The following example shows how to resolve parameters through `Inventory`,
998 /// which applies defaults and group inheritance before connection-specific overrides.
999 ///
1000 /// ```
1001 /// # use genja_core::inventory::{Host, Hosts, Inventory, ConnectionOptions, BaseBuilderHost};
1002 /// let mut hosts = Hosts::new();
1003 /// let options = ConnectionOptions::builder().port(830).build();
1004 /// let host = Host::builder()
1005 /// .hostname("10.0.0.1")
1006 /// .port(22)
1007 /// .connection_options("netconf", options)
1008 /// .build();
1009 /// hosts.add_host("router1", host);
1010 /// let inventory = Inventory::builder().hosts(hosts).build();
1011 ///
1012 /// let params = inventory
1013 /// .resolve_connection_params("router1", "netconf")
1014 /// .expect("resolved params");
1015 /// assert_eq!(params.port, Some(830));
1016 /// ```
1017 pub fn resolve_connection_params(&self, connection_type: &str) -> ResolvedConnectionParams {
1018 let mut resolved = ResolvedConnectionParams {
1019 hostname: self.hostname.clone().unwrap_or_default(),
1020 port: self.port,
1021 username: self.username.clone(),
1022 password: self.password.clone(),
1023 platform: self.platform.clone(),
1024 extras: None,
1025 };
1026
1027 if let Some(options_map) = &self.connection_options
1028 && let Some(options) = options_map.get(connection_type)
1029 {
1030 if let Some(hostname) = options.hostname.clone() {
1031 resolved.hostname = hostname;
1032 }
1033 if options.port.is_some() {
1034 resolved.port = options.port;
1035 }
1036 if options.username.is_some() {
1037 resolved.username = options.username.clone();
1038 }
1039 if options.password.is_some() {
1040 resolved.password = options.password.clone();
1041 }
1042 if options.platform.is_some() {
1043 resolved.platform = options.platform.clone();
1044 }
1045 if options.extras.is_some() {
1046 resolved.extras = options.extras.clone();
1047 }
1048 }
1049
1050 resolved
1051 }
1052}
1053
1054impl Default for Host {
1055 fn default() -> Self {
1056 Self::new()
1057 }
1058}
1059
1060impl BaseMethods for Host {}
1061
1062/// Builder for constructing `Host` instances.
1063///
1064/// This builder provides a fluent interface for creating hosts with optional configuration
1065/// fields. All fields start as `None` and can be set individually using the builder methods
1066/// before calling `build()` to create the final `Host` instance.
1067///
1068/// The builder implements the `BaseBuilderHost` trait, which provides standard methods for
1069/// setting connection parameters, group membership, and custom data. This allows for a
1070/// consistent interface across different inventory entity builders.
1071///
1072/// # Fields
1073///
1074/// * `hostname` - Optional hostname or IP address for the host. This is the primary identifier
1075/// used for network connections.
1076///
1077/// * `port` - Optional port number for connections. If not specified, defaults may be applied
1078/// during connection parameter resolution.
1079///
1080/// * `username` - Optional username for authentication. Used for establishing connections to
1081/// the host.
1082///
1083/// * `password` - Optional password for authentication. Used in conjunction with username for
1084/// connection authentication.
1085///
1086/// * `platform` - Optional platform identifier (e.g., "linux", "cisco_ios"). Used to determine
1087/// platform-specific behavior and connection handling.
1088///
1089/// * `groups` - Optional parent group names that this host belongs to. Groups provide inherited
1090/// configuration through the inventory hierarchy.
1091///
1092/// * `data` - Optional arbitrary JSON data associated with the host. Allows storing custom
1093/// metadata and configuration that doesn't fit standard fields.
1094///
1095/// * `connection_options` - Optional map of connection-specific overrides keyed by connection
1096/// type. Allows per-connection-plugin-name customization of connection parameters.
1097///
1098/// # Examples
1099///
1100/// ```
1101/// # use genja_core::inventory::{Host, BaseBuilderHost};
1102/// let host = Host::builder()
1103/// .hostname("10.0.0.1")
1104/// .port(22)
1105/// .username("admin")
1106/// .platform("linux")
1107/// .build();
1108///
1109/// assert_eq!(host.hostname(), Some("10.0.0.1"));
1110/// assert_eq!(host.port(), Some(22));
1111/// ```
1112pub struct HostBuilder {
1113 hostname: Option<String>,
1114 port: Option<u16>,
1115 username: Option<String>,
1116 password: Option<String>,
1117 platform: Option<String>,
1118 groups: Option<ParentGroups>,
1119 data: Option<Data>,
1120 connection_options: Option<CustomTreeMap<ConnectionOptions>>,
1121}
1122
1123impl HostBuilder {
1124 pub fn new() -> Self {
1125 HostBuilder {
1126 hostname: None,
1127 port: None,
1128 username: None,
1129 password: None,
1130 platform: None,
1131 groups: None,
1132 data: None,
1133 connection_options: None,
1134 }
1135 }
1136}
1137
1138impl Default for HostBuilder {
1139 fn default() -> Self {
1140 Self::new()
1141 }
1142}
1143
1144impl BaseBuilderHost for HostBuilder {
1145 type Output = Host;
1146
1147 fn hostname<S>(mut self, hostname: S) -> Self
1148 where
1149 S: Into<String>,
1150 {
1151 self.hostname = Some(hostname.into());
1152 self
1153 }
1154
1155 fn port(mut self, port: u16) -> Self {
1156 self.port = Some(port);
1157 self
1158 }
1159
1160 fn username<S>(mut self, username: S) -> Self
1161 where
1162 S: Into<String>,
1163 {
1164 self.username = Some(username.into());
1165 self
1166 }
1167
1168 fn password<S>(mut self, password: S) -> Self
1169 where
1170 S: Into<String>,
1171 {
1172 self.password = Some(password.into());
1173 self
1174 }
1175
1176 fn platform<S>(mut self, platform: S) -> Self
1177 where
1178 S: Into<String>,
1179 {
1180 self.platform = Some(platform.into());
1181 self
1182 }
1183
1184 fn groups(mut self, groups: ParentGroups) -> Self {
1185 self.groups = Some(groups);
1186 self
1187 }
1188
1189 fn data(mut self, data: Data) -> Self {
1190 self.data = Some(data);
1191 self
1192 }
1193
1194 fn connection_options<S>(mut self, name: S, options: ConnectionOptions) -> Self
1195 where
1196 S: Into<String>,
1197 {
1198 self.connection_options
1199 .get_or_insert_with(CustomTreeMap::new)
1200 .insert(name.into(), options);
1201 self
1202 }
1203
1204 fn build(self) -> Host {
1205 Host {
1206 hostname: self.hostname,
1207 port: self.port,
1208 username: self.username,
1209 password: self.password,
1210 platform: self.platform,
1211 groups: self.groups,
1212 data: self.data,
1213 connection_options: self.connection_options,
1214 }
1215 }
1216}
1217
1218/// Group-level inventory entry that applies values to member hosts.
1219///
1220/// # Fields
1221///
1222/// Group fields mirror host fields and are merged during resolution.
1223/// Groups are stored in the `Groups` collection keyed by name. Use
1224/// `Groups::add_group(name, group)` to add a group entry under a name.
1225///
1226/// * `hostname` - Optional hostname or address applied to member hosts.
1227/// * `port` - Optional connection port applied to member hosts.
1228/// * `username` - Optional username applied to member hosts.
1229/// * `password` - Optional password applied to member hosts.
1230/// * `platform` - Optional platform identifier applied to member hosts.
1231/// * `groups` - Optional parent group names for group inheritance.
1232/// * `data` - Optional arbitrary data merged into member hosts.
1233/// * `connection_options` - Optional per-connection overrides.
1234/// * Defaults are applied globally via `Inventory`.
1235///
1236/// # Deserialization
1237///
1238/// - Unknown fields are rejected (via `#[serde(deny_unknown_fields)]`).
1239/// - Connection options accept arbitrary map keys.
1240///
1241/// # Examples
1242///
1243/// ```
1244/// use genja_core::inventory::{Group, Groups, BaseBuilderHost};
1245///
1246/// let mut groups = Groups::new();
1247/// let core_group = Group::builder()
1248/// .platform("linux")
1249/// .build();
1250///
1251/// groups.add_group("core", core_group);
1252/// assert_eq!(groups.len(), 1);
1253/// ```
1254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1255#[serde(deny_unknown_fields)]
1256pub struct Group {
1257 pub(crate) hostname: Option<String>,
1258 pub(crate) port: Option<u16>,
1259 pub(crate) username: Option<String>,
1260 pub(crate) password: Option<String>,
1261 pub(crate) platform: Option<String>,
1262 pub(crate) groups: Option<ParentGroups>,
1263 pub(crate) data: Option<Data>,
1264 pub(crate) connection_options: Option<CustomTreeMap<ConnectionOptions>>,
1265}
1266
1267impl Group {
1268 /// Returns a builder for creating group entries.
1269 ///
1270 /// Use the builder to set optional fields before calling `build()`.
1271 pub fn builder() -> GroupBuilder {
1272 GroupBuilder::new()
1273 }
1274
1275 pub fn to_builder(&self) -> GroupBuilder {
1276 let mut builder = Group::builder();
1277 if let Some(hostname) = self.hostname() {
1278 builder = builder.hostname(hostname);
1279 }
1280 if let Some(port) = self.port() {
1281 builder = builder.port(port);
1282 }
1283 if let Some(username) = self.username() {
1284 builder = builder.username(username);
1285 }
1286 if let Some(password) = self.password() {
1287 builder = builder.password(password);
1288 }
1289 if let Some(platform) = self.platform() {
1290 builder = builder.platform(platform);
1291 }
1292 if let Some(groups) = self.groups() {
1293 builder = builder.groups(groups.clone());
1294 }
1295 if let Some(data) = self.data() {
1296 builder = builder.data(data.clone());
1297 }
1298 if let Some(options_map) = self.connection_options() {
1299 for (name, options) in options_map.iter() {
1300 builder = builder.connection_options(name.to_string(), options.clone());
1301 }
1302 }
1303 builder
1304 }
1305
1306 pub fn hostname(&self) -> Option<&str> {
1307 self.hostname.as_deref()
1308 }
1309
1310 pub fn port(&self) -> Option<u16> {
1311 self.port
1312 }
1313
1314 pub fn username(&self) -> Option<&str> {
1315 self.username.as_deref()
1316 }
1317
1318 pub fn password(&self) -> Option<&str> {
1319 self.password.as_deref()
1320 }
1321
1322 pub fn platform(&self) -> Option<&str> {
1323 self.platform.as_deref()
1324 }
1325
1326 pub fn groups(&self) -> Option<&ParentGroups> {
1327 self.groups.as_ref()
1328 }
1329
1330 pub fn data(&self) -> Option<&Data> {
1331 self.data.as_ref()
1332 }
1333
1334 pub fn connection_options(&self) -> Option<&CustomTreeMap<ConnectionOptions>> {
1335 self.connection_options.as_ref()
1336 }
1337}
1338
1339/// Builder for constructing `Group` entries.
1340///
1341/// Use the `BaseBuilderHost` methods to populate optional fields, then call `build()`.
1342pub struct GroupBuilder {
1343 hostname: Option<String>,
1344 port: Option<u16>,
1345 username: Option<String>,
1346 password: Option<String>,
1347 platform: Option<String>,
1348 groups: Option<ParentGroups>,
1349 data: Option<Data>,
1350 connection_options: Option<CustomTreeMap<ConnectionOptions>>,
1351}
1352
1353impl BaseBuilderHost for GroupBuilder {
1354 type Output = Group;
1355
1356 /// Sets the hostname for the group.
1357 ///
1358 /// # Parameters
1359 ///
1360 /// * `hostname` - A string-like value containing the hostname or IP address to assign to the group.
1361 ///
1362 /// # Returns
1363 ///
1364 /// Returns `Self` with the hostname field updated, allowing for method chaining.
1365 fn hostname<S>(mut self, hostname: S) -> Self
1366 where
1367 S: Into<String>,
1368 {
1369 self.hostname = Some(hostname.into());
1370 self
1371 }
1372
1373 /// Sets the connection port for the group.
1374 ///
1375 /// # Parameters
1376 ///
1377 /// * `port` - A 16-bit unsigned integer representing the port number to use for connections.
1378 ///
1379 /// # Returns
1380 ///
1381 /// Returns `Self` with the port field updated, allowing for method chaining.
1382 fn port(mut self, port: u16) -> Self {
1383 self.port = Some(port);
1384 self
1385 }
1386
1387 /// Sets the username for authentication.
1388 ///
1389 /// # Parameters
1390 ///
1391 /// * `username` - A string-like value containing the username to use for authentication.
1392 ///
1393 /// # Returns
1394 ///
1395 /// Returns `Self` with the username field updated, allowing for method chaining.
1396 fn username<S>(mut self, username: S) -> Self
1397 where
1398 S: Into<String>,
1399 {
1400 self.username = Some(username.into());
1401 self
1402 }
1403
1404 /// Sets the password for authentication.
1405 ///
1406 /// # Parameters
1407 ///
1408 /// * `password` - A string-like value containing the password to use for authentication.
1409 ///
1410 /// # Returns
1411 ///
1412 /// Returns `Self` with the password field updated, allowing for method chaining.
1413 fn password<S>(mut self, password: S) -> Self
1414 where
1415 S: Into<String>,
1416 {
1417 self.password = Some(password.into());
1418 self
1419 }
1420
1421 /// Sets the platform identifier for the group.
1422 ///
1423 /// # Parameters
1424 ///
1425 /// * `platform` - A string-like value identifying the platform type (e.g., "linux", "windows", "cisco_ios").
1426 ///
1427 /// # Returns
1428 ///
1429 /// Returns `Self` with the platform field updated, allowing for method chaining.
1430 fn platform<S>(mut self, platform: S) -> Self
1431 where
1432 S: Into<String>,
1433 {
1434 self.platform = Some(platform.into());
1435 self
1436 }
1437
1438 /// Sets the parent groups for this group.
1439 ///
1440 /// # Parameters
1441 ///
1442 /// * `groups` - A `ParentGroups` instance containing the names of parent groups this group belongs to.
1443 ///
1444 /// # Returns
1445 ///
1446 /// Returns `Self` with the groups field updated, allowing for method chaining.
1447 fn groups(mut self, groups: ParentGroups) -> Self {
1448 self.groups = Some(groups);
1449 self
1450 }
1451
1452 /// Sets arbitrary data for the group.
1453 ///
1454 /// # Parameters
1455 ///
1456 /// * `data` - A `Data` instance containing arbitrary JSON data to associate with the group.
1457 ///
1458 /// # Returns
1459 ///
1460 /// Returns `Self` with the data field updated, allowing for method chaining.
1461 fn data(mut self, data: Data) -> Self {
1462 self.data = Some(data);
1463 self
1464 }
1465
1466 /// Adds or updates connection-specific options for the group.
1467 ///
1468 /// # Parameters
1469 ///
1470 /// * `name` - A string-like value identifying the connection plugin name (e.g., "ssh", "netconf").
1471 /// * `options` - A `ConnectionOptions` instance containing connection-specific configuration.
1472 ///
1473 /// # Returns
1474 ///
1475 /// Returns `Self` with the connection options updated, allowing for method chaining.
1476 /// If no connection options map exists, one is created before inserting the new options.
1477 fn connection_options<S>(mut self, name: S, options: ConnectionOptions) -> Self
1478 where
1479 S: Into<String>,
1480 {
1481 self.connection_options
1482 .get_or_insert_with(CustomTreeMap::new)
1483 .insert(name.into(), options);
1484 self
1485 }
1486
1487 fn build(self) -> Group {
1488 Group {
1489 hostname: self.hostname,
1490 port: self.port,
1491 username: self.username,
1492 password: self.password,
1493 platform: self.platform,
1494 groups: self.groups,
1495 data: self.data,
1496 connection_options: self.connection_options,
1497 }
1498 }
1499}
1500
1501impl GroupBuilder {
1502 pub fn new() -> Self {
1503 GroupBuilder {
1504 hostname: None,
1505 port: None,
1506 username: None,
1507 password: None,
1508 platform: None,
1509 groups: None,
1510 data: None,
1511 connection_options: None,
1512 }
1513 }
1514}
1515
1516impl Default for GroupBuilder {
1517 fn default() -> Self {
1518 Self::new()
1519 }
1520}
1521
1522/// Internal storage type for `Hosts` (maps host name -> `Host`).
1523pub type HostsTarget = CustomTreeMap<Host>;
1524
1525impl DerefTarget for Hosts {
1526 type Target = CustomTreeMap<Host>;
1527}
1528
1529/// Collection of hosts keyed by name.
1530///
1531/// This type wraps a `CustomTreeMap<Host>` and is the primary container used
1532/// for host inventory data. The map keys are host names used for logging/output.
1533///
1534/// # Deserialization
1535///
1536/// - Unknown fields in individual `Host` entries are rejected (via `#[serde(deny_unknown_fields)]` on `Host`)
1537/// - The `Hosts` wrapper itself accepts any valid map structure
1538///
1539/// # Examples
1540///
1541/// ```
1542/// use genja_core::inventory::{Host, Hosts, BaseBuilderHost};
1543///
1544/// let mut hosts = Hosts::new();
1545/// let host = Host::builder().hostname("10.0.0.1").build();
1546/// hosts.add_host("router1", host);
1547/// assert_eq!(hosts.len(), 1);
1548/// ```
1549#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, DerefMacro, DerefMutMacro)]
1550pub struct Hosts(pub(crate) HostsTarget);
1551
1552impl Default for Hosts {
1553 fn default() -> Self {
1554 Self::new()
1555 }
1556}
1557
1558impl Hosts {
1559 /// Creates an empty host collection.
1560 ///
1561 /// Use `add_host` or map insertion methods to populate it.
1562 pub fn new() -> Self {
1563 Hosts(CustomTreeMap::new())
1564 }
1565
1566 /// Inserts a host into the collection under the provided name.
1567 ///
1568 /// If a host with the same name already exists, it will be replaced with the new host.
1569 /// The name serves as the unique identifier for the host and is used in logs and output.
1570 ///
1571 /// # Parameters
1572 ///
1573 /// * `name` - A string-like value that will be used as the unique identifier for the host.
1574 /// This name is used in logs and output to reference the host.
1575 /// * `host` - The `Host` instance to insert into the collection.
1576 ///
1577 /// # Examples
1578 ///
1579 /// ```
1580 /// use genja_core::inventory::{Host, Hosts, BaseBuilderHost};
1581 ///
1582 /// let mut hosts = Hosts::new();
1583 /// let host = Host::builder().hostname("10.0.0.1").build();
1584 /// hosts.add_host("router1", host);
1585 /// assert_eq!(hosts.len(), 1);
1586 /// ```
1587 pub fn add_host<N>(&mut self, name: N, host: Host)
1588 where
1589 N: Into<String>,
1590 {
1591 self.insert(name.into(), host);
1592 }
1593}
1594
1595impl BaseMethods for Hosts {}
1596
1597/// Collection of groups keyed by name.
1598///
1599/// This type wraps a `CustomTreeMap<Group>` and is the primary container used
1600/// for group inventory data. The map keys are group names.
1601///
1602/// # Deserialization
1603///
1604/// - Unknown fields in individual `Group` entries are rejected (via `#[serde(deny_unknown_fields)]` on `Group`)
1605/// - The `Groups` wrapper itself accepts any valid map structure
1606///
1607/// # Examples
1608///
1609/// ```
1610/// use genja_core::inventory::{Group, Groups, BaseBuilderHost};
1611///
1612/// let mut groups = Groups::new();
1613/// let core_group = Group::builder().platform("linux").build();
1614/// groups.add_group("core", core_group);
1615/// assert_eq!(groups.len(), 1);
1616/// ```
1617#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, DerefMacro, DerefMutMacro)]
1618pub struct Groups(CustomTreeMap<Group>);
1619
1620impl DerefTarget for Groups {
1621 type Target = CustomTreeMap<Group>;
1622}
1623
1624impl Groups {
1625 /// Creates an empty group collection.
1626 ///
1627 /// Use `add_group` or map insertion methods to populate it.
1628 pub fn new() -> Self {
1629 Groups(CustomTreeMap::new())
1630 }
1631
1632 /// Inserts a group into the collection under the provided name.
1633 ///
1634 /// If a group with the same name already exists, it will be replaced.
1635 pub fn add_group<N>(&mut self, name: N, group: Group)
1636 where
1637 N: Into<String>,
1638 {
1639 self.insert(name.into(), group);
1640 }
1641}
1642
1643impl Default for Groups {
1644 fn default() -> Self {
1645 Self::new()
1646 }
1647}