zirv_queue/config/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use std::sync::OnceLock;

use dotenvy::dotenv;
use serde::Deserialize;

static GLOBAL_CONFIG: OnceLock<Config> = OnceLock::new();

#[derive(Deserialize, Debug)]
pub struct Config {
    pub database_url: String,

    #[serde(default = "default_max_concurrent_jobs")]
    pub max_concurrent_jobs: usize,

    #[serde(default = "default_tick_rate_ms")]
    pub tick_rate_ms: u64,

    #[serde(default = "default_max_retry_attempts")]
    pub max_retry_attempts: i32,

    #[serde(default = "default_retry_interval_ms")]
    pub retry_interval_ms: i64,

    // You could include other fields (poll intervals, etc.) as needed.
}

fn default_max_concurrent_jobs() -> usize {
    4
}

fn default_tick_rate_ms() -> u64 {
    1000
}

fn default_max_retry_attempts() -> i32 {
    3
}

fn default_retry_interval_ms() -> i64 {
    300_000
}

impl Config {
    pub fn init() -> Result<(), Box<dyn std::error::Error>> {
        dotenv().ok();

        let config = envy::from_env::<Config>()?;

        GLOBAL_CONFIG
            .set(config)
            .map_err(|_| "Failed to set global config")?;

        Ok(())
    }

    pub fn from_env() -> Self {
        dotenv().ok();
        envy::from_env::<Config>().expect("Failed to read configuration from environment")
    }

    pub fn get_config() -> &'static Config {
        GLOBAL_CONFIG.get_or_init(Config::from_env)
    }
}

// Example .env
// DATABASE_URL=postgres://user:pass@localhost/db
// MAX_CONCURRENT_JOBS=8
// TICK_RATE_MS=500

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;

    /// Utility function to clear relevant environment variables before each test
    fn clear_env_vars() {
        env::remove_var("DATABASE_URL");
        env::remove_var("MAX_CONCURRENT_JOBS");
        env::remove_var("TICK_RATE_MS");
    }

    #[test]
    fn test_defaults_when_env_not_set() {
        // Clear any existing vars to force defaults
        clear_env_vars();

        // Attempt to load config
        // This will panic if DATABASE_URL is missing, because that is required in your code
        // If you truly want to test "missing DB URL" scenario, you could wrap in a catch_unwind
        // or check for a custom error message. For now, we’ll show a normal usage pattern.
        env::set_var("DATABASE_URL", "mysql://root:password@localhost/db");

        let config = Config::from_env();

        // The DB URL is the one we set
        assert_eq!(config.database_url, "mysql://root:password@localhost/db");

        // Default values
        // (The code sets 4 for max_concurrent_jobs by default and 1000 for tick_rate_ms)
        assert_eq!(config.max_concurrent_jobs, 4);
        assert_eq!(config.tick_rate_ms, 1000);
    }

    #[test]
    fn test_custom_env_values() {
        clear_env_vars();

        // Set all environment variables
        env::set_var("DATABASE_URL", "postgres://user:pass@localhost/custom_db");
        env::set_var("MAX_CONCURRENT_JOBS", "8");
        env::set_var("TICK_RATE_MS", "500");

        let config = Config::from_env();

        // Check if they are loaded correctly
        assert_eq!(config.database_url, "postgres://user:pass@localhost/custom_db");
        assert_eq!(config.max_concurrent_jobs, 8);
        assert_eq!(config.tick_rate_ms, 500);
    }

    #[test]
    #[should_panic(expected = "Failed to read configuration from environment")]
    fn test_missing_db_url_panics() {
        clear_env_vars();
        // We do NOT set DATABASE_URL here, so from_env should panic
        let _config = Config::from_env();
    }
}