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    /// Directory where sqlx query files are written (sqlx only).
27    #[serde(default = "default_queries_dir")]
28    pub queries_dir: String,
29    /// PostgreSQL schema name (optional, default "public").
30    #[serde(default)]
31    pub schema: Option<String>,
32    /// Table name prefix (default "yauth_").
33    #[serde(default = "default_table_prefix")]
34    pub table_prefix: String,
35}
36
37/// Plugin configuration.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PluginsConfig {
40    /// List of enabled plugin names.
41    pub enabled: Vec<String>,
42}
43
44fn default_migrations_dir() -> String {
45    "migrations".to_string()
46}
47
48fn default_queries_dir() -> String {
49    "queries".to_string()
50}
51
52fn default_table_prefix() -> String {
53    "yauth_".to_string()
54}
55
56impl YAuthConfig {
57    /// Load config from a TOML file.
58    pub fn load(path: &Path) -> Result<Self, ConfigError> {
59        let contents = std::fs::read_to_string(path)
60            .map_err(|e| ConfigError::Io(path.display().to_string(), e))?;
61        let config: Self = toml::from_str(&contents)
62            .map_err(|e| ConfigError::Parse(path.display().to_string(), e))?;
63        config.validate()?;
64        Ok(config)
65    }
66
67    /// Save config to a TOML file.
68    pub fn save(&self, path: &Path) -> Result<(), ConfigError> {
69        let contents =
70            toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
71        std::fs::write(path, contents)
72            .map_err(|e| ConfigError::Io(path.display().to_string(), e))?;
73        Ok(())
74    }
75
76    /// Validate config values.
77    pub fn validate(&self) -> Result<(), ConfigError> {
78        // Validate dialect
79        if self.migration.dialect.parse::<crate::Dialect>().is_err() {
80            return Err(ConfigError::InvalidValue(format!(
81                "unknown dialect: '{}'",
82                self.migration.dialect
83            )));
84        }
85
86        // Validate plugins
87        for plugin in &self.plugins.enabled {
88            if !crate::is_known_plugin(plugin) {
89                return Err(ConfigError::InvalidValue(format!(
90                    "unknown plugin: '{plugin}'"
91                )));
92            }
93        }
94
95        // Validate table prefix
96        if self.migration.table_prefix.is_empty() {
97            return Err(ConfigError::InvalidValue(
98                "table_prefix must not be empty".to_string(),
99            ));
100        }
101
102        Ok(())
103    }
104
105    /// Create a new config with defaults for the given ORM and dialect.
106    pub fn new(orm: crate::Orm, dialect: &str, plugins: Vec<String>) -> Self {
107        Self {
108            migration: MigrationConfig {
109                orm,
110                dialect: dialect.to_string(),
111                migrations_dir: default_migrations_dir(),
112                queries_dir: default_queries_dir(),
113                schema: None,
114                table_prefix: default_table_prefix(),
115            },
116            plugins: PluginsConfig { enabled: plugins },
117        }
118    }
119}
120
121/// Errors from config operations.
122#[derive(Debug)]
123pub enum ConfigError {
124    Io(String, std::io::Error),
125    Parse(String, toml::de::Error),
126    Serialize(String),
127    InvalidValue(String),
128}
129
130impl std::fmt::Display for ConfigError {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            ConfigError::Io(path, e) => write!(f, "I/O error with '{path}': {e}"),
134            ConfigError::Parse(path, e) => write!(f, "parse error in '{path}': {e}"),
135            ConfigError::Serialize(e) => write!(f, "serialization error: {e}"),
136            ConfigError::InvalidValue(msg) => write!(f, "invalid config: {msg}"),
137        }
138    }
139}
140
141impl std::error::Error for ConfigError {}