Skip to main content

yauth_migration/
config.rs

1//! `yauth.toml` configuration file support.
2//!
3//! This file intentionally has no `database_url` field -- database URLs
4//! come from environment variables only. `yauth.toml` is always safe to commit.
5
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// Top-level yauth.toml configuration.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct YAuthConfig {
12    pub migration: MigrationConfig,
13    pub plugins: PluginsConfig,
14}
15
16/// Migration-related settings.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct MigrationConfig {
19    /// ORM to generate migration files for.
20    pub orm: crate::Orm,
21    /// SQL dialect: "postgres", "mysql", or "sqlite".
22    pub dialect: String,
23    /// Directory where migration files are written.
24    #[serde(default = "default_migrations_dir")]
25    pub migrations_dir: String,
26    /// PostgreSQL schema name (optional, default "public").
27    #[serde(default)]
28    pub schema: Option<String>,
29    /// Table name prefix (default "yauth_").
30    #[serde(default = "default_table_prefix")]
31    pub table_prefix: String,
32}
33
34/// Plugin configuration.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PluginsConfig {
37    /// List of enabled plugin names.
38    pub enabled: Vec<String>,
39}
40
41fn default_migrations_dir() -> String {
42    "migrations".to_string()
43}
44
45fn default_table_prefix() -> String {
46    "yauth_".to_string()
47}
48
49impl YAuthConfig {
50    /// Load config from a TOML file.
51    pub fn load(path: &Path) -> Result<Self, ConfigError> {
52        let contents = std::fs::read_to_string(path)
53            .map_err(|e| ConfigError::Io(path.display().to_string(), e))?;
54        let config: Self = toml::from_str(&contents)
55            .map_err(|e| ConfigError::Parse(path.display().to_string(), e))?;
56        config.validate()?;
57        Ok(config)
58    }
59
60    /// Save config to a TOML file.
61    pub fn save(&self, path: &Path) -> Result<(), ConfigError> {
62        let contents =
63            toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
64        std::fs::write(path, contents)
65            .map_err(|e| ConfigError::Io(path.display().to_string(), e))?;
66        Ok(())
67    }
68
69    /// Validate config values.
70    pub fn validate(&self) -> Result<(), ConfigError> {
71        // Validate dialect
72        if self.migration.dialect.parse::<crate::Dialect>().is_err() {
73            return Err(ConfigError::InvalidValue(format!(
74                "unknown dialect: '{}'",
75                self.migration.dialect
76            )));
77        }
78
79        // Validate plugins
80        for plugin in &self.plugins.enabled {
81            if !crate::is_known_plugin(plugin) {
82                return Err(ConfigError::InvalidValue(format!(
83                    "unknown plugin: '{plugin}'"
84                )));
85            }
86        }
87
88        // Validate table prefix
89        if self.migration.table_prefix.is_empty() {
90            return Err(ConfigError::InvalidValue(
91                "table_prefix must not be empty".to_string(),
92            ));
93        }
94
95        Ok(())
96    }
97
98    /// Create a new config with defaults for the given ORM and dialect.
99    pub fn new(orm: crate::Orm, dialect: &str, plugins: Vec<String>) -> Self {
100        Self {
101            migration: MigrationConfig {
102                orm,
103                dialect: dialect.to_string(),
104                migrations_dir: default_migrations_dir(),
105                schema: None,
106                table_prefix: default_table_prefix(),
107            },
108            plugins: PluginsConfig { enabled: plugins },
109        }
110    }
111}
112
113/// Errors from config operations.
114#[derive(Debug)]
115pub enum ConfigError {
116    Io(String, std::io::Error),
117    Parse(String, toml::de::Error),
118    Serialize(String),
119    InvalidValue(String),
120}
121
122impl std::fmt::Display for ConfigError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            ConfigError::Io(path, e) => write!(f, "I/O error with '{path}': {e}"),
126            ConfigError::Parse(path, e) => write!(f, "parse error in '{path}': {e}"),
127            ConfigError::Serialize(e) => write!(f, "serialization error: {e}"),
128            ConfigError::InvalidValue(msg) => write!(f, "invalid config: {msg}"),
129        }
130    }
131}
132
133impl std::error::Error for ConfigError {}