Skip to main content

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}