1use crate::model::RetryConfig;
16use figment::providers::{Env, Format, Serialized, Toml};
17use figment::value::Value;
18use figment::Figment;
19use serde::{Deserialize, Serialize};
20use std::path::{Path, PathBuf};
21use std::time::Duration;
22use url::Url;
23
24const ENV_VAR_PREFIX: &str = "GOLEM__";
25const ENV_VAR_NESTED_SEPARATOR: &str = "__";
26
27pub trait ConfigLoaderConfig: Default + Serialize + Deserialize<'static> {}
28impl<T: Default + Serialize + Deserialize<'static>> ConfigLoaderConfig for T {}
29
30pub type ConfigExample<T> = (&'static str, T);
31
32pub trait HasConfigExamples<T> {
33 fn examples() -> Vec<ConfigExample<T>>;
34}
35
36pub struct ConfigLoader<T: ConfigLoaderConfig> {
37 pub config_file_name: PathBuf,
38 pub make_examples: Option<fn() -> Vec<ConfigExample<T>>>,
39 config_type: std::marker::PhantomData<T>,
40}
41
42impl<T: ConfigLoaderConfig> ConfigLoader<T> {
43 pub fn new(config_file_name: &Path) -> ConfigLoader<T> {
44 let config_file_name = std::env::current_dir()
45 .expect("Failed to get current directory")
46 .join(config_file_name);
47 Self {
48 config_file_name,
49 make_examples: None,
50 config_type: std::marker::PhantomData,
51 }
52 }
53
54 pub fn new_with_examples(config_file_name: &Path) -> ConfigLoader<T>
55 where
56 T: HasConfigExamples<T>,
57 {
58 let config_file_name = std::env::current_dir()
59 .expect("Failed to get current directory")
60 .join(config_file_name);
61 Self {
62 config_file_name,
63 make_examples: Some(T::examples),
64 config_type: std::marker::PhantomData,
65 }
66 }
67
68 pub fn default_figment(&self) -> Figment {
69 Figment::new().merge(Serialized::defaults(T::default()))
70 }
71
72 pub fn example_figments(&self) -> Vec<(&'static str, Figment)> {
73 self.make_examples
74 .map_or(Vec::new(), |make| make())
75 .into_iter()
76 .map(|(name, example)| (name, Figment::new().merge(Serialized::defaults(example))))
77 .collect()
78 }
79
80 pub fn figment(&self) -> Figment {
81 Figment::new()
82 .merge(Serialized::defaults(T::default()))
83 .merge(Toml::file_exact(self.config_file_name.clone()))
84 .merge(env_config_provider())
85 }
86
87 pub fn load(&self) -> figment::Result<T> {
88 self.figment().extract()
89 }
90
91 fn default_dump_source(&self) -> dump::Source {
92 dump::Source::Default {
93 default: self.default_figment(),
94 examples: self.example_figments(),
95 }
96 }
97
98 fn loaded_dump_source(&self) -> dump::Source {
99 dump::Source::Loaded(self.figment())
100 }
101
102 pub fn load_or_dump_config(&self) -> Option<T> {
108 let args: Vec<String> = std::env::args().collect();
109 match args.get(1).map_or("", |a| a.as_str()) {
110 "--dump-config-default" => self.dump(self.default_dump_source(), &dump::print(false)),
111 "--dump-config-default-env-var" => {
112 self.dump(self.default_dump_source(), &dump::env_var())
113 }
114 "--dump-config-default-toml" => self.dump(self.default_dump_source(), &dump::toml()),
115 "--dump-config" => self.dump(self.loaded_dump_source(), &dump::print(true)),
116 "--dump-config-env-var" => self.dump(self.loaded_dump_source(), &dump::env_var()),
117 "--dump-config-toml" => self.dump(self.loaded_dump_source(), &dump::toml()),
118 other => {
119 if other.starts_with("--dump-config") {
120 panic!("Unknown dump config parameter: {}", other);
121 } else {
122 match self.load() {
123 Ok(config) => Some(config),
124 Err(err) => {
125 eprintln!("Failed to load config: {err}");
126 None
127 }
128 }
129 }
130 }
131 }
132 }
133
134 fn dump<U>(&self, source: dump::Source, dump: &dump::Dump) -> Option<U> {
135 let extract =
136 |figment: &Figment| -> Value { figment.extract().expect("Failed to extract config") };
137
138 let dump_figment = |header: &str, is_example: bool, figment: &Figment| match dump {
139 dump::Dump::RootValue(dump) => dump.root_value(header, is_example, &extract(figment)),
140 dump::Dump::Visitor(dump) => {
141 dump.begin(header, is_example);
142 Self::visit_dump(
143 figment,
144 dump.as_ref(),
145 &mut Vec::new(),
146 "",
147 &extract(figment),
148 )
149 }
150 };
151
152 match source {
153 dump::Source::Default { default, examples } => {
154 dump_figment("Generated from default config", false, &default);
155 for (name, example) in examples {
156 dump_figment(
157 &format!("Generated from example config: {}", name),
158 true,
159 &example,
160 );
161 }
162 }
163 dump::Source::Loaded(loaded) => {
164 dump_figment("Generated from loaded config", false, &loaded);
165 }
166 }
167
168 None
169 }
170
171 fn visit_dump<'a>(
172 figment: &Figment,
173 dump: &dyn dump::VisitorDump,
174 path: &mut Vec<&'a str>,
175 name: &'a str,
176 value: &'a Value,
177 ) {
178 match value {
179 Value::Dict(_, dict) => {
180 if !name.is_empty() {
181 path.push(name);
182 }
183
184 for (name, value) in dict.iter().filter(|(_, v)| v.as_dict().is_none()) {
185 Self::visit_dump(figment, dump, path, name, value)
186 }
187 for (name, value) in dict.iter().filter(|(_, v)| v.as_dict().is_some()) {
188 Self::visit_dump(figment, dump, path, name, value)
189 }
190
191 if !name.is_empty() {
192 path.pop();
193 }
194 }
195 value => dump.value(path, name, figment.get_metadata(value.tag()), value),
196 }
197 }
198}
199
200pub(crate) mod dump {
201 use crate::config::{ENV_VAR_NESTED_SEPARATOR, ENV_VAR_PREFIX};
202 use figment::value::Value;
203 use figment::{Figment, Metadata};
204
205 pub enum Source {
206 Default {
207 default: Figment,
208 examples: Vec<(&'static str, Figment)>,
209 },
210 Loaded(Figment),
211 }
212
213 pub fn print(show_source: bool) -> Dump {
214 Dump::Visitor(Box::new(Print { show_source }))
215 }
216
217 pub fn env_var() -> Dump {
218 Dump::Visitor(Box::new(EnvVar))
219 }
220
221 pub fn toml() -> Dump {
222 Dump::RootValue(Box::new(Toml))
223 }
224
225 pub enum Dump {
226 RootValue(Box<dyn RootValueDump>),
227 Visitor(Box<dyn VisitorDump>),
228 }
229
230 pub trait RootValueDump {
231 fn root_value(&self, header: &str, is_example: bool, value: &Value);
232 }
233
234 pub trait VisitorDump {
235 fn begin(&self, header: &str, is_example: bool);
236 fn value(&self, path: &[&str], name: &str, metadata: Option<&Metadata>, value: &Value);
237 }
238
239 struct Print {
240 pub show_source: bool,
241 }
242
243 impl VisitorDump for Print {
244 fn begin(&self, header: &str, is_example: bool) {
245 if is_example {
246 println!();
247 }
248 println!(":: {}\n", header)
249 }
250
251 fn value(&self, path: &[&str], name: &str, metadata: Option<&Metadata>, value: &Value) {
252 if !path.is_empty() {
253 for elem in path {
254 print!("{}.", elem);
255 }
256 }
257 if !name.is_empty() {
258 print!("{}", name);
259 }
260
261 print!(
262 ": {}",
263 serde_json::to_string(value).expect("Failed to pretty print value")
264 );
265
266 if self.show_source {
267 let name = metadata.map_or("unknown", |m| &m.name);
268 let source = metadata
269 .and_then(|m| m.source.as_ref())
270 .map_or("unknown".to_owned(), |s| s.to_string());
271 println!(" ({} - {})", name, source);
272 } else {
273 println!()
274 }
275 }
276 }
277
278 struct EnvVar;
279
280 impl VisitorDump for EnvVar {
281 fn begin(&self, header: &str, is_example: bool) {
282 if is_example {
283 println!();
284 }
285 println!("### {}\n", header)
286 }
287
288 fn value(&self, path: &[&str], name: &str, _metadata: Option<&Metadata>, value: &Value) {
289 let is_empty_value = value.to_empty().is_some();
290
291 if is_empty_value {
292 print!("#");
293 }
294
295 print!("{}", ENV_VAR_PREFIX);
296
297 if !path.is_empty() {
298 for elem in path {
299 print!("{}{}", elem.to_uppercase(), ENV_VAR_NESTED_SEPARATOR);
300 }
301 }
302 if !name.is_empty() {
303 print!("{}", name.to_uppercase());
304 }
305
306 if !is_empty_value {
307 println!(
308 "={}",
309 serde_json::to_string(value).expect("Failed to pretty print value")
310 );
311 } else {
312 println!("=");
313 }
314 }
315 }
316
317 pub struct Toml;
318
319 impl RootValueDump for Toml {
320 fn root_value(&self, header: &str, is_example: bool, value: &Value) {
321 if is_example {
322 println!();
323 }
324 println!("## {}", header);
325
326 let mut config_as_toml_str =
327 toml::to_string(value).expect("Failed to serialize as TOML");
328
329 if is_example {
330 config_as_toml_str = config_as_toml_str
331 .lines()
332 .map(|l| format!("# {}", l))
333 .collect::<Vec<String>>()
334 .join("\n")
335 }
336
337 println!("{}", config_as_toml_str);
338 }
339 }
340}
341
342#[derive(Clone, Debug, Serialize, Deserialize)]
343pub struct RedisConfig {
344 pub host: String,
345 pub port: u16,
346 pub database: usize,
347 pub tracing: bool,
348 pub pool_size: usize,
349 pub retries: RetryConfig,
350 pub key_prefix: String,
351 pub username: Option<String>,
352 pub password: Option<String>,
353}
354
355impl RedisConfig {
356 pub fn url(&self) -> Url {
357 Url::parse(&format!(
358 "redis://{}:{}/{}",
359 self.host, self.port, self.database
360 ))
361 .expect("Failed to parse Redis URL")
362 }
363}
364
365impl Default for RedisConfig {
366 fn default() -> Self {
367 Self {
368 host: "localhost".to_string(),
369 port: 6380,
370 database: 0,
371 tracing: false,
372 pool_size: 8,
373 retries: RetryConfig::default(),
374 key_prefix: "".to_string(),
375 username: None,
376 password: None,
377 }
378 }
379}
380
381impl Default for RetryConfig {
382 fn default() -> Self {
383 Self::max_attempts_5()
384 }
385}
386
387impl RetryConfig {
388 pub fn max_attempts_3() -> RetryConfig {
389 Self {
390 max_attempts: 3,
391 min_delay: Duration::from_millis(100),
392 max_delay: Duration::from_secs(1),
393 multiplier: 3.0,
394 max_jitter_factor: Some(0.15),
395 }
396 }
397
398 pub fn max_attempts_5() -> RetryConfig {
399 Self {
400 max_attempts: 5,
401 min_delay: Duration::from_millis(100),
402 max_delay: Duration::from_secs(2),
403 multiplier: 2.0,
404 max_jitter_factor: Some(0.15),
405 }
406 }
407}
408
409pub fn env_config_provider() -> Env {
410 Env::prefixed(ENV_VAR_PREFIX).split(ENV_VAR_NESTED_SEPARATOR)
411}
412
413#[derive(Clone, Debug, Serialize, Deserialize)]
414#[serde(tag = "type", content = "config")]
415pub enum DbConfig {
416 Postgres(DbPostgresConfig),
417 Sqlite(DbSqliteConfig),
418}
419
420impl Default for DbConfig {
421 fn default() -> Self {
422 DbConfig::Sqlite(DbSqliteConfig {
423 database: "golem_service.db".to_string(),
424 max_connections: 10,
425 })
426 }
427}
428
429impl DbConfig {
430 pub fn postgres_example() -> Self {
431 Self::Postgres(DbPostgresConfig {
432 host: "localhost".to_string(),
433 database: "postgres".to_string(),
434 username: "postgres".to_string(),
435 password: "postgres".to_string(),
436 port: 5432,
437 max_connections: 10,
438 schema: None,
439 })
440 }
441}
442
443#[derive(Clone, Debug, Serialize, Deserialize)]
444pub struct DbSqliteConfig {
445 pub database: String,
446 pub max_connections: u32,
447}
448
449impl DbSqliteConfig {
450 #[cfg(feature = "sql")]
451 pub fn connect_options(&self) -> sqlx::sqlite::SqliteConnectOptions {
452 sqlx::sqlite::SqliteConnectOptions::new()
453 .filename(&self.database)
454 .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
455 .create_if_missing(true)
456 }
457}
458
459#[derive(Clone, Debug, Serialize, Deserialize)]
460pub struct DbPostgresConfig {
461 pub host: String,
462 pub database: String,
463 pub username: String,
464 pub password: String,
465 pub port: u16,
466 pub max_connections: u32,
467 pub schema: Option<String>,
468}
469
470impl DbPostgresConfig {
471 #[cfg(feature = "sql")]
472 pub fn connect_options(&self) -> sqlx::postgres::PgConnectOptions {
473 sqlx::postgres::PgConnectOptions::new()
474 .host(&self.host)
475 .port(self.port)
476 .database(&self.database)
477 .username(&self.username)
478 .password(&self.password)
479 }
480}