Skip to main content

doido_controller/
config.rs

1//! Per-environment application configuration loaded from `config/<env>.yml`.
2//!
3//! [`Config`] is a trait so applications can supply their own backing store;
4//! [`YamlConfig`] is the default implementation that deserializes the YAML file
5//! for the environment reported by [`Environment::get_env`].
6
7use crate::environment::Environment;
8use serde::Deserialize;
9
10/// Re-exported so `config::LoggerConfig` resolves; the logger config lives in
11/// `doido-core` alongside the logger it drives.
12pub use doido_core::logger::LoggerConfig;
13
14/// Server bind settings. The listen address is the `bind` IP joined with `port`
15/// (e.g. `0.0.0.0:3000`).
16#[derive(Debug, Clone, Deserialize)]
17pub struct ServerConfig {
18    pub bind: String,
19    pub port: u16,
20}
21
22impl Default for ServerConfig {
23    fn default() -> Self {
24        Self {
25            bind: "0.0.0.0".to_string(),
26            port: 3000,
27        }
28    }
29}
30
31/// Application configuration. Used as a trait object (`Box<dyn Config>`) so the
32/// backing store can be swapped without touching call sites.
33pub trait Config: Send + Sync {
34    /// Server bind/port settings.
35    fn server(&self) -> &ServerConfig;
36    /// Logging settings.
37    fn logger(&self) -> &LoggerConfig;
38}
39
40/// File-based [`Config`] deserialized from `config/<env>.yml`.
41#[derive(Debug, Clone, Default, Deserialize)]
42pub struct YamlConfig {
43    #[serde(default)]
44    pub server: ServerConfig,
45    #[serde(default)]
46    pub logger: LoggerConfig,
47}
48
49impl Config for YamlConfig {
50    fn server(&self) -> &ServerConfig {
51        &self.server
52    }
53
54    fn logger(&self) -> &LoggerConfig {
55        &self.logger
56    }
57}
58
59impl YamlConfig {
60    /// Loads `config/<env>.yml` for the environment from [`Environment::get_env`].
61    pub fn load() -> std::io::Result<Self> {
62        Self::load_env(Environment::get_env())
63    }
64
65    /// Loads `config/<env>.yml` for a specific environment.
66    pub fn load_env(env: Environment) -> std::io::Result<Self> {
67        let path = format!("config/{}.yml", env.as_str());
68        let contents = std::fs::read_to_string(&path)?;
69        Self::from_yaml(&contents)
70    }
71
72    /// Parses a [`YamlConfig`] from a YAML string.
73    pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
74        serde_norway::from_str(yaml)
75            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
76    }
77}
78
79/// Loads the current environment's configuration as a trait object, falling
80/// back to [`Default`] values when the file is missing or invalid.
81pub fn load() -> Box<dyn Config> {
82    Box::new(YamlConfig::load().unwrap_or_default())
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn parses_logger_level() {
91        let config = YamlConfig::from_yaml("logger:\n  level: debug\n").unwrap();
92        assert_eq!(config.logger().level, "debug");
93        assert_eq!(
94            config.logger().directives(),
95            doido_core::logger::directives_for_level("debug")
96        );
97    }
98
99    #[test]
100    fn explicit_directives_override_level() {
101        let yaml = "logger:\n  level: info\n  directives: warn,my_app=debug\n";
102        let config = YamlConfig::from_yaml(yaml).unwrap();
103        assert_eq!(config.logger().directives(), "warn,my_app=debug");
104    }
105
106    #[test]
107    fn defaults_to_info_when_logger_section_absent() {
108        let config = YamlConfig::from_yaml("server:\n  bind: 0.0.0.0\n  port: 3000\n").unwrap();
109        assert_eq!(config.logger().level, "info");
110        assert!(config.logger().sql);
111        assert!(config.logger().file.is_none());
112        assert_eq!(
113            config.logger().directives(),
114            doido_core::logger::DEFAULT_DIRECTIVES
115        );
116    }
117}