Skip to main content

genja_core/
errors.rs

1//! Core error types for Genja.
2//!
3//! This module currently defines configuration, inventory, and runtime error types used by
4//! core APIs to report failures in a consistent way.
5
6use std::fmt;
7
8/// Logical inventory section associated with an inventory load error.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum InventoryFileKind {
11    /// The hosts inventory file.
12    Hosts,
13    /// The groups inventory file.
14    Groups,
15    /// The defaults inventory file.
16    Defaults,
17}
18
19impl fmt::Display for InventoryFileKind {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            InventoryFileKind::Hosts => write!(f, "hosts"),
23            InventoryFileKind::Groups => write!(f, "groups"),
24            InventoryFileKind::Defaults => write!(f, "defaults"),
25        }
26    }
27}
28
29/// Error returned when inventory loading fails.
30#[derive(Debug, Clone)]
31pub enum InventoryLoadError {
32    /// Reading an inventory file failed.
33    Read {
34        /// Which logical inventory file failed.
35        kind: InventoryFileKind,
36        /// Filesystem path that was being read.
37        path: String,
38        /// Underlying read failure rendered as text.
39        message: String,
40    },
41    /// Parsing a JSON inventory file failed.
42    ParseJson {
43        /// Which logical inventory file failed.
44        kind: InventoryFileKind,
45        /// Filesystem path that was being parsed.
46        path: String,
47        /// Underlying parse failure rendered as text.
48        message: String,
49    },
50    /// Parsing a YAML inventory file failed.
51    ParseYaml {
52        /// Which logical inventory file failed.
53        kind: InventoryFileKind,
54        /// Filesystem path that was being parsed.
55        path: String,
56        /// Underlying parse failure rendered as text.
57        message: String,
58    },
59    /// The inventory file extension is not supported.
60    UnsupportedFormat {
61        /// Which logical inventory file failed.
62        kind: InventoryFileKind,
63        /// Filesystem path with the unsupported extension.
64        path: String,
65    },
66    /// A configured transform plugin was not found.
67    TransformPluginNotFound {
68        /// Missing plugin name.
69        name: String,
70    },
71    /// A configured plugin exists but is not a transform-function plugin.
72    NotTransformPlugin {
73        /// Plugin name with the wrong type.
74        name: String,
75    },
76    /// A human-readable fallback error message.
77    Message(String),
78}
79
80impl fmt::Display for InventoryLoadError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            InventoryLoadError::Read {
84                kind,
85                path,
86                message,
87            } => write!(f, "failed to read {kind} inventory file {path}: {message}"),
88            InventoryLoadError::ParseJson {
89                kind,
90                path,
91                message,
92            } => write!(
93                f,
94                "failed to parse {kind} inventory JSON file {path}: {message}"
95            ),
96            InventoryLoadError::ParseYaml {
97                kind,
98                path,
99                message,
100            } => write!(
101                f,
102                "failed to parse {kind} inventory YAML file {path}: {message}"
103            ),
104            InventoryLoadError::UnsupportedFormat { kind, path } => write!(
105                f,
106                "unsupported {kind} inventory file format for {path}. Use .json, .yaml, or .yml"
107            ),
108            InventoryLoadError::TransformPluginNotFound { name } => {
109                write!(f, "transform plugin '{name}' not found")
110            }
111            InventoryLoadError::NotTransformPlugin { name } => {
112                write!(f, "plugin '{name}' is not a transform function plugin")
113            }
114            InventoryLoadError::Message(msg) => write!(f, "{msg}"),
115        }
116    }
117}
118
119impl std::error::Error for InventoryLoadError {}
120
121impl From<String> for InventoryLoadError {
122    fn from(value: String) -> Self {
123        InventoryLoadError::Message(value)
124    }
125}
126
127impl From<&str> for InventoryLoadError {
128    fn from(value: &str) -> Self {
129        InventoryLoadError::Message(value.to_string())
130    }
131}
132
133/// Error returned when SSH configuration validation or parsing fails.
134#[derive(Debug, Clone)]
135pub enum SshConfigError {
136    /// The SSH config file path does not exist.
137    NotFound { path: String },
138    /// The SSH config file exists but access was denied.
139    PermissionDenied { path: String, message: String },
140    /// Checking whether the SSH config file exists failed.
141    CheckFailed { path: String, message: String },
142    /// Opening the SSH config file failed.
143    OpenFailed { path: String, message: String },
144    /// Parsing the SSH config file failed.
145    ParseFailed { path: String, message: String },
146}
147
148impl fmt::Display for SshConfigError {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            SshConfigError::NotFound { path } => write!(f, "SSH config file not found: {path}"),
152            SshConfigError::PermissionDenied { path, message } => {
153                write!(
154                    f,
155                    "SSH config file exists but permission denied: {path}: {message}"
156                )
157            }
158            SshConfigError::CheckFailed { path, message } => {
159                write!(f, "Failed to check SSH config file {path}: {message}")
160            }
161            SshConfigError::OpenFailed { path, message } => {
162                write!(f, "Failed to open SSH config file {path}: {message}")
163            }
164            SshConfigError::ParseFailed { path, message } => {
165                write!(f, "Failed to parse SSH config file {path}: {message}")
166            }
167        }
168    }
169}
170
171impl std::error::Error for SshConfigError {}
172
173/// Error returned when loading the top-level settings file fails.
174#[derive(Debug, Clone)]
175pub enum ConfigLoadError {
176    /// The settings file extension is not supported.
177    UnsupportedFormat { path: String },
178    /// Building the config source from disk failed.
179    Read { path: String, message: String },
180    /// Deserializing settings from the config source failed.
181    Deserialize { path: String, message: String },
182    /// SSH configuration referenced by settings failed validation.
183    SshConfig(SshConfigError),
184}
185
186impl fmt::Display for ConfigLoadError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            ConfigLoadError::UnsupportedFormat { path } => {
190                write!(
191                    f,
192                    "unsupported settings file format for {path}. Use .json, .yaml, or .yml"
193                )
194            }
195            ConfigLoadError::Read { path, message } => {
196                write!(f, "failed to read settings from {path}: {message}")
197            }
198            ConfigLoadError::Deserialize { path, message } => {
199                write!(f, "failed to deserialize settings from {path}: {message}")
200            }
201            ConfigLoadError::SshConfig(err) => write!(f, "{err}"),
202        }
203    }
204}
205
206impl std::error::Error for ConfigLoadError {}
207
208/// Generic error type for core Genja operations.
209#[derive(Debug, Clone)]
210pub enum GenjaError {
211    /// Plugins have not been loaded for the runtime.
212    PluginsNotLoaded,
213    /// Inventory has not been loaded for the runtime.
214    InventoryNotLoaded,
215    /// A requested plugin name could not be found.
216    PluginNotFound(String),
217    /// The named plugin is not an inventory plugin.
218    NotInventoryPlugin(String),
219    /// The named plugin is an async-only inventory plugin and requires async construction.
220    AsyncInventoryPluginRequiresAsyncConstruction(String),
221    /// The named plugin is not a runner plugin.
222    NotRunnerPlugin(String),
223    /// A plugin failed to load.
224    PluginLoad(String),
225    /// The configuration file could not be read, parsed, or validated.
226    ConfigLoad(ConfigLoadError),
227    /// Inventory loading failed.
228    InventoryLoad(InventoryLoadError),
229    /// A human-readable error message.
230    Message(String),
231    /// Functionality is not implemented yet.
232    NotImplemented(&'static str),
233}
234
235impl fmt::Display for GenjaError {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match self {
238            GenjaError::PluginsNotLoaded => write!(f, "plugins have not been loaded"),
239            GenjaError::InventoryNotLoaded => write!(f, "inventory has not been loaded"),
240            GenjaError::PluginNotFound(name) => write!(f, "plugin '{name}' not found"),
241            GenjaError::NotInventoryPlugin(name) => {
242                write!(f, "plugin '{name}' is not an inventory plugin")
243            }
244            GenjaError::AsyncInventoryPluginRequiresAsyncConstruction(name) => {
245                write!(
246                    f,
247                    "async inventory plugin '{name}' requires async runtime construction"
248                )
249            }
250            GenjaError::NotRunnerPlugin(name) => {
251                write!(f, "plugin '{name}' is not a runner plugin")
252            }
253            GenjaError::PluginLoad(err) => write!(f, "failed to load plugins: {err}"),
254            GenjaError::ConfigLoad(err) => write!(f, "failed to load settings: {err}"),
255            GenjaError::InventoryLoad(err) => write!(f, "failed to load inventory: {err}"),
256            GenjaError::Message(msg) => write!(f, "{msg}"),
257            GenjaError::NotImplemented(msg) => write!(f, "{msg}"),
258        }
259    }
260}
261
262impl std::error::Error for GenjaError {}
263
264impl From<String> for GenjaError {
265    fn from(value: String) -> Self {
266        GenjaError::Message(value)
267    }
268}
269
270impl From<&str> for GenjaError {
271    fn from(value: &str) -> Self {
272        GenjaError::Message(value.to_string())
273    }
274}
275
276impl From<InventoryLoadError> for GenjaError {
277    fn from(value: InventoryLoadError) -> Self {
278        GenjaError::InventoryLoad(value)
279    }
280}
281
282impl From<ConfigLoadError> for GenjaError {
283    fn from(value: ConfigLoadError) -> Self {
284        GenjaError::ConfigLoad(value)
285    }
286}