genja_core/settings/inventory_loading.rs
1use super::InventoryConfig;
2use crate::inventory::{Defaults, Groups, Hosts};
3use crate::{InventoryFileKind, InventoryLoadError};
4use serde::de::DeserializeOwned;
5
6impl InventoryConfig {
7 /// Loads inventory data from configured file paths.
8 ///
9 /// This method reads and deserializes inventory files (hosts, groups, and defaults)
10 /// based on the paths specified in the `options` field. If a file path is not
11 /// provided for a particular inventory component, a default or empty value is used.
12 ///
13 /// # Returns
14 ///
15 /// Returns a `Result` containing a tuple of:
16 /// * `Hosts` - The loaded hosts inventory. If no hosts file is specified, returns
17 /// an empty `Hosts` instance.
18 /// * `Option<Groups>` - The loaded groups inventory, or `None` if no groups file
19 /// is specified.
20 /// * `Option<Defaults>` - The loaded defaults inventory, or `None` if no defaults
21 /// file is specified.
22 ///
23 /// # Errors
24 ///
25 /// Returns [`InventoryLoadError`] if:
26 /// * Any specified file cannot be read
27 /// * Any file contains invalid JSON or YAML syntax
28 /// * A file has an unsupported format (not .json, .yaml, or .yml)
29 ///
30 /// # Examples
31 ///
32 /// ```no_run
33 /// use genja_core::settings::InventoryConfig;
34 ///
35 /// let config = InventoryConfig::default();
36 /// match config.load_inventory_files() {
37 /// Ok((hosts, groups, defaults)) => {
38 /// println!("Loaded {} hosts", hosts.len());
39 /// }
40 /// Err(e) => eprintln!("Failed to load inventory: {}", e),
41 /// }
42 /// ```
43 pub fn load_inventory_files(
44 &self,
45 ) -> Result<(Hosts, Option<Groups>, Option<Defaults>), InventoryLoadError> {
46 let hosts = match self.options.hosts_file.as_deref() {
47 Some(path) => Self::load_from_file::<Hosts>(InventoryFileKind::Hosts, path)?,
48 None => Hosts::new(),
49 };
50
51 let groups = match self.options.groups_file.as_deref() {
52 Some(path) => Some(Self::load_from_file::<Groups>(
53 InventoryFileKind::Groups,
54 path,
55 )?),
56 None => None,
57 };
58
59 let defaults = match self.options.defaults_file.as_deref() {
60 Some(path) => Some(Self::load_from_file::<Defaults>(
61 InventoryFileKind::Defaults,
62 path,
63 )?),
64 None => None,
65 };
66
67 Ok((hosts, groups, defaults))
68 }
69
70 /// Loads and deserializes data from a file.
71 ///
72 /// This helper method reads a file from the filesystem and deserializes its contents
73 /// based on the file extension. Supports JSON (.json) and YAML (.yaml, .yml) formats.
74 ///
75 /// # Type Parameters
76 ///
77 /// * `T` - The type to deserialize the file contents into. Must implement `DeserializeOwned`.
78 ///
79 /// # Parameters
80 ///
81 /// * `path` - The file path to read and deserialize. The file extension determines
82 /// the deserialization format.
83 ///
84 /// # Returns
85 ///
86 /// Returns a `Result` containing the deserialized data of type `T`.
87 ///
88 /// # Errors
89 ///
90 /// Returns [`InventoryLoadError`] if:
91 /// * The file cannot be read (e.g., doesn't exist, permission denied)
92 /// * The file contents cannot be parsed as valid JSON or YAML
93 /// * The file has an unsupported extension (not .json, .yaml, or .yml)
94 fn load_from_file<T>(kind: InventoryFileKind, path: &str) -> Result<T, InventoryLoadError>
95 where
96 T: DeserializeOwned,
97 {
98 let contents = std::fs::read_to_string(path).map_err(|e| InventoryLoadError::Read {
99 kind,
100 path: path.to_string(),
101 message: e.to_string(),
102 })?;
103
104 if path.ends_with(".json") {
105 serde_json::from_str(&contents).map_err(|e| InventoryLoadError::ParseJson {
106 kind,
107 path: path.to_string(),
108 message: e.to_string(),
109 })
110 } else if path.ends_with(".yaml") || path.ends_with(".yml") {
111 serde_yaml::from_str(&contents).map_err(|e| InventoryLoadError::ParseYaml {
112 kind,
113 path: path.to_string(),
114 message: e.to_string(),
115 })
116 } else {
117 Err(InventoryLoadError::UnsupportedFormat {
118 kind,
119 path: path.to_string(),
120 })
121 }
122 }
123}