Skip to main content

genja_core/settings/
root.rs

1use super::{CoreConfig, InventoryConfig, LoggingConfig, RunnerConfig, SSHConfig};
2use crate::ConfigLoadError;
3use config::{Config as ConfigBuilder, File, FileFormat};
4use serde::{Deserialize, Serialize};
5
6/// Top-level Genja configuration.
7///
8/// Missing sections deserialize to their defaults.
9#[derive(Deserialize, Serialize, Clone, Debug, Default)]
10#[serde(default)]
11pub struct Settings {
12    core: CoreConfig,
13    inventory: InventoryConfig,
14    ssh: SSHConfig,
15    runner: RunnerConfig,
16    logging: LoggingConfig,
17}
18
19impl Settings {
20    /// Loads settings from a `.json`, `.yaml`, or `.yml` file.
21    ///
22    /// Missing fields fall back to defaults. If an SSH config path is configured,
23    /// it is validated after deserialization.
24    pub fn from_file(file_path: &str) -> Result<Self, ConfigLoadError> {
25        let format = if file_path.ends_with(".json") {
26            FileFormat::Json
27        } else if file_path.ends_with(".yaml") || file_path.ends_with(".yml") {
28            FileFormat::Yaml
29        } else {
30            return Err(ConfigLoadError::UnsupportedFormat {
31                path: file_path.to_string(),
32            });
33        };
34        let config = ConfigBuilder::builder()
35            .add_source(File::new(file_path, format).required(true))
36            .build()
37            .map_err(|err| ConfigLoadError::Read {
38                path: file_path.to_string(),
39                message: err.to_string(),
40            })?;
41        let parsed_config: Settings =
42            config
43                .try_deserialize()
44                .map_err(|err| ConfigLoadError::Deserialize {
45                    path: file_path.to_string(),
46                    message: err.to_string(),
47                })?;
48
49        parsed_config
50            .ssh
51            .validate()
52            .map_err(ConfigLoadError::SshConfig)?;
53        Ok(parsed_config)
54    }
55
56    pub fn core(&self) -> &CoreConfig {
57        &self.core
58    }
59
60    pub fn inventory(&self) -> &InventoryConfig {
61        &self.inventory
62    }
63
64    pub fn ssh(&self) -> &SSHConfig {
65        &self.ssh
66    }
67
68    pub fn runner(&self) -> &RunnerConfig {
69        &self.runner
70    }
71
72    pub fn logging(&self) -> &LoggingConfig {
73        &self.logging
74    }
75}
76
77impl Settings {
78    pub fn builder() -> SettingsBuilder {
79        SettingsBuilder::default()
80    }
81}
82
83/// Builder for `Settings`.
84#[derive(Default)]
85pub struct SettingsBuilder {
86    core: Option<CoreConfig>,
87    inventory: Option<InventoryConfig>,
88    ssh: Option<SSHConfig>,
89    runner: Option<RunnerConfig>,
90    logging: Option<LoggingConfig>,
91}
92
93impl SettingsBuilder {
94    pub fn core(mut self, core: CoreConfig) -> Self {
95        self.core = Some(core);
96        self
97    }
98
99    pub fn inventory(mut self, inventory: InventoryConfig) -> Self {
100        self.inventory = Some(inventory);
101        self
102    }
103
104    pub fn ssh(mut self, ssh: SSHConfig) -> Self {
105        self.ssh = Some(ssh);
106        self
107    }
108
109    pub fn runner(mut self, runner: RunnerConfig) -> Self {
110        self.runner = Some(runner);
111        self
112    }
113
114    pub fn logging(mut self, logging: LoggingConfig) -> Self {
115        self.logging = Some(logging);
116        self
117    }
118
119    pub fn build(self) -> Settings {
120        Settings {
121            core: self.core.unwrap_or_default(),
122            inventory: self.inventory.unwrap_or_default(),
123            ssh: self.ssh.unwrap_or_default(),
124            runner: self.runner.unwrap_or_default(),
125            logging: self.logging.unwrap_or_default(),
126        }
127    }
128}