support_kit/config/
configuration.rs

1use figment::{providers::Serialized, Figment, Provider};
2use secrecy::SecretString;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    Args, Color, DeploymentConfig, DeploymentControl, Environment, LoggerConfig, Logging,
7    LoggingConfig, NetworkConfig, ServiceConfig, ServiceName, Verbosity,
8};
9
10#[derive(Clone, Debug, Default, Serialize, Deserialize, bon::Builder)]
11pub struct Configuration {
12    #[serde(default)]
13    #[builder(default, into)]
14    pub logging: LoggingConfig,
15
16    #[serde(default)]
17    #[builder(default, into)]
18    pub verbosity: Verbosity,
19
20    #[serde(default)]
21    #[builder(default, into)]
22    pub color: Color,
23
24    #[serde(default)]
25    #[builder(default, into)]
26    pub server: NetworkConfig,
27
28    #[serde(default)]
29    #[builder(default, into)]
30    pub service: ServiceConfig,
31
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    #[builder(into)]
34    pub environment: Option<Environment>,
35
36    #[serde(default)]
37    #[builder(into)]
38    pub deployment: Option<DeploymentConfig>,
39
40    #[serde(default, skip_serializing)]
41    #[builder(default)]
42    pub secret: SecretString,
43}
44
45impl Configuration {
46    pub fn init_color(&self) {
47        self.color.init();
48    }
49
50    pub fn init_logging(&self) -> Vec<tracing_appender::non_blocking::WorkerGuard> {
51        Logging::initialize(self.clone())
52    }
53
54    pub async fn init_tls(&self) -> Option<rustls_acme::axum::AxumAcceptor> {
55        match &self.deployment {
56            Some(deployment) => DeploymentControl::initialize(deployment).await,
57            None => None,
58        }
59    }
60
61    pub fn loggers(&self) -> Vec<LoggerConfig> {
62        self.logging.loggers()
63    }
64
65    pub fn name(&self) -> ServiceName {
66        self.service.name()
67    }
68
69    pub fn address(&self) -> crate::Result<std::net::SocketAddr> {
70        self.server.address()
71    }
72
73    pub fn env_filter(&self) -> tracing_subscriber::EnvFilter {
74        let log_level = self.verbosity.to_string();
75        let maybe_env_filter = tracing_subscriber::EnvFilter::try_from_default_env();
76
77        if log_level.is_empty() {
78            maybe_env_filter.unwrap_or_default()
79        } else {
80            maybe_env_filter.unwrap_or_else(|_| log_level.into())
81        }
82    }
83}
84
85impl Provider for Configuration {
86    fn metadata(&self) -> figment::Metadata {
87        Default::default()
88    }
89
90    fn data(
91        &self,
92    ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
93        Figment::new()
94            .merge(Serialized::from(self.clone(), "default"))
95            .data()
96    }
97}
98
99impl PartialEq for Configuration {
100    fn eq(&self, other: &Self) -> bool {
101        self.logging == other.logging
102            && self.verbosity == other.verbosity
103            && self.color == other.color
104            && self.server == other.server
105            && self.service == other.service
106            && self.environment == other.environment
107            && self.deployment == other.deployment
108    }
109}
110
111impl From<Args> for Configuration {
112    fn from(args: Args) -> Self {
113        let Args {
114            color,
115            environment,
116            host,
117            name,
118            service_manager,
119            system,
120            port,
121            verbose,
122            ..
123        } = args.clone();
124
125        let verbosity_level = Verbosity::from_repr(verbose as usize);
126        let server = match (&host, port) {
127            (None, None) => None,
128            _ => Some(
129                NetworkConfig::builder()
130                    .maybe_host(host)
131                    .maybe_port(port)
132                    .build(),
133            ),
134        };
135
136        let service = ServiceConfig::builder()
137            .maybe_name(name)
138            .maybe_service_manager(service_manager)
139            .system(system)
140            .build();
141
142        Self::builder()
143            .maybe_verbosity(verbosity_level)
144            .maybe_server(server)
145            .maybe_environment(environment)
146            .color(color)
147            .service(service)
148            .build()
149    }
150}
151
152impl From<&Args> for Configuration {
153    fn from(args: &Args) -> Self {
154        args.clone().into()
155    }
156}
157
158mod sources {
159
160    #[test]
161    fn getting_config_from_json() -> Result<(), Box<dyn std::error::Error>> {
162        use super::Configuration;
163
164        use figment::{
165            providers::{Format, Json},
166            Figment,
167        };
168
169        figment::Jail::expect_with(|jail| {
170            jail.create_file(
171                "support-kit.json",
172                r#"{
173                    "server": { "host": "0.0.0.0" }
174                }"#,
175            )?;
176
177            let config: Configuration = Figment::new()
178                .merge(Json::file("support-kit.json"))
179                .extract()?;
180
181            assert_eq!(config, Configuration::builder().server("0.0.0.0").build());
182            Ok(())
183        });
184
185        Ok(())
186    }
187}
188
189#[test]
190fn verbosity_and_env_filter() -> Result<(), Box<dyn std::error::Error>> {
191    let config: Configuration = serde_json::from_str(
192        r#"
193        {
194            "verbosity": "debug"
195        }
196        "#,
197    )?;
198
199    assert_eq!(
200        config,
201        Configuration::builder().verbosity(Verbosity::Debug).build()
202    );
203
204    assert_eq!(config.env_filter().to_string(), "debug");
205
206    let config: Configuration = serde_json::from_str(r#"{}"#)?;
207
208    assert_eq!(config, Configuration::builder().build());
209    assert_eq!(config.env_filter().to_string(), "");
210
211    Ok(())
212}
213
214#[test]
215fn root_config_notation() -> Result<(), Box<dyn std::error::Error>> {
216    use crate::{LogLevel, LogRotation, LoggerConfig};
217
218    let config: Configuration = serde_json::from_str(
219        r#"
220        {
221            "logging": [
222                {
223                    "directory": "logs",
224                    "level": "warn",
225                    "name": "app.error"
226                },
227                {
228                    "directory": "logs",
229                    "level": { "min": "info", "max": "warn" },
230                    "name": "app",
231                    "rotation": "daily"
232                },
233                {
234                    "directory": "logs",
235                    "level": { "min": "trace", "max": "info" },
236                    "name": "app.debug",
237                    "rotation": "per-minute"
238                }
239            ]
240        }
241        "#,
242    )?;
243
244    assert_eq!(
245        config,
246        Configuration::builder()
247            .logging(bon::vec![
248                LoggerConfig::builder()
249                    .level(LogLevel::Warn)
250                    .file(("logs", "app.error"))
251                    .build(),
252                LoggerConfig::builder()
253                    .level(LogLevel::Info..LogLevel::Warn)
254                    .file(("logs", "app", LogRotation::Daily))
255                    .build(),
256                LoggerConfig::builder()
257                    .level(LogLevel::Trace..LogLevel::Info)
258                    .file(("logs", "app.debug", LogRotation::PerMinute))
259                    .build(),
260            ])
261            .build()
262    );
263
264    Ok(())
265}
266
267#[test]
268fn server_config() -> Result<(), Box<dyn std::error::Error>> {
269    let config: Configuration = serde_json::from_str(
270        r#"
271        {
272            "server": {
273                "host": "1.2.3.4",
274                "port": 8080
275            }
276        }
277        "#,
278    )?;
279
280    assert_eq!(
281        config,
282        Configuration::builder().server(("1.2.3.4", 8080)).build()
283    );
284
285    let config: Configuration = serde_json::from_str(
286        r#"
287        {
288            "server": {
289                "host": "127.0.0.1"
290            }
291        }
292        "#,
293    )?;
294
295    assert_eq!(config, Configuration::builder().server("127.0.0.1").build());
296
297    let config: Configuration = serde_json::from_str(
298        r#"
299        {
300            "server": {
301                "port": 22
302            }
303        }
304        "#,
305    )?;
306
307    assert_eq!(
308        config,
309        Configuration::builder().server(("0.0.0.0", 22)).build()
310    );
311
312    let config: Configuration = serde_json::from_str(
313        r#"
314        {
315        }
316        "#,
317    )?;
318
319    assert_eq!(
320        config,
321        Configuration::builder().server(("0.0.0.0", 80)).build()
322    );
323
324    Ok(())
325}
326
327#[test]
328fn service_config() -> Result<(), Box<dyn std::error::Error>> {
329    let config: Configuration = serde_json::from_str(
330        r#"
331        {
332            "service": {
333                "name": "consumer-package"
334            }
335        }
336        "#,
337    )?;
338
339    assert_eq!(
340        config,
341        Configuration::builder().service("consumer-package").build()
342    );
343
344    let config: Configuration = serde_json::from_str(
345        r#"
346        {
347            "service": {
348                "name": "custom-name"
349            }
350        }
351        "#,
352    )?;
353
354    assert_eq!(
355        config,
356        Configuration::builder().service("custom-name").build()
357    );
358
359    let config: Configuration = serde_json::from_str(
360        r#"
361        {
362            "service": {
363                "system": true
364            }
365        }
366        "#,
367    )?;
368
369    assert_eq!(
370        config,
371        Configuration::builder()
372            .service(ServiceConfig::builder().system(true).build())
373            .build()
374    );
375
376    let config: Configuration = serde_json::from_str(
377        r#"
378        {
379            "service": {
380                "system": false
381            }
382        }
383        "#,
384    )?;
385
386    assert_eq!(
387        config,
388        Configuration::builder()
389            .service(ServiceConfig::builder().system(false).build())
390            .build()
391    );
392
393    Ok(())
394}
395
396#[test]
397fn color_config() -> Result<(), Box<dyn std::error::Error>> {
398    let config: Configuration = serde_json::from_str(
399        r#"
400        {
401            "color": "auto"
402        }
403        "#,
404    )?;
405
406    assert_eq!(config, Configuration::builder().color(Color::Auto).build());
407
408    let config: Configuration = serde_json::from_str(
409        r#"
410        {
411            "color": "always"
412        }
413        "#,
414    )?;
415
416    assert_eq!(
417        config,
418        Configuration::builder().color(Color::Always).build()
419    );
420
421    let config: Configuration = serde_json::from_str(
422        r#"
423        {
424            "color": "never"
425        }
426        "#,
427    )?;
428
429    assert_eq!(config, Configuration::builder().color(Color::Never).build());
430
431    Ok(())
432}
433
434#[test]
435fn environment_config() -> Result<(), Box<dyn std::error::Error>> {
436    let config: Configuration = serde_json::from_str(
437        r#"
438        {
439            "environment": "development"
440        }
441        "#,
442    )?;
443
444    assert_eq!(
445        config,
446        Configuration::builder()
447            .environment(Environment::Development)
448            .build()
449    );
450
451    let config: Configuration = serde_json::from_str(
452        r#"
453        {
454            "environment": "production"
455        }
456        "#,
457    )?;
458
459    assert_eq!(
460        config,
461        Configuration::builder()
462            .environment(Environment::Production)
463            .build()
464    );
465
466    let config: Configuration = serde_json::from_str(
467        r#"
468        {
469            "environment": "test"
470        }
471        "#,
472    )?;
473
474    assert_eq!(
475        config,
476        Configuration::builder()
477            .environment(Environment::Test)
478            .build()
479    );
480
481    Ok(())
482}