by_loco/config.rs
1//! # Configuration Management
2//!
3//! This module defines the configuration structures and functions to manage and
4//! load configuration settings for the application.
5
6/***
7=============
8CONTRIBUTORS:
9=============
10
11Here's a check list when adding configuration values:
12
13* Add the new configuration piece
14* Document each field with the appropriate rustdoc comment
15* Go to `starters/`, evaluate which starter needs a configuration update, and update as needed.
16 apply a YAML comment above the new field or section with explanation and possible values.
17
18Notes:
19* Configuration is feature-dependent: with and without database
20* Configuration is "stage" dependent: development, test, production
21* We typically provide best practice values for development and test, but by-design we do not provide default values for production
22
23***/
24use std::{
25 collections::BTreeMap,
26 fs,
27 path::{Path, PathBuf},
28 sync::OnceLock,
29};
30
31use serde::{Deserialize, Serialize};
32use serde_json::json;
33use tracing::info;
34
35use crate::{controller::middleware, environment::Environment, logger, scheduler, Error, Result};
36
37static DEFAULT_FOLDER: OnceLock<PathBuf> = OnceLock::new();
38
39fn get_default_folder() -> &'static PathBuf {
40 DEFAULT_FOLDER.get_or_init(|| PathBuf::from("config"))
41}
42/// Main application configuration structure.
43///
44/// This struct encapsulates various configuration settings. The configuration
45/// can be customized through YAML files for different environments.
46#[derive(Debug, Clone, Deserialize, Serialize)]
47pub struct Config {
48 pub logger: Logger,
49 pub server: Server,
50 #[cfg(feature = "with-db")]
51 pub database: Database,
52 #[serde(default)]
53 pub cache: CacheConfig,
54 pub queue: Option<QueueConfig>,
55 pub auth: Option<Auth>,
56 #[serde(default)]
57 pub workers: Workers,
58 pub mailer: Option<Mailer>,
59 pub initializers: Option<Initializers>,
60
61 /// Custom app settings
62 ///
63 /// Example:
64 /// ```yaml
65 /// settings:
66 /// allow_list:
67 /// - google.com
68 /// - apple.com
69 /// ```
70 /// And then optionally deserialize it to your own `Settings` type by
71 /// accessing `ctx.config.settings`.
72 #[serde(default)]
73 pub settings: Option<serde_json::Value>,
74
75 pub scheduler: Option<scheduler::Config>,
76}
77
78/// Logger configuration
79///
80/// The Loco logging stack is built on `tracing`, using a carefuly
81/// crafted stack of filters and subscribers. We filter out noise,
82/// apply a log level across your app, and sort out back traces for
83/// a great developer experience.
84///
85/// Example (development):
86/// ```yaml
87/// # config/development.yaml
88/// logger:
89/// enable: true
90/// pretty_backtrace: true
91/// level: debug
92/// format: compact
93/// ```
94#[derive(Debug, Clone, Deserialize, Serialize, Default)]
95pub struct Logger {
96 /// Enable log write to stdout
97 pub enable: bool,
98
99 /// Enable nice display of backtraces, in development this should be on.
100 /// Turn it off in performance sensitive production deployments.
101 #[serde(default)]
102 pub pretty_backtrace: bool,
103
104 /// Set the logger level.
105 ///
106 /// * options: `trace` | `debug` | `info` | `warn` | `error`
107 pub level: logger::LogLevel,
108
109 /// Set the logger format.
110 ///
111 /// * options: `compact` | `pretty` | `json`
112 pub format: logger::Format,
113
114 /// Override our custom tracing filter.
115 ///
116 /// Set this to your own filter if you want to see traces from internal
117 /// libraries. See more [here](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
118 pub override_filter: Option<String>,
119
120 /// Set this if you want to write log to file
121 pub file_appender: Option<LoggerFileAppender>,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize, Default)]
125pub struct LoggerFileAppender {
126 /// Enable logger file appender
127 pub enable: bool,
128
129 /// Enable write log to file non-blocking
130 #[serde(default)]
131 pub non_blocking: bool,
132
133 /// Set the logger file appender level.
134 ///
135 /// * options: `trace` | `debug` | `info` | `warn` | `error`
136 pub level: logger::LogLevel,
137
138 /// Set the logger file appender format.
139 ///
140 /// * options: `compact` | `pretty` | `json`
141 pub format: logger::Format,
142
143 /// Set the logger file appender rotation.
144 pub rotation: logger::Rotation,
145
146 /// Set the logger file appender dir
147 ///
148 /// default is `./logs`
149 pub dir: Option<String>,
150
151 /// Set log filename prefix
152 pub filename_prefix: Option<String>,
153
154 /// Set log filename suffix
155 pub filename_suffix: Option<String>,
156
157 /// Set the logger file appender keep max log files.
158 pub max_log_files: usize,
159}
160
161/// Database configuration
162///
163/// Configures the [SeaORM](https://www.sea-ql.org/SeaORM/) connection and pool, as well as Loco's additional DB
164/// management utils such as `auto_migrate`, `truncate` and `recreate`.
165///
166/// Example (development):
167/// ```yaml
168/// # config/development.yaml
169/// database:
170/// uri: {{ get_env(name="DATABASE_URL", default="...") }}
171/// enable_logging: true
172/// connect_timeout: 500
173/// idle_timeout: 500
174/// min_connections: 1
175/// max_connections: 1
176/// auto_migrate: true
177/// dangerously_truncate: false
178/// dangerously_recreate: false
179/// ```
180#[derive(Debug, Clone, Deserialize, Serialize)]
181#[allow(clippy::struct_excessive_bools)]
182pub struct Database {
183 /// The URI for connecting to the database. For example:
184 /// * Postgres: `postgres://root:12341234@localhost:5432/myapp_development`
185 /// * Sqlite: `sqlite://db.sqlite?mode=rwc`
186 pub uri: String,
187
188 /// Enable `SQLx` statement logging
189 pub enable_logging: bool,
190
191 /// Minimum number of connections for a pool
192 pub min_connections: u32,
193
194 /// Maximum number of connections for a pool
195 pub max_connections: u32,
196
197 /// Set the timeout duration when acquiring a connection
198 pub connect_timeout: u64,
199
200 /// Set the idle duration before closing a connection
201 pub idle_timeout: u64,
202
203 /// Set the timeout for acquiring a connection
204 pub acquire_timeout: Option<u64>,
205
206 /// Run migration up when application loads. It is recommended to turn it on
207 /// in development. In production keep it off, and explicitly migrate your
208 /// database every time you need.
209 #[serde(default)]
210 pub auto_migrate: bool,
211
212 /// Truncate database when application loads. It will delete data from your
213 /// tables. Commonly used in `test`.
214 #[serde(default)]
215 pub dangerously_truncate: bool,
216
217 /// Recreate schema when application loads. Use it when you want to reset
218 /// your database *and* structure (drop), this also deletes all of the data.
219 /// Useful when you're just sketching out your project and trying out
220 /// various things in development.
221 #[serde(default)]
222 pub dangerously_recreate: bool,
223
224 // Execute query after initializing the DB
225 /// for e.g. this can be used to confiure PRAGMAs for `SQLite` where you can pass all values as a string.
226 /// Default values are:
227 ///
228 /// PRAGMA `foreign_keys` = ON;
229 ///
230 /// PRAGMA `journal_mode` = WAL;
231 ///
232 /// PRAGMA `synchronous` = NORMAL;
233 ///
234 /// PRAGMA `mmap_size` = 134217728;
235 ///
236 /// PRAGMA `journal_size_limit` = 67108864;
237 ///
238 /// PRAGMA `cache_size` = 2000;
239 ///
240 /// PRAGMA `busy_timeout` = 5000;
241 pub run_on_start: Option<String>,
242}
243
244/// Cache configurations for the application
245#[derive(Debug, Clone, Default, Deserialize, Serialize)]
246#[serde(tag = "kind")]
247pub enum CacheConfig {
248 #[cfg(feature = "cache_inmem")]
249 /// In-memory cache
250 InMem(InMemCacheConfig),
251 #[cfg(feature = "cache_redis")]
252 /// Redis cache
253 Redis(RedisCacheConfig),
254 /// Null cache
255 #[default]
256 Null,
257}
258
259#[derive(Debug, Clone, Deserialize, Serialize)]
260pub struct InMemCacheConfig {
261 #[serde(default = "cache_in_mem_max_capacity")]
262 pub max_capacity: u64,
263}
264
265fn cache_in_mem_max_capacity() -> u64 {
266 32 * 1024 * 1024
267}
268
269#[derive(Debug, Clone, Deserialize, Serialize)]
270pub struct RedisCacheConfig {
271 pub uri: String,
272 /// Sets the maximum number of connections managed by the pool.
273 pub max_size: u32,
274}
275
276#[derive(Debug, Clone, Deserialize, Serialize)]
277#[serde(tag = "kind")]
278pub enum QueueConfig {
279 /// Redis queue
280 Redis(RedisQueueConfig),
281 /// Postgres queue
282 Postgres(PostgresQueueConfig),
283 /// Sqlite queue
284 Sqlite(SqliteQueueConfig),
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
288pub struct RedisQueueConfig {
289 pub uri: String,
290 #[serde(default)]
291 pub dangerously_flush: bool,
292
293 /// Custom queue names declaration. Useful to model priority queues.
294 /// First queue in list is more important.
295 pub queues: Option<Vec<String>>,
296
297 #[serde(default = "num_workers")]
298 pub num_workers: u32,
299}
300
301#[derive(Debug, Clone, Deserialize, Serialize)]
302pub struct PostgresQueueConfig {
303 pub uri: String,
304
305 #[serde(default)]
306 pub dangerously_flush: bool,
307
308 #[serde(default)]
309 pub enable_logging: bool,
310
311 #[serde(default = "db_max_conn")]
312 pub max_connections: u32,
313
314 #[serde(default = "db_min_conn")]
315 pub min_connections: u32,
316
317 #[serde(default = "db_connect_timeout")]
318 pub connect_timeout: u64,
319
320 #[serde(default = "db_idle_timeout")]
321 pub idle_timeout: u64,
322
323 #[serde(default = "pgq_poll_interval")]
324 pub poll_interval_sec: u32,
325
326 #[serde(default = "num_workers")]
327 pub num_workers: u32,
328}
329
330#[derive(Debug, Clone, Deserialize, Serialize)]
331pub struct SqliteQueueConfig {
332 pub uri: String,
333
334 #[serde(default)]
335 pub dangerously_flush: bool,
336
337 #[serde(default)]
338 pub enable_logging: bool,
339
340 #[serde(default = "db_max_conn")]
341 pub max_connections: u32,
342
343 #[serde(default = "db_min_conn")]
344 pub min_connections: u32,
345
346 #[serde(default = "db_connect_timeout")]
347 pub connect_timeout: u64,
348
349 #[serde(default = "db_idle_timeout")]
350 pub idle_timeout: u64,
351
352 #[serde(default = "sqlt_poll_interval")]
353 pub poll_interval_sec: u32,
354
355 #[serde(default = "num_workers")]
356 pub num_workers: u32,
357}
358
359fn db_min_conn() -> u32 {
360 1
361}
362
363fn db_max_conn() -> u32 {
364 20
365}
366
367fn db_connect_timeout() -> u64 {
368 500
369}
370
371fn db_idle_timeout() -> u64 {
372 500
373}
374
375fn pgq_poll_interval() -> u32 {
376 1
377}
378
379fn sqlt_poll_interval() -> u32 {
380 1
381}
382
383fn num_workers() -> u32 {
384 2
385}
386
387/// User authentication configuration.
388///
389/// Example (development):
390/// ```yaml
391/// # config/development.yaml
392/// auth:
393/// jwt:
394/// secret: <your secret>
395/// expiration: 604800 # 7 days
396/// ```
397#[derive(Debug, Clone, Deserialize, Serialize)]
398pub struct Auth {
399 /// JWT authentication config
400 pub jwt: Option<JWT>,
401}
402
403/// JWT configuration structure.
404#[derive(Debug, Clone, Deserialize, Serialize)]
405pub struct JWT {
406 /// The location where JWT tokens are expected to be found during
407 /// authentication.
408 pub location: Option<JWTLocation>,
409 /// The secret key For JWT token
410 pub secret: String,
411 /// The expiration time for authentication tokens
412 pub expiration: u64,
413}
414
415/// Defines the authentication mechanism for middleware.
416///
417/// This enum represents various ways to authenticate using JSON Web Tokens
418/// (JWT) within middleware.
419#[derive(Debug, Clone, Deserialize, Serialize)]
420#[serde(tag = "from")]
421pub enum JWTLocation {
422 /// Authenticate using a Bearer token.
423 Bearer,
424 /// Authenticate using a token passed as a query parameter.
425 Query { name: String },
426 /// Authenticate using a token stored in a cookie.
427 Cookie { name: String },
428}
429
430/// Server configuration structure.
431///
432/// Example (development):
433/// ```yaml
434/// # config/development.yaml
435/// server:
436/// port: {{ get_env(name="NODE_PORT", default=5150) }}
437/// host: http://localhost
438/// middlewares:
439/// limit_payload:
440/// enable: true
441/// body_limit: 5mb
442/// logger:
443/// enable: true
444/// catch_panic:
445/// enable: true
446/// timeout_request:
447/// enable: true
448/// timeout: 5000
449/// compression:
450/// enable: true
451/// cors:
452/// enable: true
453/// ```
454#[derive(Debug, Clone, Deserialize, Serialize)]
455pub struct Server {
456 /// The address on which the server should listen on for incoming
457 /// connections.
458 #[serde(default = "default_binding")]
459 pub binding: String,
460 /// The port on which the server should listen for incoming connections.
461 pub port: i32,
462 /// The webserver host
463 pub host: String,
464 /// Identify via the `Server` header
465 pub ident: Option<String>,
466 /// Middleware configurations for the server, including payload limits,
467 /// logging, and error handling.
468 #[serde(default)]
469 pub middlewares: middleware::Config,
470}
471
472fn default_binding() -> String {
473 "localhost".to_string()
474}
475
476impl Server {
477 #[must_use]
478 pub fn full_url(&self) -> String {
479 format!("{}:{}", self.host, self.port)
480 }
481}
482/// Background worker configuration
483/// Example (development):
484/// ```yaml
485/// # config/development.yaml
486/// workers:
487/// mode: BackgroundQueue
488/// ```
489#[derive(Debug, Clone, Deserialize, Serialize, Default)]
490pub struct Workers {
491 /// Toggle between different worker modes
492 pub mode: WorkerMode,
493}
494
495/// Worker mode configuration
496#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
497pub enum WorkerMode {
498 /// Workers operate asynchronously in the background, processing queued
499 /// tasks. **Requires a Redis connection**.
500 #[default]
501 BackgroundQueue,
502 /// Workers operate in the foreground in the same process and block until
503 /// tasks are completed.
504 ForegroundBlocking,
505 /// Workers operate asynchronously in the background, processing tasks with
506 /// async capabilities in the same process.
507 BackgroundAsync,
508}
509
510/// Mailer configuration
511///
512/// Example (development), to capture mails with something like [mailcrab](https://github.com/tweedegolf/mailcrab):
513/// ```yaml
514/// # config/development.yaml
515/// mailer:
516/// smtp:
517/// enable: true
518/// host: localhost
519/// port: 1025
520/// secure: false
521/// ```
522#[derive(Debug, Clone, Deserialize, Serialize)]
523pub struct Mailer {
524 pub smtp: Option<SmtpMailer>,
525
526 #[serde(default)]
527 pub stub: bool,
528}
529
530/// Initializers configuration
531///
532/// Example (development): To configure settings for oauth2 or custom view
533/// engine
534/// ```yaml
535/// # config/development.yaml
536/// initializers:
537/// oauth2:
538/// authorization_code: # Authorization code grant type
539/// - client_identifier: google # Identifier for the `OAuth2` provider.
540/// Replace 'google' with your provider's name if different, must be
541/// unique within the oauth2 config. ... # other fields
542pub type Initializers = BTreeMap<String, serde_json::Value>;
543
544/// SMTP mailer configuration structure.
545#[derive(Debug, Clone, Deserialize, Serialize)]
546pub struct SmtpMailer {
547 pub enable: bool,
548 /// SMTP host. for example: localhost, smtp.gmail.com etc.
549 pub host: String,
550 /// SMTP port/
551 pub port: u16,
552 /// Enable TLS
553 pub secure: bool,
554 /// Auth SMTP server
555 pub auth: Option<MailerAuth>,
556 /// Optional EHLO client ID instead of hostname
557 pub hello_name: Option<String>,
558}
559
560/// Authentication details for the mailer
561#[derive(Debug, Clone, Deserialize, Serialize)]
562pub struct MailerAuth {
563 /// User
564 pub user: String,
565 /// Password
566 pub password: String,
567}
568
569impl Config {
570 /// Creates a new configuration instance based on the specified environment.
571 ///
572 /// # Errors
573 ///
574 /// Returns error when could not convert the give path to
575 /// [`Config`] struct.
576 ///
577 /// # Example
578 ///
579 /// ```rust
580 /// use loco_rs::{
581 /// config::Config,
582 /// environment::Environment,
583 /// };
584 ///
585 /// #[tokio::main]
586 /// async fn load(environment: &Environment) -> Config {
587 /// Config::new(environment).expect("configuration loading")
588 /// }
589 pub fn new(env: &Environment) -> Result<Self> {
590 let config = Self::from_folder(env, get_default_folder().as_path())?;
591 Ok(config)
592 }
593
594 /// Loads configuration settings from a folder for the specified
595 /// environment.
596 ///
597 /// # Errors
598 /// Returns error when could not convert the give path to
599 /// [`Config`] struct.
600 ///
601 /// # Example
602 ///
603 /// ```rust
604 /// use loco_rs::{
605 /// config::Config,
606 /// environment::Environment,
607 /// };
608 /// use std::path::PathBuf;
609 ///
610 /// #[tokio::main]
611 /// async fn load(environment: &Environment) -> Config{
612 /// Config::from_folder(environment, &PathBuf::from("config")).expect("configuration loading")
613 /// }
614 pub fn from_folder(env: &Environment, path: &Path) -> Result<Self> {
615 // by order of precedence
616 let files = [
617 path.join(format!("{env}.local.yaml")),
618 path.join(format!("{env}.yaml")),
619 ];
620
621 let selected_path = files.iter().find(|p| p.exists()).ok_or_else(|| {
622 Error::Message(format!(
623 "no configuration file found in folder: {}",
624 path.display()
625 ))
626 })?;
627
628 info!(selected_path =? selected_path, "loading environment from");
629
630 let content = fs::read_to_string(selected_path)?;
631 let rendered = crate::tera::render_string(&content, &json!({}))?;
632
633 serde_yaml::from_str(&rendered)
634 .map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))
635 }
636
637 /// Get a reference to the JWT configuration.
638 ///
639 /// # Errors
640 /// return an error when jwt token not configured
641 pub fn get_jwt_config(&self) -> Result<&JWT> {
642 self.auth
643 .as_ref()
644 .and_then(|auth| auth.jwt.as_ref())
645 .map_or_else(
646 || Err(Error::Any("no JWT config found".to_string().into())),
647 Ok,
648 )
649 }
650}
651
652impl std::fmt::Display for Config {
653 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
654 let content = serde_yaml::to_string(self).unwrap_or_default();
655 write!(f, "{content}")
656 }
657}