1use std::path::Path;
21use std::sync::Arc;
22
23use config::{Config, Environment, File};
24use serde::Deserialize;
25use thiserror::Error;
26
27#[derive(Debug, Error)]
29pub enum PurwaConfigError {
30 #[error(transparent)]
31 Config(#[from] config::ConfigError),
32}
33
34#[derive(Debug, Clone, Deserialize)]
36#[serde(default)]
37pub struct AppSection {
38 pub name: String,
40}
41
42impl Default for AppSection {
43 fn default() -> Self {
44 Self {
45 name: "purwa-app".to_string(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Deserialize)]
52#[serde(default)]
53pub struct ServerSection {
54 pub host: String,
55 pub port: u16,
56}
57
58impl Default for ServerSection {
59 fn default() -> Self {
60 Self {
61 host: "0.0.0.0".to_string(),
62 port: 3000,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Default, Deserialize)]
69#[serde(default)]
70pub struct DatabaseSection {
71 pub url: Option<String>,
73}
74
75#[derive(Debug, Clone, Deserialize)]
77#[serde(default)]
78pub struct QueueSection {
79 pub redis_url: Option<String>,
81 pub name: String,
83}
84
85impl Default for QueueSection {
86 fn default() -> Self {
87 Self {
88 redis_url: None,
89 name: "default".to_string(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Deserialize)]
96#[serde(default)]
97pub struct InertiaSection {
98 pub asset_version: String,
100}
101
102impl Default for InertiaSection {
103 fn default() -> Self {
104 Self {
105 asset_version: "1".to_string(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Default, Deserialize)]
112#[serde(default)]
113pub struct AppConfig {
114 pub app: AppSection,
115 pub server: ServerSection,
116 pub database: DatabaseSection,
117 pub queue: QueueSection,
118 pub inertia: InertiaSection,
119}
120
121impl AppConfig {
122 pub fn load() -> Result<Arc<Self>, PurwaConfigError> {
124 Self::load_with_file(None)
125 }
126
127 pub fn load_with_file(purwa_toml: Option<&Path>) -> Result<Arc<Self>, PurwaConfigError> {
129 dotenvy::dotenv().ok();
130 let mut builder = Config::builder();
131 match purwa_toml {
132 Some(path) => {
133 builder = builder.add_source(File::from(path).required(true));
134 }
135 None => {
136 builder = builder.add_source(File::with_name("purwa").required(false));
137 }
138 }
139 builder = builder.add_source(
140 Environment::with_prefix("PURWA")
141 .separator("__")
142 .try_parsing(true),
143 );
144 let cfg = builder.build()?;
145 let app: AppConfig = cfg.try_deserialize()?;
146 Ok(Arc::new(app))
147 }
148
149 pub fn database_url(&self) -> Option<String> {
153 if let Some(ref u) = self.database.url {
154 let t = u.trim();
155 if !t.is_empty() {
156 return Some(t.to_string());
157 }
158 }
159 std::env::var("DATABASE_URL")
160 .ok()
161 .map(|s| s.trim().to_string())
162 .filter(|s| !s.is_empty())
163 }
164
165 pub fn queue_redis_url(&self) -> Option<String> {
169 if let Some(ref u) = self.queue.redis_url {
170 let t = u.trim();
171 if !t.is_empty() {
172 return Some(t.to_string());
173 }
174 }
175 std::env::var("REDIS_URL")
176 .ok()
177 .map(|s| s.trim().to_string())
178 .filter(|s| !s.is_empty())
179 }
180}