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}