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}