1pub mod backend;
2pub mod frontend;
3pub mod load;
4pub mod mailer;
5pub mod opentelemetry;
6pub mod postgres;
7pub mod scrypt;
8pub mod seed;
9pub mod sqlite;
10
11use crate::backend::{BackendConfig, BackendConfigPatch};
12use crate::frontend::{FrontendConfig, FrontendConfigPatch};
13use crate::load::{ConfigRoot, ConfigRoots};
14use crate::mailer::{MailerConfig, MailerConfigPatch};
15use crate::opentelemetry::{OpentelemetryConfig, OpentelemetryConfigPatch};
16use crate::postgres::{PostgresConfig, PostgresConfigPatch};
17use crate::scrypt::{ScryptConfig, ScryptConfigPatch};
18use crate::seed::{SeedConfig, SeedConfigPatch};
19use crate::sqlite::{SqliteConfig, SqliteConfigPatch};
20use eternaltwin_core::patch::SimplePatch;
21use eternaltwin_core::types::{DisplayErrorChain, WeakError};
22use serde::{Deserialize, Serialize};
23use std::borrow::Borrow;
24use std::collections::hash_map::Entry as HashMapEntry;
25use std::collections::{HashMap, HashSet};
26use std::convert::Infallible;
27use std::fmt::Debug;
28use std::fs;
29use std::io;
30use std::path::{Path, PathBuf};
31use std::string::ToString;
32use std::sync::RwLock;
33use url::Url;
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
36pub struct ConfigChain {
37 pub items: Vec<ResolvedConfig>,
38 pub default: BuiltinEternaltwinConfigProfile,
39}
40
41impl ConfigChain {
42 pub fn merge(self) -> Config<ConfigSource> {
43 let mut cur = Config::default_for_profile(self.default);
44 for item in self.items.into_iter().rev() {
45 cur = cur.patch(item.config, item.source.clone());
46 }
47 cur
48 }
49
50 #[cfg(test)]
51 pub(crate) fn one(resolved: ResolvedConfig, default: BuiltinEternaltwinConfigProfile) -> Self {
52 Self {
53 items: vec![resolved],
54 default,
55 }
56 }
57
58 pub fn resolve_from_roots(roots: &[ConfigRoot], working_dir: &Path, profile: &EternaltwinConfigProfile) -> Self {
59 let mut resolver = ConfigResolver::new();
60 for root in roots.iter().rev() {
61 resolver.add_root(root, working_dir)
62 }
63 Self {
64 items: resolver.resolved,
65 default: profile.as_builtin_or_dev(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
71pub struct ResolvedConfig {
72 pub source: ConfigSource,
73 format: ConfigFormat,
74 config: ConfigPatch,
75}
76
77impl ResolvedConfig {
78 pub fn parse(s: &str, format: ConfigFormat, prefer_json: bool, source: ConfigSource) -> Result<Self, WeakError> {
79 let (format, partial) = match format {
80 ConfigFormat::Auto => {
81 if prefer_json {
82 match serde_json::from_str(s) {
83 Ok(p) => (ConfigFormat::Json, p),
84 Err(e) => toml::de::from_str(s)
85 .map(|p| (ConfigFormat::Toml, p))
86 .map_err(|_| WeakError::wrap(e))?,
87 }
88 } else {
89 match toml::de::from_str(s) {
90 Ok(p) => (ConfigFormat::Toml, p),
91 Err(e) => serde_json::from_str(s)
92 .map(|p| (ConfigFormat::Json, p))
93 .map_err(|_| WeakError::wrap(e))?,
94 }
95 }
96 }
97 ConfigFormat::Json => (ConfigFormat::Json, serde_json::from_str(s).map_err(WeakError::wrap)?),
98 ConfigFormat::Toml => (ConfigFormat::Toml, toml::de::from_str(s).map_err(WeakError::wrap)?),
99 };
100 Ok(Self {
101 source,
102 format,
103 config: partial,
104 })
105 }
106
107 pub fn extends(&self, working_dir: &Url) -> Vec<(Url, ConfigFormat)> {
108 let extends = match self.config.extends.as_ref() {
109 Some(extends) => extends,
110 None => return Vec::new(),
111 };
112
113 let base_url = match &self.source {
114 ConfigSource::File(Some(file)) => Url::from_file_path(file).expect("invalid file path"),
115 _ => working_dir.clone(),
116 };
117
118 match extends {
119 ConfigRefOrList::One(e) => Self::extends_inner(&base_url, &[e]),
120 ConfigRefOrList::Many(e) => Self::extends_inner(&base_url, e),
121 }
122 }
123
124 fn extends_inner<Cr: Borrow<ConfigRef>>(base_url: &Url, extends: &[Cr]) -> Vec<(Url, ConfigFormat)> {
125 let mut result: Vec<(Url, ConfigFormat)> = Vec::new();
126 for e in extends {
127 let (url, format) = match e.borrow() {
128 ConfigRef::Url(u) => (u, ConfigFormat::Auto),
129 ConfigRef::Typed { url, format } => (url, *format),
130 };
131 let url = resolve_config_ref(base_url, url);
132 result.push((url, format));
133 }
134 result
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
139struct ConfigPatch {
140 extends: Option<ConfigRefOrList>,
141 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
142 backend: SimplePatch<BackendConfigPatch>,
143 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
144 frontend: SimplePatch<FrontendConfigPatch>,
145 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
146 postgres: SimplePatch<PostgresConfigPatch>,
147 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
148 mailer: SimplePatch<MailerConfigPatch>,
149 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
150 scrypt: SimplePatch<ScryptConfigPatch>,
151 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
152 opentelemetry: SimplePatch<OpentelemetryConfigPatch>,
153 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
154 seed: SimplePatch<Option<SeedConfigPatch>>,
155 #[serde(default, skip_serializing_if = "SimplePatch::is_skip")]
156 sqlite: SimplePatch<SqliteConfigPatch>,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[serde(untagged)]
161enum ConfigRefOrList {
162 One(ConfigRef),
163 Many(Vec<ConfigRef>),
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
167#[serde(untagged)]
168enum ConfigRef {
169 Url(String),
170 Typed { url: String, format: ConfigFormat },
171}
172
173struct FsCache {
174 state: RwLock<HashMap<PathBuf, io::Result<String>>>,
175}
176
177impl FsCache {
178 pub fn new() -> Self {
179 Self {
180 state: RwLock::new(HashMap::new()),
181 }
182 }
183
184 pub fn read_to_string<F, R>(&self, p: &Path, f: F) -> R
185 where
186 F: for<'a> Fn(&'a io::Result<String>) -> R,
187 {
188 {
189 if let Some(r) = self.state.read().expect("lock is poisoned").get(p) {
190 return f(r);
191 }
192 }
193 let mut s = self.state.write().expect("lock is poisoned");
194 match s.entry(p.to_path_buf()) {
195 HashMapEntry::Vacant(e) => {
196 let p = e.key();
197 let r = fs::read_to_string(p);
198 let r = e.insert(r);
199 f(r)
200 }
201 HashMapEntry::Occupied(e) => f(e.get()),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
207enum TryResolveUrlError {
208 #[error("resource not found")]
209 NotFound,
210 #[error("unexpected URL scheme {0:?}")]
211 Scheme(String),
212 #[error("unexpected io error")]
213 Io(#[source] WeakError),
214}
215
216struct ConfigResolver {
217 resolved: Vec<ResolvedConfig>,
218 fs: FsCache,
219}
220
221impl ConfigResolver {
222 pub(crate) fn new() -> Self {
223 Self {
224 resolved: Vec::new(),
225 fs: FsCache::new(),
226 }
227 }
228
229 pub(crate) fn add_root(&mut self, root: &ConfigRoot, working_dir: &Path) {
230 let working_dir_url = Url::from_directory_path(working_dir).expect("invalid working dir");
231 let resolved = match root {
232 ConfigRoot::Data { data, format } => Some(self.add_data(data, format)),
233 ConfigRoot::Search { patterns, format } => self.add_search(patterns, format, working_dir.to_path_buf()),
234 ConfigRoot::Url { url, format } => Some(self.add_url(&working_dir_url, url, format)),
235 };
236 let resolved: &ResolvedConfig = match resolved {
237 None => return,
238 Some(resolved) => resolved,
239 };
240 let mut stack = resolved.extends(&working_dir_url);
241 while let Some((url, format)) = stack.pop() {
242 let dependency = self
243 .try_resolve_url(&url, format)
244 .expect("failed to resolve base config");
245 stack.extend_from_slice(&dependency.extends(&working_dir_url));
246 self.resolved.push(dependency);
247 }
248 }
249
250 fn add_data(
251 &mut self,
252 data: &ConfigValue<String, LoadConfigSource>,
253 format: &ConfigValue<ConfigFormat, LoadConfigSource>,
254 ) -> &ResolvedConfig {
255 let resolved_source: ConfigSource = match data.meta {
256 LoadConfigSource::Args => ConfigSource::Cli,
257 LoadConfigSource::Env => ConfigSource::Env,
258 LoadConfigSource::Default(_) => ConfigSource::Default,
259 };
260 let resolved = ResolvedConfig::parse(&data.value, format.value, true, resolved_source).expect("invalid config");
261 self.resolved.push(resolved);
262 self.resolved.last().expect("`self.resolved` is non-empty")
263 }
264
265 fn add_search(
266 &mut self,
267 patterns: &ConfigValue<Vec<String>, LoadConfigSource>,
268 format: &ConfigValue<ConfigFormat, LoadConfigSource>,
269 start_dir: PathBuf,
270 ) -> Option<&ResolvedConfig> {
271 let mut visited: HashSet<PathBuf> = HashSet::new();
272 let mut cur: PathBuf = start_dir;
273 loop {
274 let cur_url = Url::from_directory_path(&cur).expect("invalid cur search dir");
275 for pat in &patterns.value {
276 let is_relative_path = pat.starts_with("./") || pat.starts_with("../");
277 if !is_relative_path {
278 panic!(r#"search pattern must start with "./" or "../", received {pat}"#);
279 }
280 let target = Url::options().base_url(Some(&cur_url)).parse(pat);
281 let target = match target {
282 Ok(t) => t,
283 Err(e) => panic!("failed to build file url from pattern {pat:?} and base {cur_url}: {e:?}"),
284 };
285 match self.try_resolve_url(&target, format.value) {
286 Ok(r) => {
287 self.resolved.push(r);
288 return Some(self.resolved.last().expect("`self.resolved` is non-empty"));
289 }
290 Err(TryResolveUrlError::NotFound) => continue,
291 Err(e) => panic!("read error for {target}: {}", DisplayErrorChain(&e)),
292 };
293 }
294 let parent: Option<PathBuf> = cur.parent().map(|p| p.to_path_buf());
295 if let Some(parent) = parent {
296 visited.insert(cur);
297 if visited.contains(parent.as_path()) {
298 return None;
301 } else {
302 cur = parent;
303 }
304 } else {
305 return None;
307 }
308 }
309 }
310
311 fn add_url(
312 &mut self,
313 working_dir: &Url,
314 url: &ConfigValue<String, LoadConfigSource>,
315 format: &ConfigValue<ConfigFormat, LoadConfigSource>,
316 ) -> &ResolvedConfig {
317 let url = resolve_config_ref(working_dir, &url.value);
318 match self.try_resolve_url(&url, format.value) {
319 Ok(r) => {
320 self.resolved.push(r);
321 self.resolved.last().expect("`self.resolved` is non-empty")
322 }
323 Err(e) => panic!("read error for {url}: {}", DisplayErrorChain(&e)),
324 }
325 }
326
327 fn try_resolve_url(&self, url: &Url, format: ConfigFormat) -> Result<ResolvedConfig, TryResolveUrlError> {
328 match url.scheme() {
329 "file" => {
330 let path = url.to_file_path().expect("must be a file url");
331 self.fs.read_to_string(&path, |r| match r.as_deref() {
332 Ok(s) => {
333 let resolved_source = ConfigSource::File(Some(path.clone()));
334 let resolved = ResolvedConfig::parse(s, format, false, resolved_source).expect("config format error");
335 Ok(resolved)
336 }
337 Err(e) => Err(match e.kind() {
338 io::ErrorKind::NotFound => TryResolveUrlError::NotFound,
339 _ => TryResolveUrlError::Io(WeakError::wrap(e)),
340 }),
341 })
342 }
343 s => Err(TryResolveUrlError::Scheme(s.to_string())),
344 }
345 }
346}
347
348fn resolve_config_ref(base: &Url, url: &str) -> Url {
349 let parsed = if url.starts_with("./") || url.starts_with("../") {
350 Url::options().base_url(Some(base)).parse(url)
351 } else {
352 Url::parse(url)
353 };
354 match parsed {
355 Ok(u) => u,
356 Err(e) => panic!("invalid url {url}: {}", DisplayErrorChain(&e)),
357 }
358}
359
360pub trait ConfigField<TyContainer, TyValue> {
365 fn path(&self) -> &[&str];
367
368 fn get(&self, container: TyContainer) -> TyValue;
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
375pub struct ConfigValue<T, M = ()> {
376 pub value: T,
377 pub meta: M,
378}
379
380impl<T, M> ConfigValue<T, M>
381where
382 M: Default,
383{
384 pub fn new(value: T) -> Self {
385 Self {
386 value,
387 meta: M::default(),
388 }
389 }
390}
391
392impl<T, M> ConfigValue<T, M> {
393 pub fn new_meta(value: T, meta: M) -> Self {
394 Self { value, meta }
395 }
396
397 pub fn clone_without_meta(&self) -> ConfigValue<T, ()>
398 where
399 T: Clone,
400 {
401 ConfigValue {
402 value: self.value.clone(),
403 meta: (),
404 }
405 }
406
407 pub fn map<U, F>(self, f: F) -> ConfigValue<U, M>
411 where
412 F: FnOnce(T) -> U,
413 {
414 ConfigValue {
415 value: f(self.value),
416 meta: self.meta,
417 }
418 }
419
420 pub fn map_meta<N, F>(self, f: F) -> ConfigValue<T, N>
424 where
425 F: FnOnce(M) -> N,
426 {
427 ConfigValue {
428 value: self.value,
429 meta: f(self.meta),
430 }
431 }
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
436pub struct ConfigEntry<F, V> {
437 pub field: F,
438 pub value: V,
439}
440
441pub type DebugConfigValue<T> = ConfigValue<T, ConfigMeta<T, LoadConfigSource>>;
442
443pub type DebugConfigEntry<'a> = ConfigEntry<&'a [&'a str], DebugConfigValue<&'a dyn Debug>>;
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
447pub enum LoadConfigSource {
448 Args,
449 Env,
450 Default(BuiltinEternaltwinConfigProfile),
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
455pub struct ConfigMeta<V, S> {
456 pub default: V,
457 pub source: S,
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
461pub enum BuiltinEternaltwinConfigProfile {
462 Production,
463 Dev,
464 Test,
465 Sdk,
466}
467
468impl BuiltinEternaltwinConfigProfile {
469 pub fn as_str(&self) -> &str {
470 match &self {
471 Self::Production => "production",
472 Self::Dev => "dev",
473 Self::Test => "test",
474 Self::Sdk => "sdk",
475 }
476 }
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
480pub enum EternaltwinConfigProfile {
481 Production,
482 Dev,
483 Test,
484 Sdk,
485 Custom(String),
486}
487
488impl EternaltwinConfigProfile {
489 pub fn as_str(&self) -> &str {
490 match &self {
491 Self::Production => "production",
492 Self::Dev => "dev",
493 Self::Test => "test",
494 Self::Sdk => "sdk",
495 Self::Custom(ref profile) => profile.as_str(),
496 }
497 }
498
499 pub fn parse(s: &str) -> Self {
500 match s {
501 "production" => Self::Production,
502 "dev" => Self::Dev,
503 "test" => Self::Test,
504 "sdk" => Self::Sdk,
505 s => Self::Custom(s.to_string()),
506 }
507 }
508
509 pub fn as_builtin_or_dev(&self) -> BuiltinEternaltwinConfigProfile {
510 match &self {
511 Self::Production => BuiltinEternaltwinConfigProfile::Production,
512 Self::Dev => BuiltinEternaltwinConfigProfile::Dev,
513 Self::Test => BuiltinEternaltwinConfigProfile::Test,
514 Self::Sdk => BuiltinEternaltwinConfigProfile::Sdk,
515 Self::Custom(_) => BuiltinEternaltwinConfigProfile::Dev,
516 }
517 }
518}
519
520impl core::str::FromStr for EternaltwinConfigProfile {
521 type Err = Infallible;
522
523 fn from_str(s: &str) -> Result<Self, Self::Err> {
524 Ok(Self::parse(s))
525 }
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
529pub enum ConfigFormat {
530 Auto,
531 Json,
532 Toml,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
536#[error("invalid config format: possible values are `auto`, `json`, `toml`")]
537pub struct InvalidConfigFormat;
538
539impl ConfigFormat {
540 pub fn as_str(&self) -> &str {
541 match &self {
542 Self::Auto => "auto",
543 Self::Json => "json",
544 Self::Toml => "toml",
545 }
546 }
547
548 pub fn parse(s: &str) -> Result<Self, InvalidConfigFormat> {
549 match s {
550 "auto" => Ok(Self::Auto),
551 "json" => Ok(Self::Json),
552 "toml" => Ok(Self::Toml),
553 _ => Err(InvalidConfigFormat),
554 }
555 }
556}
557
558impl core::str::FromStr for ConfigFormat {
559 type Err = InvalidConfigFormat;
560
561 fn from_str(s: &str) -> Result<Self, Self::Err> {
562 Self::parse(s)
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
567pub enum ConfigSource {
568 Default,
569 File(Option<PathBuf>),
570 Cli,
571 Env,
572}
573
574pub enum InputConfigSource {
575 Default,
576 Cwd(PathBuf),
577 Cli,
578 Env,
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
582pub enum ClockConfig {
583 System,
584 Virtual,
585}
586
587#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
588pub enum StoreConfig {
589 Memory,
590 Postgres,
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
594pub enum SqliteStoreConfig {
595 Memory,
596 Postgres,
597 Sqlite,
598}
599
600impl From<StoreConfig> for SqliteStoreConfig {
601 fn from(value: StoreConfig) -> Self {
602 match value {
603 StoreConfig::Memory => Self::Memory,
604 StoreConfig::Postgres => Self::Postgres,
605 }
606 }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
610pub enum ClientConfig {
611 Mock,
612 Network,
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
616pub enum MailerType {
617 Mock,
618 Network,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
622pub struct Config<TyMeta = ()> {
623 pub backend: BackendConfig<TyMeta>,
624 pub frontend: FrontendConfig<TyMeta>,
625 pub postgres: PostgresConfig<TyMeta>,
626 pub mailer: MailerConfig<TyMeta>,
627 pub scrypt: ScryptConfig<TyMeta>,
628 pub opentelemetry: OpentelemetryConfig<TyMeta>,
629 pub seed: SeedConfig<TyMeta>,
630 pub sqlite: SqliteConfig<TyMeta>,
631}
632
633impl Config<ConfigSource> {
634 pub fn for_test() -> Self {
635 let cwd = std::env::current_dir().expect("failed to resolve cwd");
636 let profile = EternaltwinConfigProfile::Test;
637 let roots = ConfigRoots::default_value(&profile);
638 let chain = ConfigChain::resolve_from_roots(&roots, cwd.as_path(), &profile);
639 chain.merge()
640 }
641
642 fn patch(mut self, config: ConfigPatch, meta: ConfigSource) -> Self {
643 if let SimplePatch::Set(backend) = config.backend {
644 self.backend = self.backend.patch(backend, meta.clone());
645 }
646 if let SimplePatch::Set(frontend) = config.frontend {
647 self.frontend = self.frontend.patch(frontend, meta.clone());
648 }
649 if let SimplePatch::Set(postgres) = config.postgres {
650 self.postgres = self.postgres.patch(postgres, meta.clone());
651 }
652 if let SimplePatch::Set(mailer) = config.mailer {
653 self.mailer = self.mailer.patch(mailer, meta.clone());
654 }
655 if let SimplePatch::Set(scrypt) = config.scrypt {
656 self.scrypt = self.scrypt.patch(scrypt, meta.clone());
657 }
658 if let SimplePatch::Set(opentelemetry) = config.opentelemetry {
659 self.opentelemetry = self.opentelemetry.patch(opentelemetry, meta.clone());
660 }
661 if let SimplePatch::Set(seed) = config.seed {
662 let patch = match seed {
663 None => SeedConfigPatch {
665 user: SimplePatch::Set(None),
666 app: SimplePatch::Set(None),
667 forum_section: SimplePatch::Set(None),
668 },
669 Some(patch) => patch,
670 };
671 self.seed = self.seed.patch(patch, meta.clone());
672 }
673 if let SimplePatch::Set(sqlite) = config.sqlite {
674 self.sqlite = self.sqlite.patch(sqlite, meta.clone());
675 }
676 self
677 }
678
679 fn default_for_profile(profile: BuiltinEternaltwinConfigProfile) -> Self {
680 Self {
681 backend: BackendConfig::default_for_profile(profile),
682 frontend: FrontendConfig::default_for_profile(profile),
683 postgres: PostgresConfig::default_for_profile(profile),
684 mailer: MailerConfig::default_for_profile(profile),
685 scrypt: ScryptConfig::default_for_profile(profile),
686 opentelemetry: OpentelemetryConfig::default_for_profile(profile),
687 seed: SeedConfig::default_for_profile(profile),
688 sqlite: SqliteConfig::default_for_profile(profile),
689 }
690 }
691}
692
693#[cfg(test)]
694mod test {
695 use super::*;
696
697 #[test]
698 fn test_default_dev_config() {
699 const INPUT: &str = r#"
702[backend]
703listen = "[::]:50320"
704secret = "dev"
705clock = "System"
706mailer = "Mock"
707store = "Memory"
708oauth_client = "Network"
709
710[frontend]
711port = 50321
712uri = "http://localhost:50321/"
713forum_posts_per_page = 10
714forum_threads_per_page = 20
715
716[postgres]
717host = "localhost"
718port = 5432
719name = "eternaltwin.dev"
720user = "eternaltwin.dev.main"
721password = "dev"
722max_connections = 50
723
724[sqlite]
725file = "./eternaltwin.dev.sqlite"
726max_connections = 50
727
728[mailer]
729host = "localhost"
730username = "eternaltwin_mailer"
731password = "dev"
732sender = "support@eternaltwin.localhost"
733headers = [
734]
735
736[scrypt]
737max_time = "100ms"
738max_mem_frac = 0.05
739
740[opentelemetry]
741enabled = true
742grpc_proxy_port = 4317
743
744[opentelemetry.attributes]
745[opentelemetry.exporter]
746[opentelemetry.exporter.stdout]
747type = "Human"
748color = "Auto"
749target = "eternaltwin://stdout"
750
751[seed]
752[seed.user]
753[seed.user.alice]
754display_name = "Alice"
755username = "alice"
756password = "aaaaaaaaaa"
757is_administrator = true
758[seed.user.bob]
759display_name = "Bob"
760username = "bob"
761password = "bbbbbbbbbb"
762[seed.user.charlie]
763display_name = "Charlie"
764username = "charlie"
765password = "cccccccccc"
766[seed.user.dan]
767display_name = "Dan"
768username = "dan"
769password = "dddddddddd"
770[seed.user.eve]
771display_name = "Eve"
772username = "eve"
773password = "eeeeeeeeee"
774[seed.user.frank]
775display_name = "Frank"
776username = "frank"
777password = "ffffffffff"
778
779[seed.app]
780[seed.app.brute_dev]
781display_name = "LaBrute"
782uri = "http://localhost:3000/"
783oauth_callback = "http://localhost:3000/oauth/callback"
784secret = "dev"
785
786[seed.app.emush_dev]
787display_name = "eMush"
788uri = "http://emush.localhost/"
789oauth_callback = "http://emush.localhost/oauth/callback"
790secret = "dev"
791
792[seed.app.eternalfest_dev]
793display_name = "Eternalfest"
794uri = "http://localhost:50313/"
795oauth_callback = "http://localhost:50313/oauth/callback"
796secret = "dev"
797
798[seed.app.kadokadeo_dev]
799display_name = "Kadokadeo"
800uri = "http://kadokadeo.localhost/"
801oauth_callback = "http://kadokadeo.localhost/oauth/callback"
802secret = "dev"
803
804[seed.app.kingdom_dev]
805display_name = "Kingdom"
806uri = "http://localhost:8000/"
807oauth_callback = "http://localhost:8000/oauth/callback"
808secret = "dev"
809
810[seed.app.myhordes_dev]
811display_name = "MyHordes"
812uri = "http://myhordes.localhost/"
813oauth_callback = "http://myhordes.localhost/twinoid"
814secret = "dev"
815
816[seed.app.neoparc_dev]
817display_name = "NeoParc"
818uri = "http://localhost:8880/"
819oauth_callback = "http://localhost:8880/api/account/callback"
820secret = "dev"
821
822[seed.forum_section.main_en]
823display_name = "Main Forum (en-US)"
824locale = "en-US"
825
826[seed.forum_section.main_fr]
827display_name = "Forum Général (fr-FR)"
828locale = "fr-FR"
829
830[seed.forum_section.main_es]
831display_name = "Foro principal (es-SP)"
832locale = "es-SP"
833
834[seed.forum_section.main_de]
835display_name = "Hauptforum (de-DE)"
836locale = "de-DE"
837
838[seed.forum_section.main_eo]
839display_name = "Ĉefa forumo (eo)"
840locale = "eo"
841
842[seed.forum_section.eternalfest_main]
843display_name = "[Eternalfest] Le Panthéon"
844locale = "fr-FR"
845
846[seed.forum_section.emush_main]
847display_name = "[eMush] Neron is watching you"
848locale = "fr-FR"
849
850[seed.forum_section.drpg_main]
851display_name = "[DinoRPG] Jurassic Park"
852locale = "fr-FR"
853
854[seed.forum_section.myhordes_main]
855display_name = "[Myhordes] Le Saloon"
856locale = "fr-FR"
857
858[seed.forum_section.kadokadeo_main]
859display_name = "[Kadokadeo] Café des palabres"
860locale = "fr-FR"
861
862[seed.forum_section.kingdom_main]
863display_name = "[Kingdom] La foire du trône"
864locale = "fr-FR"
865
866[seed.forum_section.na_main]
867display_name = "[Naturalchimie] Le laboratoire"
868locale = "fr-FR"
869
870[seed.forum_section.sq_main]
871display_name = "[Studioquiz] Le bar à questions"
872locale = "fr-FR"
873
874[seed.forum_section.ts_main]
875display_name = "[Teacher Story] La salle des profs"
876locale = "fr-FR"
877
878[seed.forum_section.popotamo_main]
879display_name = "[Popotamo] Le mot le plus long"
880locale = "fr-FR"
881"#;
882 let actual = ResolvedConfig::parse(INPUT, ConfigFormat::Toml, false, ConfigSource::Default).unwrap();
883 let actual = ConfigChain::one(actual, BuiltinEternaltwinConfigProfile::Production).merge();
884 let expected = Config::default_for_profile(BuiltinEternaltwinConfigProfile::Dev);
885 assert_eq!(actual, expected);
886 }
887}