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/// Client generator configuration.
238#[derive(Debug, Clone, Deserialize, Serialize)]
239#[serde(deny_unknown_fields)]
240pub struct ClientGeneratorConfig {
241    /// Output directory.
242    #[serde(default = "default_output")]
243    pub output: String,
244
245    /// Generate async client.
246    #[serde(default = "default_true")]
247    pub async_client: bool,
248
249    /// Enable tracing instrumentation.
250    #[serde(default)]
251    pub tracing: bool,
252
253    /// Preview features to enable.
254    #[serde(default)]
255    pub preview_features: Vec<String>,
256}
257
258impl Default for ClientGeneratorConfig {
259    fn default() -> Self {
260        Self {
261            output: default_output(),
262            async_client: true,
263            tracing: false,
264            preview_features: vec![],
265        }
266    }
267}
268
269fn default_output() -> String {
270    "./src/generated".to_string()
271}
272fn default_true() -> bool {
273    true
274}
275
276/// Migration configuration.
277#[derive(Debug, Clone, Deserialize, Serialize)]
278#[serde(deny_unknown_fields)]
279pub struct MigrationConfig {
280    /// Migration files directory.
281    #[serde(default = "default_migrations_dir")]
282    pub directory: String,
283
284    /// Auto-apply migrations in development.
285    #[serde(default)]
286    pub auto_migrate: bool,
287
288    /// Migration history table name.
289    #[serde(default = "default_migrations_table")]
290    pub table_name: String,
291}
292
293impl Default for MigrationConfig {
294    fn default() -> Self {
295        Self {
296            directory: default_migrations_dir(),
297            auto_migrate: false,
298            table_name: default_migrations_table(),
299        }
300    }
301}
302
303fn default_migrations_dir() -> String {
304    "./migrations".to_string()
305}
306fn default_migrations_table() -> String {
307    "_prax_migrations".to_string()
308}
309
310/// Seed configuration.
311#[derive(Debug, Clone, Default, Deserialize, Serialize)]
312#[serde(deny_unknown_fields)]
313pub struct SeedConfig {
314    /// Seed script path.
315    pub script: Option<String>,
316
317    /// Run seed after migrations.
318    #[serde(default)]
319    pub auto_seed: bool,
320
321    /// Environment-specific seeding flags.
322    #[serde(default)]
323    pub environments: HashMap<String, bool>,
324}
325
326/// Debug/logging configuration.
327#[derive(Debug, Clone, Deserialize, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct DebugConfig {
330    /// Log all queries.
331    #[serde(default)]
332    pub log_queries: bool,
333
334    /// Pretty print SQL.
335    #[serde(default = "default_true")]
336    pub pretty_sql: bool,
337
338    /// Slow query threshold in milliseconds.
339    #[serde(default = "default_slow_query_threshold")]
340    pub slow_query_threshold: u64,
341}
342
343impl Default for DebugConfig {
344    fn default() -> Self {
345        Self {
346            log_queries: false,
347            pretty_sql: true,
348            slow_query_threshold: default_slow_query_threshold(),
349        }
350    }
351}
352
353fn default_slow_query_threshold() -> u64 {
354    1000
355}
356
357/// Environment-specific configuration overrides.
358#[derive(Debug, Clone, Default, Deserialize, Serialize)]
359#[serde(deny_unknown_fields)]
360pub struct EnvironmentOverride {
361    /// Database overrides.
362    pub database: Option<DatabaseOverride>,
363
364    /// Debug overrides.
365    pub debug: Option<DebugOverride>,
366}
367
368/// Database configuration overrides.
369#[derive(Debug, Clone, Default, Deserialize, Serialize)]
370#[serde(deny_unknown_fields)]
371pub struct DatabaseOverride {
372    /// Override connection URL.
373    pub url: Option<String>,
374
375    /// Override pool settings.
376    pub pool: Option<PoolConfig>,
377}
378
379/// Debug configuration overrides.
380#[derive(Debug, Clone, Default, Deserialize, Serialize)]
381#[serde(deny_unknown_fields)]
382pub struct DebugOverride {
383    /// Override log_queries.
384    pub log_queries: Option<bool>,
385
386    /// Override pretty_sql.
387    pub pretty_sql: Option<bool>,
388
389    /// Override slow_query_threshold.
390    pub slow_query_threshold: Option<u64>,
391}
392
393/// Expand environment variables in the format `${VAR_NAME}`.
394fn expand_env_vars(content: &str) -> String {
395    let mut result = content.to_string();
396    let re = regex_lite::Regex::new(r"\$\{([^}]+)\}").unwrap();
397
398    for cap in re.captures_iter(content) {
399        let var_name = &cap[1];
400        let full_match = &cap[0];
401
402        if let Ok(value) = std::env::var(var_name) {
403            result = result.replace(full_match, &value);
404        }
405    }
406
407    result
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    // ==================== PraxConfig Tests ====================
415
416    #[test]
417    fn test_default_config() {
418        let config = PraxConfig::default();
419        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
420        assert_eq!(config.schema.path, "schema.prax");
421        assert!(config.database.url.is_none());
422        assert!(config.environments.is_empty());
423    }
424
425    #[test]
426    fn test_parse_minimal_config() {
427        let toml = r#"
428            [database]
429            provider = "postgresql"
430            url = "postgres://localhost/test"
431        "#;
432
433        let config = PraxConfig::from_str(toml).unwrap();
434        assert_eq!(
435            config.database.url,
436            Some("postgres://localhost/test".to_string())
437        );
438    }
439
440    #[test]
441    fn test_parse_full_config() {
442        let toml = r#"
443            [database]
444            provider = "postgresql"
445            url = "postgres://user:pass@localhost:5432/db"
446
447            [database.pool]
448            min_connections = 5
449            max_connections = 20
450            connect_timeout = "60s"
451            idle_timeout = "5m"
452            max_lifetime = "1h"
453
454            [schema]
455            path = "prisma/schema.prax"
456
457            [generator.client]
458            output = "./src/db"
459            async_client = true
460            tracing = true
461            preview_features = ["json", "fulltext"]
462
463            [migrations]
464            directory = "./db/migrations"
465            auto_migrate = true
466            table_name = "_migrations"
467
468            [seed]
469            script = "./scripts/seed.sh"
470            auto_seed = true
471
472            [seed.environments]
473            development = true
474            test = true
475            production = false
476
477            [debug]
478            log_queries = true
479            pretty_sql = false
480            slow_query_threshold = 500
481        "#;
482
483        let config = PraxConfig::from_str(toml).unwrap();
484
485        // Database
486        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
487        assert!(config.database.url.is_some());
488        assert_eq!(config.database.pool.min_connections, 5);
489        assert_eq!(config.database.pool.max_connections, 20);
490
491        // Schema
492        assert_eq!(config.schema.path, "prisma/schema.prax");
493
494        // Generator
495        assert_eq!(config.generator.client.output, "./src/db");
496        assert!(config.generator.client.async_client);
497        assert!(config.generator.client.tracing);
498        assert_eq!(config.generator.client.preview_features.len(), 2);
499
500        // Migrations
501        assert_eq!(config.migrations.directory, "./db/migrations");
502        assert!(config.migrations.auto_migrate);
503        assert_eq!(config.migrations.table_name, "_migrations");
504
505        // Seed
506        assert_eq!(config.seed.script, Some("./scripts/seed.sh".to_string()));
507        assert!(config.seed.auto_seed);
508        assert!(
509            config
510                .seed
511                .environments
512                .get("development")
513                .copied()
514                .unwrap_or(false)
515        );
516
517        // Debug
518        assert!(config.debug.log_queries);
519        assert!(!config.debug.pretty_sql);
520        assert_eq!(config.debug.slow_query_threshold, 500);
521    }
522
523    #[test]
524    fn test_database_url_method() {
525        let config = PraxConfig {
526            database: DatabaseConfig {
527                url: Some("postgres://localhost/test".to_string()),
528                ..Default::default()
529            },
530            ..Default::default()
531        };
532
533        assert_eq!(config.database_url(), Some("postgres://localhost/test"));
534    }
535
536    #[test]
537    fn test_database_url_method_none() {
538        let config = PraxConfig::default();
539        assert!(config.database_url().is_none());
540    }
541
542    #[test]
543    fn test_with_environment_overrides() {
544        let toml = r#"
545            [database]
546            url = "postgres://localhost/dev"
547
548            [debug]
549            log_queries = false
550
551            [environments.production]
552            [environments.production.database]
553            url = "postgres://prod.server/db"
554
555            [environments.production.debug]
556            log_queries = true
557            slow_query_threshold = 100
558        "#;
559
560        let config = PraxConfig::from_str(toml)
561            .unwrap()
562            .with_environment("production");
563
564        assert_eq!(
565            config.database.url,
566            Some("postgres://prod.server/db".to_string())
567        );
568        assert!(config.debug.log_queries);
569        assert_eq!(config.debug.slow_query_threshold, 100);
570    }
571
572    #[test]
573    fn test_with_environment_nonexistent() {
574        let config = PraxConfig::default().with_environment("nonexistent");
575        // Should not panic and return unchanged config
576        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
577    }
578
579    #[test]
580    fn test_parse_invalid_toml() {
581        let toml = "this is not valid [[ toml";
582        let result = PraxConfig::from_str(toml);
583        assert!(result.is_err());
584    }
585
586    // ==================== DatabaseProvider Tests ====================
587
588    #[test]
589    fn test_database_provider_postgresql() {
590        let toml = r#"
591            [database]
592            provider = "postgresql"
593        "#;
594        let config = PraxConfig::from_str(toml).unwrap();
595        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
596        assert_eq!(config.database.provider.as_str(), "postgresql");
597    }
598
599    #[test]
600    fn test_database_provider_postgres_alias() {
601        let toml = r#"
602            [database]
603            provider = "postgres"
604        "#;
605        let config = PraxConfig::from_str(toml).unwrap();
606        assert_eq!(config.database.provider, DatabaseProvider::PostgreSql);
607    }
608
609    #[test]
610    fn test_database_provider_mysql() {
611        let toml = r#"
612            [database]
613            provider = "mysql"
614        "#;
615        let config = PraxConfig::from_str(toml).unwrap();
616        assert_eq!(config.database.provider, DatabaseProvider::MySql);
617        assert_eq!(config.database.provider.as_str(), "mysql");
618    }
619
620    #[test]
621    fn test_database_provider_sqlite() {
622        let toml = r#"
623            [database]
624            provider = "sqlite"
625        "#;
626        let config = PraxConfig::from_str(toml).unwrap();
627        assert_eq!(config.database.provider, DatabaseProvider::Sqlite);
628        assert_eq!(config.database.provider.as_str(), "sqlite");
629    }
630
631    #[test]
632    fn test_database_provider_sqlite3_alias() {
633        let toml = r#"
634            [database]
635            provider = "sqlite3"
636        "#;
637        let config = PraxConfig::from_str(toml).unwrap();
638        assert_eq!(config.database.provider, DatabaseProvider::Sqlite);
639    }
640
641    #[test]
642    fn test_database_provider_mongodb() {
643        let toml = r#"
644            [database]
645            provider = "mongodb"
646        "#;
647        let config = PraxConfig::from_str(toml).unwrap();
648        assert_eq!(config.database.provider, DatabaseProvider::MongoDb);
649        assert_eq!(config.database.provider.as_str(), "mongodb");
650    }
651
652    #[test]
653    fn test_database_provider_mongo_alias() {
654        let toml = r#"
655            [database]
656            provider = "mongo"
657        "#;
658        let config = PraxConfig::from_str(toml).unwrap();
659        assert_eq!(config.database.provider, DatabaseProvider::MongoDb);
660    }
661
662    // ==================== PoolConfig Tests ====================
663
664    #[test]
665    fn test_pool_config_defaults() {
666        let config = PoolConfig::default();
667        assert_eq!(config.min_connections, 2);
668        assert_eq!(config.max_connections, 10);
669        assert_eq!(config.connect_timeout, "30s");
670        assert_eq!(config.idle_timeout, "10m");
671        assert_eq!(config.max_lifetime, "30m");
672    }
673
674    #[test]
675    fn test_pool_config_custom() {
676        let toml = r#"
677            [database]
678            provider = "postgresql"
679
680            [database.pool]
681            min_connections = 1
682            max_connections = 50
683            connect_timeout = "10s"
684            idle_timeout = "30m"
685            max_lifetime = "2h"
686        "#;
687
688        let config = PraxConfig::from_str(toml).unwrap();
689        assert_eq!(config.database.pool.min_connections, 1);
690        assert_eq!(config.database.pool.max_connections, 50);
691        assert_eq!(config.database.pool.connect_timeout, "10s");
692    }
693
694    // ==================== SchemaConfig Tests ====================
695
696    #[test]
697    fn test_schema_config_default() {
698        let config = SchemaConfig::default();
699        assert_eq!(config.path, "schema.prax");
700    }
701
702    #[test]
703    fn test_schema_config_custom() {
704        let toml = r#"
705            [schema]
706            path = "db/schema.prax"
707        "#;
708
709        let config = PraxConfig::from_str(toml).unwrap();
710        assert_eq!(config.schema.path, "db/schema.prax");
711    }
712
713    // ==================== GeneratorConfig Tests ====================
714
715    #[test]
716    fn test_generator_config_default() {
717        let config = GeneratorConfig::default();
718        assert_eq!(config.client.output, "./src/generated");
719        assert!(config.client.async_client);
720        assert!(!config.client.tracing);
721        assert!(config.client.preview_features.is_empty());
722    }
723
724    #[test]
725    fn test_generator_config_custom() {
726        let toml = r#"
727            [generator.client]
728            output = "./generated"
729            async_client = false
730            tracing = true
731            preview_features = ["feature1", "feature2"]
732        "#;
733
734        let config = PraxConfig::from_str(toml).unwrap();
735        assert_eq!(config.generator.client.output, "./generated");
736        assert!(!config.generator.client.async_client);
737        assert!(config.generator.client.tracing);
738        assert_eq!(config.generator.client.preview_features.len(), 2);
739    }
740
741    // ==================== MigrationConfig Tests ====================
742
743    #[test]
744    fn test_migration_config_default() {
745        let config = MigrationConfig::default();
746        assert_eq!(config.directory, "./migrations");
747        assert!(!config.auto_migrate);
748        assert_eq!(config.table_name, "_prax_migrations");
749    }
750
751    #[test]
752    fn test_migration_config_custom() {
753        let toml = r#"
754            [migrations]
755            directory = "./db/migrate"
756            auto_migrate = true
757            table_name = "schema_migrations"
758        "#;
759
760        let config = PraxConfig::from_str(toml).unwrap();
761        assert_eq!(config.migrations.directory, "./db/migrate");
762        assert!(config.migrations.auto_migrate);
763        assert_eq!(config.migrations.table_name, "schema_migrations");
764    }
765
766    // ==================== SeedConfig Tests ====================
767
768    #[test]
769    fn test_seed_config_default() {
770        let config = SeedConfig::default();
771        assert!(config.script.is_none());
772        assert!(!config.auto_seed);
773        assert!(config.environments.is_empty());
774    }
775
776    #[test]
777    fn test_seed_config_custom() {
778        let toml = r#"
779            [seed]
780            script = "seed.rs"
781            auto_seed = true
782
783            [seed.environments]
784            dev = true
785            prod = false
786        "#;
787
788        let config = PraxConfig::from_str(toml).unwrap();
789        assert_eq!(config.seed.script, Some("seed.rs".to_string()));
790        assert!(config.seed.auto_seed);
791        assert_eq!(config.seed.environments.get("dev"), Some(&true));
792        assert_eq!(config.seed.environments.get("prod"), Some(&false));
793    }
794
795    // ==================== DebugConfig Tests ====================
796
797    #[test]
798    fn test_debug_config_default() {
799        let config = DebugConfig::default();
800        assert!(!config.log_queries);
801        assert!(config.pretty_sql);
802        assert_eq!(config.slow_query_threshold, 1000);
803    }
804
805    #[test]
806    fn test_debug_config_custom() {
807        let toml = r#"
808            [debug]
809            log_queries = true
810            pretty_sql = false
811            slow_query_threshold = 200
812        "#;
813
814        let config = PraxConfig::from_str(toml).unwrap();
815        assert!(config.debug.log_queries);
816        assert!(!config.debug.pretty_sql);
817        assert_eq!(config.debug.slow_query_threshold, 200);
818    }
819
820    // ==================== Environment Variable Tests ====================
821
822    #[test]
823    fn test_env_var_expansion() {
824        // SAFETY: This test runs single-threaded and we clean up after
825        unsafe {
826            std::env::set_var("TEST_DB_URL", "postgres://test");
827        }
828        let expanded = expand_env_vars("url = \"${TEST_DB_URL}\"");
829        assert_eq!(expanded, "url = \"postgres://test\"");
830        unsafe {
831            std::env::remove_var("TEST_DB_URL");
832        }
833    }
834
835    #[test]
836    fn test_env_var_expansion_multiple() {
837        unsafe {
838            std::env::set_var("TEST_HOST", "localhost");
839            std::env::set_var("TEST_PORT", "5432");
840        }
841        let content = "host = \"${TEST_HOST}\"\nport = \"${TEST_PORT}\"";
842        let expanded = expand_env_vars(content);
843        assert!(expanded.contains("localhost"));
844        assert!(expanded.contains("5432"));
845        unsafe {
846            std::env::remove_var("TEST_HOST");
847            std::env::remove_var("TEST_PORT");
848        }
849    }
850
851    #[test]
852    fn test_env_var_expansion_missing_var() {
853        let content = "url = \"${DEFINITELY_NOT_SET_VAR_12345}\"";
854        let expanded = expand_env_vars(content);
855        // Should not expand missing variables
856        assert_eq!(expanded, content);
857    }
858
859    #[test]
860    fn test_env_var_expansion_in_config() {
861        unsafe {
862            std::env::set_var("TEST_DATABASE_URL_2", "postgres://user:pass@localhost/db");
863        }
864
865        let toml = r#"
866            [database]
867            url = "${TEST_DATABASE_URL_2}"
868        "#;
869
870        let config = PraxConfig::from_str(toml).unwrap();
871        assert_eq!(
872            config.database.url,
873            Some("postgres://user:pass@localhost/db".to_string())
874        );
875
876        unsafe {
877            std::env::remove_var("TEST_DATABASE_URL_2");
878        }
879    }
880
881    // ==================== Environment Override Tests ====================
882
883    #[test]
884    fn test_environment_override_database_url() {
885        let toml = r#"
886            [database]
887            url = "postgres://localhost/dev"
888
889            [environments.test]
890            [environments.test.database]
891            url = "postgres://localhost/test_db"
892        "#;
893
894        let config = PraxConfig::from_str(toml).unwrap().with_environment("test");
895
896        assert_eq!(
897            config.database.url,
898            Some("postgres://localhost/test_db".to_string())
899        );
900    }
901
902    #[test]
903    fn test_environment_override_pool() {
904        let toml = r#"
905            [database.pool]
906            max_connections = 10
907
908            [environments.production]
909            [environments.production.database.pool]
910            max_connections = 100
911            min_connections = 10
912        "#;
913
914        let config = PraxConfig::from_str(toml)
915            .unwrap()
916            .with_environment("production");
917
918        assert_eq!(config.database.pool.max_connections, 100);
919        assert_eq!(config.database.pool.min_connections, 10);
920    }
921
922    #[test]
923    fn test_environment_override_debug() {
924        let toml = r#"
925            [debug]
926            log_queries = false
927            pretty_sql = true
928
929            [environments.development]
930            [environments.development.debug]
931            log_queries = true
932            pretty_sql = false
933            slow_query_threshold = 50
934        "#;
935
936        let config = PraxConfig::from_str(toml)
937            .unwrap()
938            .with_environment("development");
939
940        assert!(config.debug.log_queries);
941        assert!(!config.debug.pretty_sql);
942        assert_eq!(config.debug.slow_query_threshold, 50);
943    }
944
945    // ==================== Serialization Tests ====================
946
947    #[test]
948    fn test_config_serialization() {
949        let config = PraxConfig::default();
950        let toml_str = toml::to_string(&config).unwrap();
951        assert!(toml_str.contains("[database]"));
952    }
953
954    #[test]
955    fn test_config_roundtrip() {
956        let original = PraxConfig {
957            database: DatabaseConfig {
958                provider: DatabaseProvider::MySql,
959                url: Some("mysql://localhost/test".to_string()),
960                pool: PoolConfig::default(),
961            },
962            ..Default::default()
963        };
964
965        let toml_str = toml::to_string(&original).unwrap();
966        let parsed: PraxConfig = toml::from_str(&toml_str).unwrap();
967
968        assert_eq!(parsed.database.provider, original.database.provider);
969        assert_eq!(parsed.database.url, original.database.url);
970    }
971
972    // ==================== Clone and Debug Tests ====================
973
974    #[test]
975    fn test_config_clone() {
976        let config = PraxConfig::default();
977        let cloned = config.clone();
978        assert_eq!(config.database.provider, cloned.database.provider);
979    }
980
981    #[test]
982    fn test_config_debug() {
983        let config = PraxConfig::default();
984        let debug_str = format!("{:?}", config);
985        assert!(debug_str.contains("PraxConfig"));
986    }
987
988    #[test]
989    fn test_provider_equality() {
990        assert_eq!(DatabaseProvider::PostgreSql, DatabaseProvider::PostgreSql);
991        assert_ne!(DatabaseProvider::PostgreSql, DatabaseProvider::MySql);
992    }
993
994    // ==================== Default Function Tests ====================
995
996    #[test]
997    fn test_default_functions() {
998        assert_eq!(default_provider(), DatabaseProvider::PostgreSql);
999        assert_eq!(default_min_connections(), 2);
1000        assert_eq!(default_max_connections(), 10);
1001        assert_eq!(default_connect_timeout(), "30s");
1002        assert_eq!(default_idle_timeout(), "10m");
1003        assert_eq!(default_max_lifetime(), "30m");
1004        assert_eq!(default_schema_path(), "schema.prax");
1005        assert_eq!(default_output(), "./src/generated");
1006        assert!(default_true());
1007        assert_eq!(default_migrations_dir(), "./migrations");
1008        assert_eq!(default_migrations_table(), "_prax_migrations");
1009        assert_eq!(default_slow_query_threshold(), 1000);
1010    }
1011}