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}