Skip to main content

prax_schema/config/
mod.rs

1//! Configuration file parsing for `prax.toml`.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6
7use crate::error::{SchemaError, SchemaResult};
8
9/// Main configuration structure for `prax.toml`.
10#[derive(Debug, Clone, Default, Deserialize, Serialize)]
11#[serde(deny_unknown_fields)]
12pub struct PraxConfig {
13    /// Database configuration.
14    #[serde(default)]
15    pub database: DatabaseConfig,
16
17    /// Schema file configuration.
18    #[serde(default)]
19    pub schema: SchemaConfig,
20
21    /// Generator configuration.
22    #[serde(default)]
23    pub generator: GeneratorConfig,
24
25    /// Migration settings.
26    #[serde(default)]
27    pub migrations: MigrationConfig,
28
29    /// Seeding configuration.
30    #[serde(default)]
31    pub seed: SeedConfig,
32
33    /// Debug/logging settings.
34    #[serde(default)]
35    pub debug: DebugConfig,
36
37    /// Environment-specific overrides.
38    #[serde(default)]
39    pub environments: HashMap<String, EnvironmentOverride>,
40}
41
42impl PraxConfig {
43    /// Load configuration from a file path.
44    pub fn from_file(path: impl AsRef<Path>) -> SchemaResult<Self> {
45        let path = path.as_ref();
46        let content = std::fs::read_to_string(path).map_err(|e| SchemaError::IoError {
47            path: path.display().to_string(),
48            source: e,
49        })?;
50
51        Self::from_str(&content)
52    }
53
54    /// Parse configuration from a TOML string.
55    #[allow(clippy::should_implement_trait)]
56    pub fn from_str(content: &str) -> SchemaResult<Self> {
57        // First, expand environment variables
58        let expanded = expand_env_vars(content);
59
60        toml::from_str(&expanded).map_err(|e| SchemaError::TomlError { source: e })
61    }
62
63    /// Get the database URL, resolving environment variables.
64    pub fn database_url(&self) -> Option<&str> {
65        self.database.url.as_deref()
66    }
67
68    /// Apply environment-specific overrides.
69    pub fn with_environment(mut self, env: &str) -> Self {
70        if let Some(overrides) = self.environments.remove(env) {
71            if let Some(db) = overrides.database {
72                if let Some(url) = db.url {
73                    self.database.url = Some(url);
74                }
75                if let Some(pool) = db.pool {
76                    self.database.pool = pool;
77                }
78            }
79            if let Some(debug) = overrides.debug {
80                if let Some(log_queries) = debug.log_queries {
81                    self.debug.log_queries = log_queries;
82                }
83                if let Some(pretty_sql) = debug.pretty_sql {
84                    self.debug.pretty_sql = pretty_sql;
85                }
86                if let Some(threshold) = debug.slow_query_threshold {
87                    self.debug.slow_query_threshold = threshold;
88                }
89            }
90        }
91        self
92    }
93}
94
95/// Database configuration.
96#[derive(Debug, Clone, Deserialize, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct DatabaseConfig {
99    /// Database provider.
100    #[serde(default = "default_provider")]
101    pub provider: DatabaseProvider,
102
103    /// Connection URL (supports `${ENV_VAR}` interpolation).
104    pub url: Option<String>,
105
106    /// Connection pool settings.
107    #[serde(default)]
108    pub pool: PoolConfig,
109}
110
111impl Default for DatabaseConfig {
112    fn default() -> Self {
113        Self {
114            provider: DatabaseProvider::PostgreSql,
115            url: None,
116            pool: PoolConfig::default(),
117        }
118    }
119}
120
121fn default_provider() -> DatabaseProvider {
122    DatabaseProvider::PostgreSql
123}
124
125/// Supported database providers.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
127#[serde(rename_all = "lowercase")]
128pub enum DatabaseProvider {
129    /// PostgreSQL.
130    #[serde(alias = "postgres")]
131    PostgreSql,
132    /// MySQL / MariaDB.
133    MySql,
134    /// SQLite.
135    #[serde(alias = "sqlite3")]
136    Sqlite,
137    /// MongoDB.
138    #[serde(alias = "mongo")]
139    MongoDb,
140}
141
142impl DatabaseProvider {
143    /// Get the provider name as a string.
144    pub fn as_str(&self) -> &'static str {
145        match self {
146            Self::PostgreSql => "postgresql",
147            Self::MySql => "mysql",
148            Self::Sqlite => "sqlite",
149            Self::MongoDb => "mongodb",
150        }
151    }
152}
153
154/// Connection pool configuration.
155#[derive(Debug, Clone, Deserialize, Serialize)]
156#[serde(deny_unknown_fields)]
157pub struct PoolConfig {
158    /// Minimum number of connections.
159    #[serde(default = "default_min_connections")]
160    pub min_connections: u32,
161
162    /// Maximum number of connections.
163    #[serde(default = "default_max_connections")]
164    pub max_connections: u32,
165
166    /// Connection timeout.
167    #[serde(default = "default_connect_timeout")]
168    pub connect_timeout: String,
169
170    /// Idle connection timeout.
171    #[serde(default = "default_idle_timeout")]
172    pub idle_timeout: String,
173
174    /// Maximum connection lifetime.
175    #[serde(default = "default_max_lifetime")]
176    pub max_lifetime: String,
177}
178
179impl Default for PoolConfig {
180    fn default() -> Self {
181        Self {
182            min_connections: default_min_connections(),
183            max_connections: default_max_connections(),
184            connect_timeout: default_connect_timeout(),
185            idle_timeout: default_idle_timeout(),
186            max_lifetime: default_max_lifetime(),
187        }
188    }
189}
190
191fn default_min_connections() -> u32 {
192    2
193}
194fn default_max_connections() -> u32 {
195    10
196}
197fn default_connect_timeout() -> String {
198    "30s".to_string()
199}
200fn default_idle_timeout() -> String {
201    "10m".to_string()
202}
203fn default_max_lifetime() -> String {
204    "30m".to_string()
205}
206
207/// Schema file configuration.
208#[derive(Debug, Clone, Deserialize, Serialize)]
209#[serde(deny_unknown_fields)]
210pub struct SchemaConfig {
211    /// Path to the schema file.
212    #[serde(default = "default_schema_path")]
213    pub path: String,
214}
215
216impl Default for SchemaConfig {
217    fn default() -> Self {
218        Self {
219            path: default_schema_path(),
220        }
221    }
222}
223
224fn default_schema_path() -> String {
225    "schema.prax".to_string()
226}
227
228/// Generator configuration.
229#[derive(Debug, Clone, Default, Deserialize, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct GeneratorConfig {
232    /// Client generator settings.
233    #[serde(default)]
234    pub client: ClientGeneratorConfig,
235}
236
237/// Style of model code generation.
238///
239/// Controls whether models are generated as plain Rust structs or with
240/// additional framework-specific derives like async-graphql.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
242#[serde(rename_all = "lowercase")]
243pub enum ModelStyle {
244    /// Generate plain Rust models with Serde derives.
245    /// This is the default and generates the lightest weight models.
246    #[default]
247    Standard,
248
249    /// Generate models with async-graphql derives.
250    /// Adds `#[derive(SimpleObject)]`, `#[derive(InputObject)]`, etc.
251    /// Requires the `async-graphql` crate as a dependency.
252    #[serde(alias = "async-graphql")]
253    GraphQL,
254}
255
256impl ModelStyle {
257    /// Returns true if this style requires GraphQL derives.
258    pub fn is_graphql(&self) -> bool {
259        matches!(self, Self::GraphQL)
260    }
261}
262
263/// Client generator configuration.
264#[derive(Debug, Clone, Deserialize, Serialize)]
265#[serde(deny_unknown_fields)]
266pub struct ClientGeneratorConfig {
267    /// Output directory.
268    #[serde(default = "default_output")]
269    pub output: String,
270
271    /// Generate async client.
272    #[serde(default = "default_true")]
273    pub async_client: bool,
274
275    /// Enable tracing instrumentation.
276    #[serde(default)]
277    pub tracing: bool,
278
279    /// Preview features to enable.
280    #[serde(default)]
281    pub preview_features: Vec<String>,
282
283    /// Model generation style.
284    ///
285    /// Controls the type of derives and attributes added to generated models:
286    /// - `standard`: Plain Rust structs with Serde (default)
287    /// - `graphql`: Adds async-graphql derives (SimpleObject, InputObject, etc.)
288    #[serde(default)]
289    pub model_style: ModelStyle,
290}
291
292impl Default for ClientGeneratorConfig {
293    fn default() -> Self {
294        Self {
295            output: default_output(),
296            async_client: true,
297            tracing: false,
298            preview_features: vec![],
299            model_style: ModelStyle::default(),
300        }
301    }
302}
303
304fn default_output() -> String {
305    "./src/generated".to_string()
306}
307fn default_true() -> bool {
308    true
309}
310
311/// Migration configuration.
312#[derive(Debug, Clone, Deserialize, Serialize)]
313#[serde(deny_unknown_fields)]
314pub struct MigrationConfig {
315    /// Migration files directory.
316    #[serde(default = "default_migrations_dir")]
317    pub directory: String,
318
319    /// Auto-apply migrations in development.
320    #[serde(default)]
321    pub auto_migrate: bool,
322
323    /// Migration history table name.
324    #[serde(default = "default_migrations_table")]
325    pub table_name: String,
326}
327
328impl Default for MigrationConfig {
329    fn default() -> Self {
330        Self {
331            directory: default_migrations_dir(),
332            auto_migrate: false,
333            table_name: default_migrations_table(),
334        }
335    }
336}
337
338fn default_migrations_dir() -> String {
339    "./migrations".to_string()
340}
341fn default_migrations_table() -> String {
342    "_prax_migrations".to_string()
343}
344
345/// Seed configuration.
346#[derive(Debug, Clone, Default, Deserialize, Serialize)]
347#[serde(deny_unknown_fields)]
348pub struct SeedConfig {
349    /// Seed script path.
350    pub script: Option<String>,
351
352    /// Run seed after migrations.
353    #[serde(default)]
354    pub auto_seed: bool,
355
356    /// Environment-specific seeding flags.
357    #[serde(default)]
358    pub environments: HashMap<String, bool>,
359}
360
361/// Debug/logging configuration.
362#[derive(Debug, Clone, Deserialize, Serialize)]
363#[serde(deny_unknown_fields)]
364pub struct DebugConfig {
365    /// Log all queries.
366    #[serde(default)]
367    pub log_queries: bool,
368
369    /// Pretty print SQL.
370    #[serde(default = "default_true")]
371    pub pretty_sql: bool,
372
373    /// Slow query threshold in milliseconds.
374    #[serde(default = "default_slow_query_threshold")]
375    pub slow_query_threshold: u64,
376}
377
378impl Default for DebugConfig {
379    fn default() -> Self {
380        Self {
381            log_queries: false,
382            pretty_sql: true,
383            slow_query_threshold: default_slow_query_threshold(),
384        }
385    }
386}
387
388fn default_slow_query_threshold() -> u64 {
389    1000
390}
391
392/// Environment-specific configuration overrides.
393#[derive(Debug, Clone, Default, Deserialize, Serialize)]
394#[serde(deny_unknown_fields)]
395pub struct EnvironmentOverride {
396    /// Database overrides.
397    pub database: Option<DatabaseOverride>,
398
399    /// Debug overrides.
400    pub debug: Option<DebugOverride>,
401}
402
403/// Database configuration overrides.
404#[derive(Debug, Clone, Default, Deserialize, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct DatabaseOverride {
407    /// Override connection URL.
408    pub url: Option<String>,
409
410    /// Override pool settings.
411    pub pool: Option<PoolConfig>,
412}
413
414/// Debug configuration overrides.
415#[derive(Debug, Clone, Default, Deserialize, Serialize)]
416#[serde(deny_unknown_fields)]
417pub struct DebugOverride {
418    /// Override log_queries.
419    pub log_queries: Option<bool>,
420
421    /// Override pretty_sql.
422    pub pretty_sql: Option<bool>,
423
424    /// Override slow_query_threshold.
425    pub slow_query_threshold: Option<u64>,
426}
427
428/// Expand environment variables in the format `${VAR_NAME}`.
429fn expand_env_vars(content: &str) -> String {
430    let mut result = content.to_string();
431    // Compile the `${...}` pattern once; `expand_env_vars` runs on every
432    // config load and the pattern is constant.
433    static PATTERN: std::sync::OnceLock<regex_lite::Regex> = std::sync::OnceLock::new();
434    let re = PATTERN.get_or_init(|| regex_lite::Regex::new(r"\$\{([^}]+)\}").unwrap());
435
436    for cap in re.captures_iter(content) {
437        let var_name = &cap[1];
438        let full_match = &cap[0];
439
440        if let Ok(value) = std::env::var(var_name) {
441            result = result.replace(full_match, &value);
442        }
443    }
444
445    result
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    // ==================== PraxConfig Tests ====================
453
454    #[test]
455    fn test_default_config() {
456        let config = PraxConfig::default();
457        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
458        assert_eq!(config.schema.path, "schema.prax");
459        assert!(config.database.url.is_none());
460        assert!(config.environments.is_empty());
461    }
462
463    #[test]
464    fn test_parse_minimal_config() {
465        let toml = r#"
466            [database]
467            provider = "postgresql"
468            url = "postgres://localhost/test"
469        "#;
470
471        let config = PraxConfig::from_str(toml).unwrap();
472        assert_eq!(
473            config.database.url,
474            Some("postgres://localhost/test".to_string())
475        );
476    }
477
478    #[test]
479    fn test_parse_full_config() {
480        let toml = r#"
481            [database]
482            provider = "postgresql"
483            url = "postgres://user:pass@localhost:5432/db"
484
485            [database.pool]
486            min_connections = 5
487            max_connections = 20
488            connect_timeout = "60s"
489            idle_timeout = "5m"
490            max_lifetime = "1h"
491
492            [schema]
493            path = "prisma/schema.prax"
494
495            [generator.client]
496            output = "./src/db"
497            async_client = true
498            tracing = true
499            preview_features = ["json", "fulltext"]
500
501            [migrations]
502            directory = "./db/migrations"
503            auto_migrate = true
504            table_name = "_migrations"
505
506            [seed]
507            script = "./scripts/seed.sh"
508            auto_seed = true
509
510            [seed.environments]
511            development = true
512            test = true
513            production = false
514
515            [debug]
516            log_queries = true
517            pretty_sql = false
518            slow_query_threshold = 500
519        "#;
520
521        let config = PraxConfig::from_str(toml).unwrap();
522
523        // Database
524        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
525        assert!(config.database.url.is_some());
526        assert_eq!(config.database.pool.min_connections, 5);
527        assert_eq!(config.database.pool.max_connections, 20);
528
529        // Schema
530        assert_eq!(config.schema.path, "prisma/schema.prax");
531
532        // Generator
533        assert_eq!(config.generator.client.output, "./src/db");
534        assert!(config.generator.client.async_client);
535        assert!(config.generator.client.tracing);
536        assert_eq!(config.generator.client.preview_features.len(), 2);
537
538        // Migrations
539        assert_eq!(config.migrations.directory, "./db/migrations");
540        assert!(config.migrations.auto_migrate);
541        assert_eq!(config.migrations.table_name, "_migrations");
542
543        // Seed
544        assert_eq!(config.seed.script, Some("./scripts/seed.sh".to_string()));
545        assert!(config.seed.auto_seed);
546        assert!(
547            config
548                .seed
549                .environments
550                .get("development")
551                .copied()
552                .unwrap_or(false)
553        );
554
555        // Debug
556        assert!(config.debug.log_queries);
557        assert!(!config.debug.pretty_sql);
558        assert_eq!(config.debug.slow_query_threshold, 500);
559    }
560
561    #[test]
562    fn test_database_url_method() {
563        let config = PraxConfig {
564            database: DatabaseConfig {
565                url: Some("postgres://localhost/test".to_string()),
566                ..Default::default()
567            },
568            ..Default::default()
569        };
570
571        assert_eq!(config.database_url(), Some("postgres://localhost/test"));
572    }
573
574    #[test]
575    fn test_database_url_method_none() {
576        let config = PraxConfig::default();
577        assert!(config.database_url().is_none());
578    }
579
580    #[test]
581    fn test_with_environment_overrides() {
582        let toml = r#"
583            [database]
584            url = "postgres://localhost/dev"
585
586            [debug]
587            log_queries = false
588
589            [environments.production]
590            [environments.production.database]
591            url = "postgres://prod.server/db"
592
593            [environments.production.debug]
594            log_queries = true
595            slow_query_threshold = 100
596        "#;
597
598        let config = PraxConfig::from_str(toml)
599            .unwrap()
600            .with_environment("production");
601
602        assert_eq!(
603            config.database.url,
604            Some("postgres://prod.server/db".to_string())
605        );
606        assert!(config.debug.log_queries);
607        assert_eq!(config.debug.slow_query_threshold, 100);
608    }
609
610    #[test]
611    fn test_with_environment_nonexistent() {
612        let config = PraxConfig::default().with_environment("nonexistent");
613        // Should not panic and return unchanged config
614        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
615    }
616
617    #[test]
618    fn test_parse_invalid_toml() {
619        let toml = "this is not valid [[ toml";
620        let result = PraxConfig::from_str(toml);
621        assert!(result.is_err());
622    }
623
624    // ==================== DatabaseProvider Tests ====================
625
626    #[test]
627    fn test_database_provider_postgresql() {
628        let toml = r#"
629            [database]
630            provider = "postgresql"
631        "#;
632        let config = PraxConfig::from_str(toml).unwrap();
633        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
634        assert_eq!(config.database.provider.as_str(), "postgresql");
635    }
636
637    #[test]
638    fn test_database_provider_postgres_alias() {
639        let toml = r#"
640            [database]
641            provider = "postgres"
642        "#;
643        let config = PraxConfig::from_str(toml).unwrap();
644        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
645    }
646
647    #[test]
648    fn test_database_provider_mysql() {
649        let toml = r#"
650            [database]
651            provider = "mysql"
652        "#;
653        let config = PraxConfig::from_str(toml).unwrap();
654        assert_eq!(config.database.provider, DatabaseProvider::MySql);
655        assert_eq!(config.database.provider.as_str(), "mysql");
656    }
657
658    #[test]
659    fn test_database_provider_sqlite() {
660        let toml = r#"
661            [database]
662            provider = "sqlite"
663        "#;
664        let config = PraxConfig::from_str(toml).unwrap();
665        assert_eq!(config.database.provider, DatabaseProvider::Sqlite);
666        assert_eq!(config.database.provider.as_str(), "sqlite");
667    }
668
669    #[test]
670    fn test_database_provider_sqlite3_alias() {
671        let toml = r#"
672            [database]
673            provider = "sqlite3"
674        "#;
675        let config = PraxConfig::from_str(toml).unwrap();
676        assert_eq!(config.database.provider, DatabaseProvider::Sqlite);
677    }
678
679    #[test]
680    fn test_database_provider_mongodb() {
681        let toml = r#"
682            [database]
683            provider = "mongodb"
684        "#;
685        let config = PraxConfig::from_str(toml).unwrap();
686        assert_eq!(config.database.provider, DatabaseProvider::MongoDb);
687        assert_eq!(config.database.provider.as_str(), "mongodb");
688    }
689
690    #[test]
691    fn test_database_provider_mongo_alias() {
692        let toml = r#"
693            [database]
694            provider = "mongo"
695        "#;
696        let config = PraxConfig::from_str(toml).unwrap();
697        assert_eq!(config.database.provider, DatabaseProvider::MongoDb);
698    }
699
700    // ==================== PoolConfig Tests ====================
701
702    #[test]
703    fn test_pool_config_defaults() {
704        let config = PoolConfig::default();
705        assert_eq!(config.min_connections, 2);
706        assert_eq!(config.max_connections, 10);
707        assert_eq!(config.connect_timeout, "30s");
708        assert_eq!(config.idle_timeout, "10m");
709        assert_eq!(config.max_lifetime, "30m");
710    }
711
712    #[test]
713    fn test_pool_config_custom() {
714        let toml = r#"
715            [database]
716            provider = "postgresql"
717
718            [database.pool]
719            min_connections = 1
720            max_connections = 50
721            connect_timeout = "10s"
722            idle_timeout = "30m"
723            max_lifetime = "2h"
724        "#;
725
726        let config = PraxConfig::from_str(toml).unwrap();
727        assert_eq!(config.database.pool.min_connections, 1);
728        assert_eq!(config.database.pool.max_connections, 50);
729        assert_eq!(config.database.pool.connect_timeout, "10s");
730    }
731
732    // ==================== SchemaConfig Tests ====================
733
734    #[test]
735    fn test_schema_config_default() {
736        let config = SchemaConfig::default();
737        assert_eq!(config.path, "schema.prax");
738    }
739
740    #[test]
741    fn test_schema_config_custom() {
742        let toml = r#"
743            [schema]
744            path = "db/schema.prax"
745        "#;
746
747        let config = PraxConfig::from_str(toml).unwrap();
748        assert_eq!(config.schema.path, "db/schema.prax");
749    }
750
751    // ==================== GeneratorConfig Tests ====================
752
753    #[test]
754    fn test_generator_config_default() {
755        let config = GeneratorConfig::default();
756        assert_eq!(config.client.output, "./src/generated");
757        assert!(config.client.async_client);
758        assert!(!config.client.tracing);
759        assert!(config.client.preview_features.is_empty());
760        assert_eq!(config.client.model_style, ModelStyle::Standard);
761    }
762
763    #[test]
764    fn test_generator_config_custom() {
765        let toml = r#"
766            [generator.client]
767            output = "./generated"
768            async_client = false
769            tracing = true
770            preview_features = ["feature1", "feature2"]
771        "#;
772
773        let config = PraxConfig::from_str(toml).unwrap();
774        assert_eq!(config.generator.client.output, "./generated");
775        assert!(!config.generator.client.async_client);
776        assert!(config.generator.client.tracing);
777        assert_eq!(config.generator.client.preview_features.len(), 2);
778    }
779
780    #[test]
781    fn test_generator_config_graphql_model_style() {
782        let toml = r#"
783            [generator.client]
784            model_style = "graphql"
785        "#;
786
787        let config = PraxConfig::from_str(toml).unwrap();
788        assert_eq!(config.generator.client.model_style, ModelStyle::GraphQL);
789        assert!(config.generator.client.model_style.is_graphql());
790    }
791
792    #[test]
793    fn test_generator_config_graphql_model_style_alias() {
794        let toml = r#"
795            [generator.client]
796            model_style = "async-graphql"
797        "#;
798
799        let config = PraxConfig::from_str(toml).unwrap();
800        assert_eq!(config.generator.client.model_style, ModelStyle::GraphQL);
801    }
802
803    #[test]
804    fn test_model_style_standard_is_not_graphql() {
805        assert!(!ModelStyle::Standard.is_graphql());
806        assert!(ModelStyle::GraphQL.is_graphql());
807    }
808
809    // ==================== MigrationConfig Tests ====================
810
811    #[test]
812    fn test_migration_config_default() {
813        let config = MigrationConfig::default();
814        assert_eq!(config.directory, "./migrations");
815        assert!(!config.auto_migrate);
816        assert_eq!(config.table_name, "_prax_migrations");
817    }
818
819    #[test]
820    fn test_migration_config_custom() {
821        let toml = r#"
822            [migrations]
823            directory = "./db/migrate"
824            auto_migrate = true
825            table_name = "schema_migrations"
826        "#;
827
828        let config = PraxConfig::from_str(toml).unwrap();
829        assert_eq!(config.migrations.directory, "./db/migrate");
830        assert!(config.migrations.auto_migrate);
831        assert_eq!(config.migrations.table_name, "schema_migrations");
832    }
833
834    // ==================== SeedConfig Tests ====================
835
836    #[test]
837    fn test_seed_config_default() {
838        let config = SeedConfig::default();
839        assert!(config.script.is_none());
840        assert!(!config.auto_seed);
841        assert!(config.environments.is_empty());
842    }
843
844    #[test]
845    fn test_seed_config_custom() {
846        let toml = r#"
847            [seed]
848            script = "seed.rs"
849            auto_seed = true
850
851            [seed.environments]
852            dev = true
853            prod = false
854        "#;
855
856        let config = PraxConfig::from_str(toml).unwrap();
857        assert_eq!(config.seed.script, Some("seed.rs".to_string()));
858        assert!(config.seed.auto_seed);
859        assert_eq!(config.seed.environments.get("dev"), Some(&true));
860        assert_eq!(config.seed.environments.get("prod"), Some(&false));
861    }
862
863    // ==================== DebugConfig Tests ====================
864
865    #[test]
866    fn test_debug_config_default() {
867        let config = DebugConfig::default();
868        assert!(!config.log_queries);
869        assert!(config.pretty_sql);
870        assert_eq!(config.slow_query_threshold, 1000);
871    }
872
873    #[test]
874    fn test_debug_config_custom() {
875        let toml = r#"
876            [debug]
877            log_queries = true
878            pretty_sql = false
879            slow_query_threshold = 200
880        "#;
881
882        let config = PraxConfig::from_str(toml).unwrap();
883        assert!(config.debug.log_queries);
884        assert!(!config.debug.pretty_sql);
885        assert_eq!(config.debug.slow_query_threshold, 200);
886    }
887
888    // ==================== Environment Variable Tests ====================
889
890    #[test]
891    fn test_env_var_expansion() {
892        // SAFETY: This test runs single-threaded and we clean up after
893        unsafe {
894            std::env::set_var("TEST_DB_URL", "postgres://test");
895        }
896        let expanded = expand_env_vars("url = \"${TEST_DB_URL}\"");
897        assert_eq!(expanded, "url = \"postgres://test\"");
898        unsafe {
899            std::env::remove_var("TEST_DB_URL");
900        }
901    }
902
903    #[test]
904    fn test_env_var_expansion_multiple() {
905        unsafe {
906            std::env::set_var("TEST_HOST", "localhost");
907            std::env::set_var("TEST_PORT", "5432");
908        }
909        let content = "host = \"${TEST_HOST}\"\nport = \"${TEST_PORT}\"";
910        let expanded = expand_env_vars(content);
911        assert!(expanded.contains("localhost"));
912        assert!(expanded.contains("5432"));
913        unsafe {
914            std::env::remove_var("TEST_HOST");
915            std::env::remove_var("TEST_PORT");
916        }
917    }
918
919    #[test]
920    fn test_env_var_expansion_missing_var() {
921        let content = "url = \"${DEFINITELY_NOT_SET_VAR_12345}\"";
922        let expanded = expand_env_vars(content);
923        // Should not expand missing variables
924        assert_eq!(expanded, content);
925    }
926
927    #[test]
928    fn test_env_var_expansion_in_config() {
929        unsafe {
930            std::env::set_var("TEST_DATABASE_URL_2", "postgres://user:pass@localhost/db");
931        }
932
933        let toml = r#"
934            [database]
935            url = "${TEST_DATABASE_URL_2}"
936        "#;
937
938        let config = PraxConfig::from_str(toml).unwrap();
939        assert_eq!(
940            config.database.url,
941            Some("postgres://user:pass@localhost/db".to_string())
942        );
943
944        unsafe {
945            std::env::remove_var("TEST_DATABASE_URL_2");
946        }
947    }
948
949    // ==================== Environment Override Tests ====================
950
951    #[test]
952    fn test_environment_override_database_url() {
953        let toml = r#"
954            [database]
955            url = "postgres://localhost/dev"
956
957            [environments.test]
958            [environments.test.database]
959            url = "postgres://localhost/test_db"
960        "#;
961
962        let config = PraxConfig::from_str(toml).unwrap().with_environment("test");
963
964        assert_eq!(
965            config.database.url,
966            Some("postgres://localhost/test_db".to_string())
967        );
968    }
969
970    #[test]
971    fn test_environment_override_pool() {
972        let toml = r#"
973            [database.pool]
974            max_connections = 10
975
976            [environments.production]
977            [environments.production.database.pool]
978            max_connections = 100
979            min_connections = 10
980        "#;
981
982        let config = PraxConfig::from_str(toml)
983            .unwrap()
984            .with_environment("production");
985
986        assert_eq!(config.database.pool.max_connections, 100);
987        assert_eq!(config.database.pool.min_connections, 10);
988    }
989
990    #[test]
991    fn test_environment_override_debug() {
992        let toml = r#"
993            [debug]
994            log_queries = false
995            pretty_sql = true
996
997            [environments.development]
998            [environments.development.debug]
999            log_queries = true
1000            pretty_sql = false
1001            slow_query_threshold = 50
1002        "#;
1003
1004        let config = PraxConfig::from_str(toml)
1005            .unwrap()
1006            .with_environment("development");
1007
1008        assert!(config.debug.log_queries);
1009        assert!(!config.debug.pretty_sql);
1010        assert_eq!(config.debug.slow_query_threshold, 50);
1011    }
1012
1013    // ==================== Serialization Tests ====================
1014
1015    #[test]
1016    fn test_config_serialization() {
1017        let config = PraxConfig::default();
1018        let toml_str = toml::to_string(&config).unwrap();
1019        assert!(toml_str.contains("[database]"));
1020    }
1021
1022    #[test]
1023    fn test_config_roundtrip() {
1024        let original = PraxConfig {
1025            database: DatabaseConfig {
1026                provider: DatabaseProvider::MySql,
1027                url: Some("mysql://localhost/test".to_string()),
1028                pool: PoolConfig::default(),
1029            },
1030            ..Default::default()
1031        };
1032
1033        let toml_str = toml::to_string(&original).unwrap();
1034        let parsed: PraxConfig = toml::from_str(&toml_str).unwrap();
1035
1036        assert_eq!(parsed.database.provider, original.database.provider);
1037        assert_eq!(parsed.database.url, original.database.url);
1038    }
1039
1040    // ==================== Clone and Debug Tests ====================
1041
1042    #[test]
1043    fn test_config_clone() {
1044        let config = PraxConfig::default();
1045        let cloned = config.clone();
1046        assert_eq!(config.database.provider, cloned.database.provider);
1047    }
1048
1049    #[test]
1050    fn test_config_debug() {
1051        let config = PraxConfig::default();
1052        let debug_str = format!("{:?}", config);
1053        assert!(debug_str.contains("PraxConfig"));
1054    }
1055
1056    #[test]
1057    fn test_provider_equality() {
1058        assert_eq!(DatabaseProvider::PostgreSql, DatabaseProvider::PostgreSql);
1059        assert_ne!(DatabaseProvider::PostgreSql, DatabaseProvider::MySql);
1060    }
1061
1062    // ==================== Default Function Tests ====================
1063
1064    #[test]
1065    fn test_default_functions() {
1066        assert_eq!(default_provider(), DatabaseProvider::PostgreSql);
1067        assert_eq!(default_min_connections(), 2);
1068        assert_eq!(default_max_connections(), 10);
1069        assert_eq!(default_connect_timeout(), "30s");
1070        assert_eq!(default_idle_timeout(), "10m");
1071        assert_eq!(default_max_lifetime(), "30m");
1072        assert_eq!(default_schema_path(), "schema.prax");
1073        assert_eq!(default_output(), "./src/generated");
1074        assert!(default_true());
1075        assert_eq!(default_migrations_dir(), "./migrations");
1076        assert_eq!(default_migrations_table(), "_prax_migrations");
1077        assert_eq!(default_slow_query_threshold(), 1000);
1078    }
1079}