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 doido_core::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/// Opt-in CORS settings (spec 07 `[middleware.cors]`). Disabled unless
32/// `enabled: true`; an empty `allowed_origins`/`allowed_methods` leaves that
33/// dimension unset on the layer. Use `"*"` in `allowed_origins` for any origin.
34#[derive(Debug, Clone, Default, Deserialize)]
35pub struct CorsConfig {
36    #[serde(default)]
37    pub enabled: bool,
38    #[serde(default)]
39    pub allowed_origins: Vec<String>,
40    #[serde(default)]
41    pub allowed_methods: Vec<String>,
42}
43
44/// Opt-in middleware settings (spec 07 `[middleware]`).
45#[derive(Debug, Clone, Default, Deserialize)]
46pub struct MiddlewareConfig {
47    #[serde(default)]
48    pub cors: CorsConfig,
49}
50
51/// Application configuration. Used as a trait object (`Box<dyn Config>`) so the
52/// backing store can be swapped without touching call sites.
53pub trait Config: Send + Sync {
54    /// Server bind/port settings.
55    fn server(&self) -> &ServerConfig;
56    /// Logging settings.
57    fn logger(&self) -> &LoggerConfig;
58    /// Opt-in middleware settings (CORS, …).
59    fn middleware(&self) -> &MiddlewareConfig;
60}
61
62/// File-based [`Config`] deserialized from `config/<env>.yml`.
63#[derive(Debug, Clone, Default, Deserialize)]
64pub struct YamlConfig {
65    #[serde(default)]
66    pub server: ServerConfig,
67    #[serde(default)]
68    pub logger: LoggerConfig,
69    #[serde(default)]
70    pub middleware: MiddlewareConfig,
71}
72
73impl Config for YamlConfig {
74    fn server(&self) -> &ServerConfig {
75        &self.server
76    }
77
78    fn logger(&self) -> &LoggerConfig {
79        &self.logger
80    }
81
82    fn middleware(&self) -> &MiddlewareConfig {
83        &self.middleware
84    }
85}
86
87impl YamlConfig {
88    /// Loads `config/<env>.yml` for the environment from [`Environment::get_env`].
89    pub fn load() -> std::io::Result<Self> {
90        Self::load_env(Environment::get_env())
91    }
92
93    /// Loads `config/<env>.yml` for a specific environment.
94    pub fn load_env(env: Environment) -> std::io::Result<Self> {
95        let path = format!("config/{}.yml", env.as_str());
96        let contents = std::fs::read_to_string(&path)?;
97        Self::from_yaml(&contents)
98    }
99
100    /// Parses a [`YamlConfig`] from a YAML string.
101    pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
102        serde_norway::from_str(yaml)
103            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
104    }
105}
106
107/// Loads the current environment's configuration as a trait object, falling
108/// back to [`Default`] values when the file is missing or invalid.
109pub fn load() -> Box<dyn Config> {
110    Box::new(YamlConfig::load().unwrap_or_default())
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn parses_logger_level() {
119        let config = YamlConfig::from_yaml("logger:\n  level: debug\n").unwrap();
120        assert_eq!(config.logger().level, "debug");
121        assert_eq!(
122            config.logger().directives(),
123            doido_core::logger::directives_for_level("debug")
124        );
125    }
126
127    #[test]
128    fn explicit_directives_override_level() {
129        let yaml = "logger:\n  level: info\n  directives: warn,my_app=debug\n";
130        let config = YamlConfig::from_yaml(yaml).unwrap();
131        assert_eq!(config.logger().directives(), "warn,my_app=debug");
132    }
133
134    #[test]
135    fn defaults_to_info_when_logger_section_absent() {
136        let config = YamlConfig::from_yaml("server:\n  bind: 0.0.0.0\n  port: 3000\n").unwrap();
137        assert_eq!(config.logger().level, "info");
138        assert!(config.logger().sql);
139        assert!(config.logger().file.is_none());
140        assert_eq!(
141            config.logger().directives(),
142            doido_core::logger::DEFAULT_DIRECTIVES
143        );
144    }
145}