Skip to main content

genja_core/inventory/
runtime.rs

1use super::{
2    BaseMethods, ConnectionManager, ConnectionOptions, Data, Defaults, Group, Groups, Host, Hosts,
3    ResolvedConnectionParams, TransformFunction, TransformFunctionOptions,
4};
5use crate::{CustomTreeMap, NatString, State};
6use dashmap::DashMap;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11/// In-memory inventory container.
12///
13/// Aggregates hosts, groups, defaults, and optional transform settings.
14/// This struct is deserializable and is the primary shape used by the
15/// inventory loader and runtime.
16///
17/// Transforms are applied lazily when accessing hosts, groups, or defaults
18/// via the view accessors (e.g., `hosts()`).
19///
20/// # Deserialization
21///
22/// - Missing fields use their default values (see `Default` impl)
23/// - Unknown fields are rejected for nested host/group items (see `Hosts` and `Groups`)
24///
25/// # Examples
26///
27/// ```
28/// use genja_core::inventory::{Inventory, Hosts, Host};
29/// use genja_core::inventory::BaseBuilderHost;
30///
31/// let mut hosts = Hosts::new();
32/// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
33///
34/// let inventory = Inventory::builder().hosts(hosts).build();
35/// assert_eq!(inventory.hosts().len(), 1);
36/// ```
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
38pub struct Inventory {
39    pub(crate) hosts: Hosts,
40    pub(crate) groups: Option<Groups>,
41    pub(crate) defaults: Option<Defaults>,
42    #[serde(skip)]
43    transform_function: Option<TransformFunction>,
44    transform_function_options: Option<TransformFunctionOptions>,
45    #[serde(skip)]
46    #[schemars(skip)]
47    connections: Arc<ConnectionManager>,
48    #[serde(skip)]
49    #[schemars(skip)]
50    host_cache: DashMap<NatString, Host>,
51    #[serde(skip)]
52    #[schemars(skip)]
53    group_cache: DashMap<NatString, Group>,
54    #[serde(skip)]
55    #[schemars(skip)]
56    resolved_host_cache: DashMap<NatString, Host>,
57    #[serde(skip)]
58    #[schemars(skip)]
59    resolved_params_cache: DashMap<(NatString, String), ResolvedConnectionParams>,
60    #[serde(skip)]
61    #[schemars(skip)]
62    state: Arc<State>,
63}
64
65impl BaseMethods for Inventory {}
66
67impl Inventory {
68    /// Creates a new builder for constructing an `Inventory` instance.
69    ///
70    /// This method provides a fluent interface for building an `Inventory` with custom
71    /// configuration. The builder allows you to set optional hosts, groups, defaults,
72    /// transform functions, and connection managers before calling `build()` to create
73    /// the final inventory.
74    ///
75    /// # Returns
76    ///
77    /// Returns a new `InventoryBuilder` instance with all fields initialized to `None`.
78    /// Use the builder's methods to configure the inventory before calling `build()`.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// # use genja_core::inventory::{Inventory, Hosts, Host, BaseBuilderHost};
84    /// let mut hosts = Hosts::new();
85    /// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
86    ///
87    /// let inventory = Inventory::builder()
88    ///     .hosts(hosts)
89    ///     .build();
90    ///
91    /// assert_eq!(inventory.hosts().len(), 1);
92    /// ```
93    pub fn builder() -> InventoryBuilder {
94        InventoryBuilder::new()
95    }
96
97    /// Returns a view of the inventory's hosts collection with transform functions applied.
98    ///
99    /// This method provides access to the inventory's hosts through a `HostsView` wrapper
100    /// that applies any configured transform function when accessing individual hosts.
101    /// The view provides read-only access to the hosts and caches transformed results
102    /// for improved performance on subsequent accesses.
103    ///
104    /// Only hosts that are currently in scope are visible through this view. Hosts marked
105    /// out of scope in the runtime [`State`] are filtered out of `len()`, `keys()`, `get()`,
106    /// and `iter()`.
107    ///
108    /// # Returns
109    ///
110    /// Returns a `HostsView` containing a view of the in-scope hosts collection. The view
111    /// allows iteration over hosts and lookup by name, with transforms applied lazily on access.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # use genja_core::inventory::{Inventory, Hosts, Host, BaseBuilderHost};
117    /// let mut hosts = Hosts::new();
118    /// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
119    ///
120    /// let inventory = Inventory::builder()
121    ///     .hosts(hosts)
122    ///     .build();
123    ///
124    /// let hosts_view = inventory.hosts();
125    /// assert_eq!(hosts_view.len(), 1);
126    /// if let Some(host) = hosts_view.get("router1") {
127    ///     assert_eq!(host.hostname(), Some("10.0.0.1"));
128    /// }
129    /// ```
130    pub fn hosts(&self) -> HostsView<'_> {
131        HostsView { inventory: self }
132    }
133
134    /// Returns the global runtime state for the current Genja instance.
135    pub fn state(&self) -> &State {
136        self.state.as_ref()
137    }
138
139    /// Returns a reference to the raw hosts collection without applying transforms.
140    ///
141    /// This accessor provides direct, read-only access to the underlying `Hosts`
142    /// data stored in the inventory. No transform function is applied, and no
143    /// cache is populated. This is useful for debugging, inspection, or when you
144    /// explicitly need the original, unmodified host data.
145    ///
146    /// # Returns
147    ///
148    /// Returns a reference to the raw `Hosts` collection.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// # use genja_core::inventory::{Inventory, Hosts, Host, BaseBuilderHost};
154    /// let mut hosts = Hosts::new();
155    /// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
156    ///
157    /// let inventory = Inventory::builder()
158    ///     .hosts(hosts)
159    ///     .build();
160    ///
161    /// let raw_hosts = inventory.hosts_raw();
162    /// assert_eq!(raw_hosts.len(), 1);
163    /// ```
164    pub fn hosts_raw(&self) -> &Hosts {
165        &self.hosts
166    }
167
168    /// Returns a view of the inventory's groups collection with transform functions applied.
169    ///
170    /// This method provides access to the inventory's groups through a `GroupsView` wrapper
171    /// that applies any configured transform function when accessing individual groups.
172    /// The view provides read-only access to the groups and caches transformed results
173    /// for improved performance on subsequent accesses.
174    ///
175    /// # Returns
176    ///
177    /// Returns `Some(GroupsView)` containing a view of the groups collection if groups
178    /// are configured in the inventory. Returns `None` if no groups are present.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// # use genja_core::inventory::{Inventory, Groups, Group, BaseBuilderHost};
184    /// let mut groups = Groups::new();
185    /// groups.add_group("core", Group::builder().platform("linux").build());
186    ///
187    /// let inventory = Inventory::builder()
188    ///     .groups(groups)
189    ///     .build();
190    ///
191    /// if let Some(groups_view) = inventory.groups() {
192    ///     assert_eq!(groups_view.len(), 1);
193    ///     if let Some(group) = groups_view.get("core") {
194    ///         assert_eq!(group.platform(), Some("linux"));
195    ///     }
196    /// }
197    /// ```
198    pub fn groups(&self) -> Option<GroupsView<'_>> {
199        self.groups.as_ref().map(|groups| GroupsView {
200            inventory: self,
201            groups,
202        })
203    }
204
205    /// Returns a reference to the raw groups collection without applying transforms.
206    ///
207    /// This accessor provides direct, read-only access to the underlying `Groups`
208    /// data stored in the inventory. No transform function is applied, and no
209    /// cache is populated. This is useful for debugging, inspection, or when you
210    /// explicitly need the original, unmodified group data.
211    ///
212    /// # Returns
213    ///
214    /// Returns `Some(&Groups)` if groups are configured in the inventory, or `None`
215    /// if no groups are present.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// # use genja_core::inventory::{Inventory, Groups, Group, BaseBuilderHost};
221    /// let mut groups = Groups::new();
222    /// groups.add_group("core", Group::builder().platform("linux").build());
223    ///
224    /// let inventory = Inventory::builder()
225    ///     .groups(groups)
226    ///     .build();
227    ///
228    /// let raw_groups = inventory.groups_raw().expect("groups exist");
229    /// assert_eq!(raw_groups.len(), 1);
230    /// ```
231    pub fn groups_raw(&self) -> Option<&Groups> {
232        self.groups.as_ref()
233    }
234
235    /// Returns the inventory's default configuration after applying any configured transform function.
236    ///
237    /// This method provides access to the inventory-wide defaults that apply to all hosts and groups.
238    /// If a transform function is configured on the inventory, it will be applied to the defaults
239    /// before returning them. The transform allows for dynamic modification of default values based
240    /// on custom logic or external configuration.
241    ///
242    /// # Returns
243    ///
244    /// Returns `Some(Defaults)` containing the default configuration (potentially transformed) if
245    /// defaults are configured in the inventory. Returns `None` if no defaults are set.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// # use genja_core::inventory::{Inventory, Defaults};
251    /// let defaults = Defaults::builder()
252    ///     .username("admin")
253    ///     .port(22)
254    ///     .build();
255    ///
256    /// let inventory = Inventory::builder()
257    ///     .defaults(defaults)
258    ///     .build();
259    ///
260    /// if let Some(defaults) = inventory.defaults() {
261    ///     assert_eq!(defaults.username(), Some("admin"));
262    ///     assert_eq!(defaults.port(), Some(22));
263    /// }
264    /// ```
265    pub fn defaults(&self) -> Option<Defaults> {
266        self.defaults
267            .as_ref()
268            .map(|defaults| self.transform_defaults_value(defaults))
269    }
270
271    /// Returns a reference to the raw defaults configuration without applying transforms.
272    ///
273    /// This accessor provides direct, read-only access to the underlying `Defaults`
274    /// data stored in the inventory. No transform function is applied. This is useful
275    /// for debugging, inspection, or when you explicitly need the original defaults.
276    ///
277    /// # Returns
278    ///
279    /// Returns `Some(&Defaults)` if defaults are configured in the inventory, or `None`
280    /// if no defaults are set.
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// # use genja_core::inventory::{Inventory, Defaults};
286    /// let defaults = Defaults::builder()
287    ///     .username("admin")
288    ///     .port(22)
289    ///     .build();
290    ///
291    /// let inventory = Inventory::builder()
292    ///     .defaults(defaults)
293    ///     .build();
294    ///
295    /// let raw_defaults = inventory.defaults_raw().expect("defaults exist");
296    /// assert_eq!(raw_defaults.username(), Some("admin"));
297    /// ```
298    pub fn defaults_raw(&self) -> Option<&Defaults> {
299        self.defaults.as_ref()
300    }
301
302    /// Returns a reference to the transform function options configured for this inventory.
303    ///
304    /// Transform function options provide additional configuration data that is passed to
305    /// the transform function when it processes hosts, groups, or defaults. These options
306    /// allow for dynamic customization of the transform behavior without modifying the
307    /// transform function itself.
308    ///
309    /// The options are stored as a `TransformFunctionOptions` wrapper around a JSON value,
310    /// allowing for flexible, schema-free configuration data.
311    ///
312    /// # Returns
313    ///
314    /// Returns `Some(&TransformFunctionOptions)` containing a reference to the configured
315    /// options if they are set. Returns `None` if no transform function options have been
316    /// configured for this inventory.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// # use genja_core::inventory::{Inventory, TransformFunctionOptions};
322    /// let options = TransformFunctionOptions::new(serde_json::json!({"key": "value"}));
323    /// let inventory = Inventory::builder()
324    ///     .transform_function_options(options)
325    ///     .build();
326    ///
327    /// if let Some(opts) = inventory.transform_function_options() {
328    ///     println!("Transform options configured");
329    /// }
330    /// ```
331    pub fn transform_function_options(&self) -> Option<&TransformFunctionOptions> {
332        self.transform_function_options.as_ref()
333    }
334
335    pub fn connections(&self) -> &ConnectionManager {
336        &self.connections
337    }
338
339    #[cfg(test)]
340    pub(crate) fn resolved_host_cache_len(&self) -> usize {
341        self.resolved_host_cache.len()
342    }
343
344    #[cfg(test)]
345    pub(crate) fn resolved_params_cache_len(&self) -> usize {
346        self.resolved_params_cache.len()
347    }
348
349    /// Resolves a host by applying defaults, group settings, and host-specific configuration.
350    ///
351    /// This method performs hierarchical resolution of host configuration by merging settings
352    /// from multiple sources in priority order. The resolution follows this sequence:
353    ///
354    /// 1. Start with an empty host configuration
355    /// 2. Apply inventory defaults (if present)
356    /// 3. Apply parent group settings recursively (in order of group declaration)
357    /// 4. Apply host-specific settings
358    /// 5. Apply transform function (if configured)
359    ///
360    /// The result is cached to improve performance on subsequent calls for the same host.
361    /// Group resolution handles inheritance chains and prevents circular references.
362    ///
363    /// # Parameters
364    ///
365    /// * `name` - The name of the host to resolve. This should match a key in the inventory's
366    ///   hosts collection. The name is used for both lookup and cache key generation.
367    ///
368    /// # Returns
369    ///
370    /// Returns `Some(Host)` containing the fully resolved host configuration if the host exists
371    /// in the inventory. Returns `None` if the host is not found.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// # use genja_core::inventory::{Inventory, Host, Hosts, BaseBuilderHost};
377    /// let mut hosts = Hosts::new();
378    /// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
379    /// let inventory = Inventory::builder().hosts(hosts).build();
380    ///
381    /// if let Some(resolved) = inventory.resolve_host("router1") {
382    ///     println!("Resolved hostname: {:?}", resolved.hostname());
383    /// }
384    /// ```
385    pub fn resolve_host(&self, name: &str) -> Option<Host> {
386        let key = NatString::new(name.to_string());
387        if let Some(entry) = self.resolved_host_cache.get(&key) {
388            return Some(entry.value().clone());
389        }
390
391        let host = self.hosts.get(name)?;
392        let mut resolved = Host::new();
393
394        if let Some(defaults) = self.defaults.as_ref() {
395            merge_defaults_into_host(&mut resolved, defaults);
396        }
397
398        let mut group_stack = std::collections::HashSet::new();
399        let mut group_cache = std::collections::HashMap::new();
400        if let Some(groups) = host.groups.as_ref() {
401            for group_name in groups.iter() {
402                if let Some(group) =
403                    self.resolve_group_internal(group_name, &mut group_stack, &mut group_cache)
404                {
405                    merge_group_into_host(&mut resolved, &group);
406                }
407            }
408        }
409
410        merge_host_into_host(&mut resolved, host);
411
412        let resolved = self.transform_host_value(&resolved);
413        self.resolved_host_cache.insert(key, resolved.clone());
414        Some(resolved)
415    }
416
417    /// Resolves connection parameters for a specific host and connection plugin name.
418    ///
419    /// This method combines defaults, group settings, and host-specific configuration
420    /// to produce a complete set of connection parameters. The resolution follows a
421    /// hierarchical priority order where each level can have both base fields and
422    /// connection-specific overrides:
423    ///
424    /// **Priority Order (lowest to highest):**
425    /// 1. `defaults` base fields
426    /// 2. `defaults.connection_options[connection_type]`
427    /// 3. `groups` base fields (applied in order for each parent group)
428    /// 4. `groups.connection_options[connection_type]` (applied in order for each parent group)
429    /// 5. `host` base fields
430    /// 6. `host.connection_options[connection_type]`
431    ///
432    /// At each level, connection-specific options override the base fields for that level.
433    /// The final result is a complete set of connection parameters with all fields resolved
434    /// according to this cascading priority system.
435    ///
436    /// Results are cached to improve performance on subsequent calls with the same parameters.
437    ///
438    /// # Parameters
439    ///
440    /// * `name` - The name of the host to resolve connection parameters for. This should
441    ///   match a key in the inventory's hosts collection.
442    /// * `connection_type` - The type of connection to resolve parameters for (e.g., "ssh",
443    ///   "netconf", "http"). This determines which connection_options entry to apply.
444    ///
445    /// # Returns
446    ///
447    /// Returns `Some(ResolvedConnectionParams)` containing the fully resolved connection
448    /// parameters if the host exists in the inventory. Returns `None` if the host is not
449    /// found or cannot be resolved.
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// # use genja_core::inventory::{Inventory, Host, Hosts, BaseBuilderHost};
455    /// let mut hosts = Hosts::new();
456    /// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
457    /// let inventory = Inventory::builder().hosts(hosts).build();
458    ///
459    /// if let Some(params) = inventory.resolve_connection_params("router1", "ssh") {
460    ///     println!("Hostname: {}", params.hostname);
461    /// }
462    /// ```
463    ///
464    /// # Resolution Example
465    ///
466    /// ```text
467    /// Given:
468    ///   defaults:
469    ///     port: 22
470    ///     connection_options:
471    ///       netconf: { port: 830 }
472    ///
473    ///   groups["cisco"]:
474    ///     port: 2200
475    ///     connection_options:
476    ///       netconf: { port: 831 }
477    ///
478    ///   host["router1.lab"]:
479    ///     groups: ["cisco"]
480    ///     port: 2201
481    ///     connection_options:
482    ///       netconf: { port: 832 }
483    ///
484    /// Resolution for connection_type "netconf":
485    ///   1. defaults.port = 22
486    ///   2. defaults.connection_options["netconf"].port = 830 (overrides step 1)
487    ///   3. groups["cisco"].port = 2200 (overrides step 2)
488    ///   4. groups["cisco"].connection_options["netconf"].port = 831 (overrides step 3)
489    ///   5. host.port = 2201 (overrides step 4)
490    ///   6. host.connection_options["netconf"].port = 832 (overrides step 5)
491    ///
492    /// Final result: port = 832
493    /// ```
494    pub fn resolve_connection_params(
495        &self,
496        name: &str,
497        connection_type: &str,
498    ) -> Option<ResolvedConnectionParams> {
499        let key = (
500            NatString::new(name.to_string()),
501            connection_type.to_string(),
502        );
503        if let Some(entry) = self.resolved_params_cache.get(&key) {
504            return Some(entry.value().clone());
505        }
506
507        let host = self.resolve_host(name)?;
508        let resolved = host.resolve_connection_params(connection_type);
509        self.resolved_params_cache.insert(key, resolved.clone());
510        Some(resolved)
511    }
512
513    /// Recursively resolves a group by applying parent group settings and handling inheritance chains.
514    ///
515    /// This internal method performs hierarchical resolution of group configuration by merging settings
516    /// from parent groups. It uses memoization to cache resolved groups and a stack to detect and prevent
517    /// circular references in the group hierarchy.
518    ///
519    /// The resolution process:
520    /// 1. Checks the memo cache for previously resolved groups
521    /// 2. Detects circular references using the stack
522    /// 3. Recursively resolves parent groups
523    /// 4. Merges parent group settings into the current group
524    /// 5. Caches the result for future lookups
525    ///
526    /// # Parameters
527    ///
528    /// * `name` - The name of the group to resolve. This should match a key in the inventory's
529    ///   groups collection.
530    /// * `stack` - A mutable reference to a HashSet tracking the current resolution path. Used to
531    ///   detect circular references in the group hierarchy. Groups already in the stack indicate
532    ///   a circular dependency and will cause the method to return `None`.
533    /// * `memo` - A mutable reference to a HashMap caching previously resolved groups. This improves
534    ///   performance by avoiding redundant resolution of the same group during recursive traversal.
535    ///
536    /// # Returns
537    ///
538    /// Returns `Some(Group)` containing the fully resolved group configuration with all parent
539    /// settings merged. Returns `None` if:
540    /// - The group does not exist in the inventory
541    /// - A circular reference is detected in the group hierarchy
542    /// - The inventory has no groups collection
543    pub(crate) fn resolve_group_internal(
544        &self,
545        name: &str,
546        stack: &mut std::collections::HashSet<String>,
547        memo: &mut std::collections::HashMap<String, Group>,
548    ) -> Option<Group> {
549        if let Some(cached) = memo.get(name) {
550            return Some(cached.clone());
551        }
552
553        if !stack.insert(name.to_string()) {
554            return None;
555        }
556
557        let group = self.groups.as_ref()?.get(name)?;
558        let mut resolved = empty_group();
559
560        if let Some(parent_groups) = group.groups.as_ref() {
561            for parent in parent_groups.iter() {
562                if let Some(parent_group) = self.resolve_group_internal(parent, stack, memo) {
563                    merge_group_into_group(&mut resolved, &parent_group);
564                }
565            }
566        }
567
568        merge_group_into_group(&mut resolved, group);
569
570        stack.remove(name);
571        memo.insert(name.to_string(), resolved.clone());
572        Some(resolved)
573    }
574
575    fn transform_value<T: Clone>(
576        &self,
577        value: &T,
578        apply: impl FnOnce(&TransformFunction, &T, Option<&TransformFunctionOptions>) -> T,
579    ) -> T {
580        match &self.transform_function {
581            Some(transform) => apply(transform, value, self.transform_function_options.as_ref()),
582            None => value.clone(),
583        }
584    }
585
586    fn transform_host_value(&self, host: &Host) -> Host {
587        self.transform_value(host, |transform, host, options| {
588            transform.transform_host(host, options)
589        })
590    }
591
592    fn transform_group_value(&self, group: &Group) -> Group {
593        self.transform_value(group, |transform, group, options| {
594            transform.transform_group(group, options)
595        })
596    }
597
598    fn transform_defaults_value(&self, defaults: &Defaults) -> Defaults {
599        self.transform_value(defaults, |transform, defaults, options| {
600            transform.transform_defaults(defaults, options)
601        })
602    }
603
604    fn cached_value<T: Clone>(
605        &self,
606        key: &NatString,
607        cache: &DashMap<NatString, T>,
608        raw: &T,
609        transform: impl FnOnce(&T) -> T,
610    ) -> T {
611        if let Some(entry) = cache.get(key) {
612            return entry.value().clone();
613        }
614
615        let transformed = transform(raw);
616        cache.insert(key.clone(), transformed.clone());
617        transformed
618    }
619
620    fn cached_host_value(&self, key: &NatString, host: &Host) -> Host {
621        self.cached_value(key, &self.host_cache, host, |host| {
622            self.transform_host_value(host)
623        })
624    }
625
626    fn cached_group_value(&self, key: &NatString, group: &Group) -> Group {
627        self.cached_value(key, &self.group_cache, group, |group| {
628            self.transform_group_value(group)
629        })
630    }
631
632    fn host_in_scope(&self, name: &str) -> bool {
633        self.state.is_in_scope(name)
634    }
635
636    fn host_key_in_scope(&self, key: &NatString) -> bool {
637        self.state.is_in_scope_key(key)
638    }
639}
640
641fn empty_group() -> Group {
642    Group {
643        hostname: None,
644        port: None,
645        username: None,
646        password: None,
647        platform: None,
648        groups: None,
649        data: None,
650        connection_options: None,
651    }
652}
653
654trait OverlayFields {
655    fn hostname(&self) -> &Option<String>;
656    fn port(&self) -> &Option<u16>;
657    fn username(&self) -> &Option<String>;
658    fn password(&self) -> &Option<String>;
659    fn platform(&self) -> &Option<String>;
660    fn data(&self) -> &Option<Data>;
661    fn connection_options(&self) -> &Option<CustomTreeMap<ConnectionOptions>>;
662}
663
664impl OverlayFields for Defaults {
665    fn hostname(&self) -> &Option<String> {
666        &self.hostname
667    }
668
669    fn port(&self) -> &Option<u16> {
670        &self.port
671    }
672
673    fn username(&self) -> &Option<String> {
674        &self.username
675    }
676
677    fn password(&self) -> &Option<String> {
678        &self.password
679    }
680
681    fn platform(&self) -> &Option<String> {
682        &self.platform
683    }
684
685    fn data(&self) -> &Option<Data> {
686        &self.data
687    }
688
689    fn connection_options(&self) -> &Option<CustomTreeMap<ConnectionOptions>> {
690        &self.connection_options
691    }
692}
693
694impl OverlayFields for Group {
695    fn hostname(&self) -> &Option<String> {
696        &self.hostname
697    }
698
699    fn port(&self) -> &Option<u16> {
700        &self.port
701    }
702
703    fn username(&self) -> &Option<String> {
704        &self.username
705    }
706
707    fn password(&self) -> &Option<String> {
708        &self.password
709    }
710
711    fn platform(&self) -> &Option<String> {
712        &self.platform
713    }
714
715    fn data(&self) -> &Option<Data> {
716        &self.data
717    }
718
719    fn connection_options(&self) -> &Option<CustomTreeMap<ConnectionOptions>> {
720        &self.connection_options
721    }
722}
723
724impl OverlayFields for Host {
725    fn hostname(&self) -> &Option<String> {
726        &self.hostname
727    }
728
729    fn port(&self) -> &Option<u16> {
730        &self.port
731    }
732
733    fn username(&self) -> &Option<String> {
734        &self.username
735    }
736
737    fn password(&self) -> &Option<String> {
738        &self.password
739    }
740
741    fn platform(&self) -> &Option<String> {
742        &self.platform
743    }
744
745    fn data(&self) -> &Option<Data> {
746        &self.data
747    }
748
749    fn connection_options(&self) -> &Option<CustomTreeMap<ConnectionOptions>> {
750        &self.connection_options
751    }
752}
753
754fn merge_overlay_into_host<T: OverlayFields>(target: &mut Host, source: &T) {
755    merge_option(&mut target.hostname, source.hostname());
756    merge_option(&mut target.port, source.port());
757    merge_option(&mut target.username, source.username());
758    merge_option(&mut target.password, source.password());
759    merge_option(&mut target.platform, source.platform());
760    merge_data(&mut target.data, source.data());
761    merge_connection_options(&mut target.connection_options, source.connection_options());
762}
763
764fn merge_overlay_into_group<T: OverlayFields>(target: &mut Group, source: &T) {
765    merge_option(&mut target.hostname, source.hostname());
766    merge_option(&mut target.port, source.port());
767    merge_option(&mut target.username, source.username());
768    merge_option(&mut target.password, source.password());
769    merge_option(&mut target.platform, source.platform());
770    merge_data(&mut target.data, source.data());
771    merge_connection_options(&mut target.connection_options, source.connection_options());
772}
773
774fn merge_defaults_into_host(target: &mut Host, defaults: &Defaults) {
775    merge_overlay_into_host(target, defaults);
776}
777
778fn merge_group_into_host(target: &mut Host, group: &Group) {
779    merge_overlay_into_host(target, group);
780}
781
782fn merge_host_into_host(target: &mut Host, host: &Host) {
783    merge_overlay_into_host(target, host);
784    if host.groups.is_some() {
785        target.groups = host.groups.clone();
786    }
787}
788
789fn merge_group_into_group(target: &mut Group, group: &Group) {
790    merge_overlay_into_group(target, group);
791    if group.groups.is_some() {
792        target.groups = group.groups.clone();
793    }
794}
795
796fn merge_option<T: Clone>(target: &mut Option<T>, source: &Option<T>) {
797    if let Some(value) = source.as_ref() {
798        *target = Some(value.clone());
799    }
800}
801
802/// Merges data from a source `Data` option into a target `Data` option.
803///
804/// This function performs intelligent merging of JSON data structures with the following behavior:
805///
806/// 1. **Object Merging**: When both target and source contain JSON objects, the function merges
807///    their key-value pairs. Keys present in the source object will overwrite corresponding keys
808///    in the target object, while keys unique to either object are preserved.
809///
810/// 2. **Non-Object Replacement**: When the target is not a JSON object (e.g., array, string, number)
811///    but the source is an object, the entire target is replaced with the source object rather than
812///    attempting to merge incompatible types.
813///
814/// 3. **Initialization**: When the target is `None` and the source contains data, the target is
815///    initialized with a clone of the source data.
816///
817/// 4. **No-Op Cases**: When the source is `None`, the target remains unchanged regardless of its state.
818///
819/// This function is used internally during host and group resolution to merge data fields from
820/// defaults, parent groups, and host-specific configurations in the proper priority order.
821///
822/// # Parameters
823///
824/// * `target` - A mutable reference to an optional `Data` value that will be modified in place.
825///   This represents the destination for the merge operation. If `None`, it may be initialized
826///   with the source data. If `Some`, its contents may be merged with or replaced by the source.
827///
828/// * `source` - A reference to an optional `Data` value containing the data to merge into the target.
829///   This represents the source of new or overriding values. If `None`, no changes are made to the
830///   target. If `Some`, its contents are merged into or replace the target based on their types.
831///
832/// # Examples
833///
834/// See the unit test `merge_data_merges_objects_and_replaces_non_objects` in the unit tests
835/// for a comprehensive example of how this function is used in practice during inventory resolution.
836pub(crate) fn merge_data(target: &mut Option<Data>, source: &Option<Data>) {
837    match (target.as_mut(), source.as_ref()) {
838        (Some(target_data), Some(source_data)) => {
839            if let (Some(target_obj), Some(source_obj)) =
840                (target_data.as_object_mut(), source_data.as_object())
841            {
842                for (key, value) in source_obj {
843                    target_obj.insert(key.clone(), value.clone());
844                }
845            } else {
846                *target = Some(source_data.clone());
847            }
848        }
849        (None, Some(source_data)) => {
850            *target = Some(source_data.clone());
851        }
852        _ => {}
853    }
854}
855
856pub(crate) fn merge_connection_options(
857    target: &mut Option<CustomTreeMap<ConnectionOptions>>,
858    source: &Option<CustomTreeMap<ConnectionOptions>>,
859) {
860    let Some(source_map) = source.as_ref() else {
861        return;
862    };
863
864    if target.is_none() {
865        *target = Some(CustomTreeMap::new());
866    }
867
868    let target_map = target.as_mut().expect("target map initialized");
869    for (name, options) in source_map.iter() {
870        if let Some(existing) = target_map.get_mut(name.as_str()) {
871            merge_connection_options_fields(existing, options);
872        } else {
873            target_map.insert(name.as_str(), options.clone());
874        }
875    }
876}
877
878pub(crate) fn merge_connection_options_fields(
879    target: &mut ConnectionOptions,
880    source: &ConnectionOptions,
881) {
882    if source.hostname.is_some() {
883        target.hostname = source.hostname.clone();
884    }
885    if source.port.is_some() {
886        target.port = source.port;
887    }
888    if source.username.is_some() {
889        target.username = source.username.clone();
890    }
891    if source.password.is_some() {
892        target.password = source.password.clone();
893    }
894    if source.platform.is_some() {
895        target.platform = source.platform.clone();
896    }
897    if source.extras.is_some() {
898        target.extras = source.extras.clone();
899    }
900}
901
902/// A view over the hosts collection in an inventory that applies transform functions on access.
903///
904/// This struct provides a read-only view of the hosts stored in an `Inventory`. When accessing
905/// individual hosts through this view, any configured transform function is automatically applied.
906/// The view caches transformed results to improve performance on subsequent accesses to the same host.
907///
908/// The view does not own the inventory data; it holds a reference to the parent `Inventory` and
909/// provides methods to iterate over hosts, look up hosts by name, and query collection metadata.
910///
911/// # Lifetime
912///
913/// * `'a` - The lifetime of the reference to the parent `Inventory`. The view cannot outlive
914///   the inventory it references.
915///
916/// # Examples
917///
918/// ```
919/// # use genja_core::inventory::{Inventory, Host, Hosts, BaseBuilderHost};
920/// let mut hosts = Hosts::new();
921/// hosts.add_host("router1", Host::builder().hostname("10.0.0.1").build());
922/// let inventory = Inventory::builder().hosts(hosts).build();
923///
924/// let hosts_view = inventory.hosts();
925/// assert_eq!(hosts_view.len(), 1);
926///
927/// if let Some(host) = hosts_view.get("router1") {
928///     assert_eq!(host.hostname(), Some("10.0.0.1"));
929/// }
930///
931/// for (name, host) in hosts_view.iter() {
932///     println!("Host: {}", name);
933/// }
934/// ```
935pub struct HostsView<'a> {
936    inventory: &'a Inventory,
937}
938
939impl<'a> HostsView<'a> {
940    pub fn len(&self) -> usize {
941        self.inventory
942            .hosts
943            .keys()
944            .filter(|key| self.inventory.host_key_in_scope(key))
945            .count()
946    }
947
948    pub fn is_empty(&self) -> bool {
949        self.len() == 0
950    }
951
952    pub fn keys(&self) -> impl Iterator<Item = &'a NatString> {
953        self.inventory
954            .hosts
955            .keys()
956            .filter(|key| self.inventory.host_key_in_scope(key))
957    }
958
959    pub fn get(&self, name: &str) -> Option<Host> {
960        if !self.inventory.host_in_scope(name) {
961            return None;
962        }
963        let key = NatString::new(name.to_string());
964        self.inventory
965            .hosts
966            .get(name)
967            .map(|host| self.inventory.cached_host_value(&key, host))
968    }
969
970    pub fn iter(&self) -> impl Iterator<Item = (&'a NatString, Host)> {
971        self.inventory.hosts.iter().filter_map(|(id, host)| {
972            if self.inventory.host_key_in_scope(id) {
973                Some((id, self.inventory.cached_host_value(id, host)))
974            } else {
975                None
976            }
977        })
978    }
979}
980
981/// A view over the groups collection in an inventory that applies transform functions on access.
982///
983/// This struct provides a read-only view of the groups stored in an `Inventory`. When accessing
984/// individual groups through this view, any configured transform function is automatically applied.
985/// The view caches transformed results to improve performance on subsequent accesses to the same group.
986///
987/// The view does not own the inventory data; it holds references to both the parent `Inventory` and
988/// the underlying `Groups` collection. It provides methods to iterate over groups, look up groups by
989/// name, and query collection metadata.
990///
991/// # Lifetime
992///
993/// * `'a` - The lifetime of the references to the parent `Inventory` and `Groups` collection. The view
994///   cannot outlive either the inventory or groups it references.
995///
996/// # Examples
997///
998/// ```
999/// # use genja_core::inventory::{Inventory, Group, Groups, BaseBuilderHost};
1000/// let mut groups = Groups::new();
1001/// groups.add_group("core", Group::builder().platform("linux").build());
1002/// let inventory = Inventory::builder().groups(groups).build();
1003///
1004/// if let Some(groups_view) = inventory.groups() {
1005///     assert_eq!(groups_view.len(), 1);
1006///
1007///     if let Some(group) = groups_view.get("core") {
1008///         assert_eq!(group.platform(), Some("linux"));
1009///     }
1010///
1011///     for (name, group) in groups_view.iter() {
1012///         println!("Group: {}", name);
1013///     }
1014/// }
1015/// ```
1016pub struct GroupsView<'a> {
1017    inventory: &'a Inventory,
1018    groups: &'a Groups,
1019}
1020
1021impl<'a> GroupsView<'a> {
1022    pub fn len(&self) -> usize {
1023        self.groups.len()
1024    }
1025
1026    pub fn is_empty(&self) -> bool {
1027        self.groups.is_empty()
1028    }
1029
1030    pub fn keys(&self) -> impl Iterator<Item = &'a NatString> {
1031        self.groups.keys()
1032    }
1033
1034    pub fn get(&self, name: &str) -> Option<Group> {
1035        let key = NatString::new(name.to_string());
1036        self.groups
1037            .get(name)
1038            .map(|group| self.inventory.cached_group_value(&key, group))
1039    }
1040
1041    pub fn iter(&self) -> impl Iterator<Item = (&'a NatString, Group)> {
1042        self.groups
1043            .iter()
1044            .map(|(id, group)| (id, self.inventory.cached_group_value(id, group)))
1045    }
1046}
1047
1048impl Default for Inventory {
1049    fn default() -> Self {
1050        Inventory {
1051            hosts: Hosts::new(),
1052            groups: None,
1053            defaults: None,
1054            transform_function: None,
1055            transform_function_options: None,
1056            connections: Arc::new(ConnectionManager::default()),
1057            host_cache: DashMap::new(),
1058            group_cache: DashMap::new(),
1059            resolved_host_cache: DashMap::new(),
1060            resolved_params_cache: DashMap::new(),
1061            state: Arc::new(State::new()),
1062        }
1063    }
1064}
1065/// Builder for constructing `Inventory` instances with custom configuration.
1066///
1067/// This builder provides a fluent interface for creating `Inventory` objects
1068/// with optional hosts, groups, defaults, and transform settings. Fields that
1069/// are not explicitly set will use their default values when `build()` is called.
1070///
1071/// # Fields
1072///
1073/// * `hosts` - Optional hosts map. When set to `Some(hosts)`, the provided hosts
1074///   are used. When `None`, an empty `Hosts` map is used.
1075/// * `groups` - Optional groups map. When set, the provided groups are used.
1076/// * `defaults` - Optional defaults object. When set, the provided defaults are used.
1077/// * `transform_function` - Optional transform function applied lazily on access.
1078/// * `transform_function_options` - Optional JSON options passed to the transform.
1079/// * `connections` - Optional connection manager. When `None`, a default
1080///   `ConnectionManager` is created.
1081///
1082/// # Examples
1083///
1084/// ```
1085/// use genja_core::inventory::{Host, Hosts, Inventory, BaseBuilderHost};
1086///
1087/// let mut hosts = Hosts::new();
1088/// let host = Host::builder().hostname("10.0.0.1").build();
1089/// hosts.add_host("router1", host);
1090///
1091/// let inventory = Inventory::builder()
1092///     .hosts(hosts)
1093///     .build();
1094/// ```
1095pub struct InventoryBuilder {
1096    pub hosts: Option<Hosts>,
1097    pub groups: Option<Groups>,
1098    pub defaults: Option<Defaults>,
1099    pub transform_function: Option<TransformFunction>,
1100    pub transform_function_options: Option<TransformFunctionOptions>,
1101    pub connections: Option<Arc<ConnectionManager>>,
1102}
1103
1104impl InventoryBuilder {
1105    pub fn new() -> InventoryBuilder {
1106        InventoryBuilder {
1107            hosts: None,
1108            groups: None,
1109            defaults: None,
1110            transform_function: None,
1111            transform_function_options: None,
1112            connections: None,
1113        }
1114    }
1115
1116    pub fn hosts(mut self, hosts: Hosts) -> Self {
1117        self.hosts = Some(hosts);
1118        self
1119    }
1120
1121    pub fn groups(mut self, groups: Groups) -> Self {
1122        self.groups = Some(groups);
1123        self
1124    }
1125
1126    pub fn defaults(mut self, defaults: Defaults) -> Self {
1127        self.defaults = Some(defaults);
1128        self
1129    }
1130
1131    pub fn transform_function(mut self, transform: TransformFunction) -> Self {
1132        self.transform_function = Some(transform);
1133        self
1134    }
1135
1136    pub fn transform_function_options(mut self, options: TransformFunctionOptions) -> Self {
1137        self.transform_function_options = Some(options);
1138        self
1139    }
1140
1141    pub fn connections(mut self, connections: ConnectionManager) -> Self {
1142        self.connections = Some(Arc::new(connections));
1143        self
1144    }
1145
1146    pub fn build(self) -> Inventory {
1147        let hosts = self.hosts.unwrap_or_default();
1148        let state = State::new();
1149        for key in hosts.keys() {
1150            state.mark_in_scope_key(key);
1151        }
1152
1153        Inventory {
1154            hosts,
1155            groups: self.groups,
1156            defaults: self.defaults,
1157            transform_function: self.transform_function,
1158            transform_function_options: self.transform_function_options,
1159            connections: self
1160                .connections
1161                .unwrap_or_else(|| Arc::new(ConnectionManager::default())),
1162            host_cache: DashMap::new(),
1163            group_cache: DashMap::new(),
1164            resolved_host_cache: DashMap::new(),
1165            resolved_params_cache: DashMap::new(),
1166            state: Arc::new(state),
1167        }
1168    }
1169}
1170
1171impl Default for InventoryBuilder {
1172    fn default() -> Self {
1173        Self::new()
1174    }
1175}