Skip to main content

doido_model/
config.rs

1//! Per-environment database configuration loaded from `config/<env>.yml`.
2//!
3//! Mirrors `doido-controller`'s config: [`Config`] is a trait so applications
4//! can supply their own backing store, and [`YamlConfig`] is the default
5//! implementation that deserializes the `database` section of the YAML file for
6//! the environment reported by [`Environment::get_env`].
7
8use crate::environment::Environment;
9use serde::Deserialize;
10
11/// Re-exported so `config::LoggerConfig` resolves; the logger config lives in
12/// `doido-core` alongside the logger it drives. The model layer reads it for the
13/// `sql` toggle (whether sea-orm logs each statement).
14pub use doido_core::logger::LoggerConfig;
15
16/// Database connection settings.
17#[derive(Debug, Clone, Deserialize)]
18pub struct DatabaseConfig {
19    /// Connection URL, e.g. `postgres://localhost/my_app_development` or
20    /// `sqlite://db/development.db`.
21    pub url: String,
22}
23
24impl Default for DatabaseConfig {
25    fn default() -> Self {
26        Self {
27            url: "sqlite://db/development.db".to_string(),
28        }
29    }
30}
31
32/// Model-layer configuration. Used as a trait object (`Box<dyn Config>`) so the
33/// backing store can be swapped without touching call sites.
34pub trait Config: Send + Sync {
35    /// Database connection settings.
36    fn database(&self) -> &DatabaseConfig;
37    /// Logging settings; the model layer reads the `sql` toggle.
38    fn logger(&self) -> &LoggerConfig;
39}
40
41/// File-based [`Config`] deserialized from the `database` and `logger` sections
42/// of `config/<env>.yml`. Other sections (e.g. `server`) are ignored.
43#[derive(Debug, Clone, Default, Deserialize)]
44pub struct YamlConfig {
45    #[serde(default)]
46    pub database: DatabaseConfig,
47    #[serde(default)]
48    pub logger: LoggerConfig,
49}
50
51impl Config for YamlConfig {
52    fn database(&self) -> &DatabaseConfig {
53        &self.database
54    }
55
56    fn logger(&self) -> &LoggerConfig {
57        &self.logger
58    }
59}
60
61impl YamlConfig {
62    /// Loads `config/<env>.yml` for the environment from [`Environment::get_env`].
63    pub fn load() -> std::io::Result<Self> {
64        Self::load_env(Environment::get_env())
65    }
66
67    /// Loads `config/<env>.yml` for a specific environment.
68    pub fn load_env(env: Environment) -> std::io::Result<Self> {
69        let path = format!("config/{}.yml", env.as_str());
70        let contents = std::fs::read_to_string(&path)?;
71        Self::from_yaml(&contents)
72    }
73
74    /// Parses a [`YamlConfig`] from a YAML string.
75    pub fn from_yaml(yaml: &str) -> std::io::Result<Self> {
76        serde_norway::from_str(yaml)
77            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
78    }
79}
80
81/// Loads the current environment's configuration as a trait object, falling
82/// back to [`Default`] values when the file is missing or invalid.
83pub fn load() -> Box<dyn Config> {
84    Box::new(YamlConfig::load().unwrap_or_default())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn parses_database_url_and_ignores_other_sections() {
93        let yaml = "server:\n  bind: 0.0.0.0\n  port: 3000\ndatabase:\n  url: postgres://localhost/app_development\n";
94        let config = YamlConfig::from_yaml(yaml).unwrap();
95        assert_eq!(
96            config.database().url,
97            "postgres://localhost/app_development"
98        );
99    }
100
101    #[test]
102    fn defaults_when_database_section_absent() {
103        let config = YamlConfig::from_yaml("server:\n  port: 3000\n").unwrap();
104        assert_eq!(config.database().url, "sqlite://db/development.db");
105    }
106}