arachnid_cli/config/
settings.rs

1/*
2    Appellation: settings <module>
3    Contrib: FL03 <jo3mccain@icloud.com>
4*/
5use super::{kinds::*, types::*};
6
7type ConfigBuilder<S = config::builder::DefaultState> = config::builder::ConfigBuilder<S>;
8
9type ConfigResult<T> = core::result::Result<T, config::ConfigError>;
10
11fn _load_env_or_default(env: &str, default: impl ToString) -> String {
12    std::env::var(env).unwrap_or_else(|_| default.to_string())
13}
14
15fn source_file(
16    dir: impl ToString,
17    name: impl ToString,
18) -> config::File<config::FileSourceFile, config::FileFormat> {
19    let fname = format!("{p}/{f}", p = dir.to_string(), f = name.to_string());
20    config::File::with_name(&fname)
21}
22
23fn with_sources<T: core::fmt::Display>(
24    ctx: ConfigBuilder,
25    workdir: &str,
26    names: impl IntoIterator<Item = T>,
27) -> ConfigBuilder {
28    let mut tmp = ctx;
29    for n in names {
30        tmp = tmp.add_source(source_file(workdir, n).required(false));
31    }
32    tmp
33}
34
35fn set_default(builder: ConfigBuilder) -> ConfigResult<ConfigBuilder> {
36    let builder = builder
37        .set_default("mode", "debug")?
38        .set_default("name", crate::config::APP_NAME)?
39        .set_default("version", env!("CARGO_PKG_VERSION"))?
40        .set_default("scope.context", ".")?
41        .set_default("scope.workdir", crate::config::DEFAULT_WORKDIR)?
42        .set_default("network.address.host", crate::config::DEFAULT_HOST)?
43        .set_default("network.address.port", crate::config::DEFAULT_PORT)?
44        .set_default("services.tracing.level", "info")?;
45    Ok(builder)
46}
47
48fn add_sources(builder: ConfigBuilder) -> ConfigBuilder {
49    let workdir = _load_env_or_default("APP_CONFIG_DIR", crate::config::DEFAULT_DIR_CONFIG);
50    // get the settings file name
51    let fname = _load_env_or_default("APP_CONFIG_FILE", crate::config::DEFAULT_CONFIG_FILE);
52    // setup the builder's sources
53    let builder = with_sources(
54        builder,
55        &workdir,
56        &[
57            "default.config",
58            "debug.config",
59            "development.config",
60            "app.config",
61            "prod.config",
62            &fname,
63        ],
64    );
65
66    builder
67        .add_source(config::Environment::with_prefix("APP").separator("_"))
68        .add_source(config::File::with_name(&fname).required(false))
69}
70
71fn set_overrides(builder: ConfigBuilder) -> ConfigResult<ConfigBuilder> {
72    Ok({
73        builder
74            .set_override_option("mode", std::env::var("APP_MODE").ok())?
75            .set_override_option("name", std::env::var("APP_NAME").ok())?
76            .set_override_option("network.address.host", std::env::var("APP_HOST").ok())?
77            .set_override_option("network.address.port", std::env::var("APP_PORT").ok())?
78            .set_override_option("workspace.workdir", std::env::var("APP_WORKDIR").ok())?
79    })
80}
81
82#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
83#[serde(default, deny_unknown_fields, rename_all = "snake_case")]
84pub struct Settings {
85    pub mode: Mode,
86    pub name: String,
87    pub network: NetworkConfig,
88    pub scope: Scope,
89    pub services: ServicesConfig,
90    pub version: String,
91    pub workspace: WorkspaceConfig,
92}
93
94impl Settings {
95    pub const DEFAULT_MODE: Mode = Mode::Debug;
96    pub const APP_NAME: &'static str = "arachnid";
97    /// attempts to initialize a [`build`](Self::build) a new instance of the configuration,
98    /// falling back to the logical default if unsuccessful
99    pub fn new() -> Self {
100        Self::build().unwrap_or_default()
101    }
102    /// initialize a new instance of the settings from the given [`Mode`]
103    pub fn from_mode(mode: Mode) -> Self {
104        Self {
105            mode,
106            name: Self::APP_NAME.to_string(),
107            network: NetworkConfig::default(),
108            scope: Scope::default(),
109            services: ServicesConfig::default(),
110            version: env!("CARGO_PKG_VERSION").to_string(),
111            workspace: WorkspaceConfig::default(),
112        }
113    }
114    /// consumes the current instance to create another in [`Debug`](Mode::Debug) mode
115    pub fn debug(self) -> Self {
116        Self {
117            mode: Mode::Debug,
118            ..self
119        }
120    }
121    /// consumes the current instance to create another in [`Release`](Mode::Release) mode
122    pub fn release(self) -> Self {
123        Self {
124            mode: Mode::Release,
125            ..self
126        }
127    }
128    /// tries to build the settings from the configuration sources
129    pub fn build() -> Result<Self, config::ConfigError> {
130        Self::builder_base()?.build()?.try_deserialize()
131    }
132    /// returns a copy of the configured [`Mode`]
133    pub const fn mode(&self) -> Mode {
134        self.mode
135    }
136    /// returns a reference to the application name
137    pub fn name(&self) -> &str {
138        &self.name
139    }
140    /// returns a reference to the network configuration
141    pub const fn network(&self) -> &NetworkConfig {
142        &self.network
143    }
144    /// returns a mutable reference to the network configuration
145    pub const fn network_mut(&mut self) -> &mut NetworkConfig {
146        &mut self.network
147    }
148    /// returns a reference to the current scope
149    pub const fn scope(&self) -> &Scope {
150        &self.scope
151    }
152    /// returns a mutable reference to the current scope
153    pub const fn scope_mut(&mut self) -> &mut Scope {
154        &mut self.scope
155    }
156    /// returns a reference to the services
157    pub const fn services(&self) -> &ServicesConfig {
158        &self.services
159    }
160    /// returns a mutable reference to the services
161    pub const fn services_mut(&mut self) -> &mut ServicesConfig {
162        &mut self.services
163    }
164    /// returns the version of the application
165    pub fn version(&self) -> &str {
166        &self.version
167    }
168    /// returns a reference to the workspace
169    pub const fn workspace(&self) -> &WorkspaceConfig {
170        &self.workspace
171    }
172    /// returns a mutable reference to the workspace
173    pub const fn workspace_mut(&mut self) -> &mut WorkspaceConfig {
174        &mut self.workspace
175    }
176    /// attempts to bind a [`TcpListener`](tokio::net::TcpListener) to the configured network
177    /// address.
178    pub async fn bind(&self) -> std::io::Result<tokio::net::TcpListener> {
179        self.network().bind().await
180    }
181    /// Initialize tracing modules
182    pub fn init_tracing(&self) {
183        self.services().tracing().init_tracing(self.name());
184    }
185    /// set the working directory of the scope
186    pub fn set_workdir<T>(&mut self, workdir: T)
187    where
188        std::path::PathBuf: From<T>,
189    {
190        self.workspace_mut().set_workdir(workdir.into());
191    }
192    /// if the workdir is set, set it to the given workdir
193    pub fn set_workdir_option<T>(&mut self, workdir: Option<T>)
194    where
195        std::path::PathBuf: From<T>,
196    {
197        workdir.map(|w| self.set_workdir(w));
198    }
199
200    pub fn set_port(&mut self, port: u16) {
201        self.network_mut().set_port(port);
202    }
203
204    pub fn set_log_level(&mut self, level: LogLevel) {
205        self.services_mut().tracing_mut().set_level(level);
206    }
207
208    fn builder_base() -> ConfigResult<ConfigBuilder> {
209        // initialize the builder
210        let mut builder = config::Config::builder();
211        // set defaults
212        builder = set_default(builder)?;
213        // add sources
214        builder = add_sources(builder);
215        // set overrides
216        builder = set_overrides(builder)?;
217        // return the builder
218        Ok(builder)
219    }
220}
221
222impl Default for Settings {
223    fn default() -> Self {
224        Self::from_mode(Mode::Debug)
225    }
226}
227
228impl core::fmt::Debug for Settings {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        f.write_str(&serde_json::to_string_pretty(&self).unwrap())
231    }
232}
233
234impl core::fmt::Display for Settings {
235    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
236        f.write_str(&serde_json::to_string(self).unwrap())
237    }
238}