1use indexmap::IndexMap;
2use once_cell::sync::Lazy;
3use serde::Deserialize;
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::string::ToString;
8use url::Url;
9
10#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
11pub struct Config {
12 pub etwin: EtwinConfig,
13 pub db: DbConfig,
14 pub clients: IndexMap<String, ClientConfig>,
15 pub auth: AuthConfig,
16 pub forum: ForumConfig,
17 pub mailer: Option<MailerConfig>,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
21pub struct EtwinConfig {
22 pub http_port: u16,
23 pub backend_port: u16,
24 pub api: String,
25 pub secret: String,
26 pub external_uri: Url,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
30pub struct DbConfig {
31 pub host: String,
32 pub port: u16,
33 pub name: String,
34 pub admin_user: String,
35 pub admin_password: String,
36 pub user: String,
37 pub password: String,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
41pub struct ClientConfig {
42 pub display_name: String,
43 pub app_uri: Url,
44 pub callback_uri: Url,
45 pub secret: String,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
49pub struct AuthConfig {
50 pub twinoid: TwinoidAuthConfig,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
54pub struct ForumConfig {
55 pub posts_per_page: u16,
56 pub threads_per_page: u16,
57 pub sections: IndexMap<String, ForumSectionConfig>,
58}
59
60impl Default for ForumConfig {
61 fn default() -> Self {
62 Self {
63 posts_per_page: 10,
64 threads_per_page: 20,
65 sections: IndexMap::new(),
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
71pub struct ForumSectionConfig {
72 pub display_name: String,
73 pub locale: Option<String>,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
77pub struct TwinoidAuthConfig {
78 pub client_id: String,
79 pub secret: String,
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
83pub struct MailerConfig {
84 pub host: String,
85 pub username: String,
86 pub password: String,
87 pub sender: String,
88 pub headers: Option<Vec<MailerHeader>>,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
92pub struct MailerHeader {
93 pub name: String,
94 pub value: String,
95}
96
97#[derive(Debug)]
98pub enum FindConfigFileError {
99 NotFound(PathBuf),
100 Other(PathBuf, io::Error),
101}
102
103#[derive(Debug)]
104pub enum FindConfigError {
105 NotFound(PathBuf),
106 ParseError(toml::de::Error),
107 Other(PathBuf, io::Error),
108}
109
110fn find_config_file(dir: PathBuf) -> Result<(PathBuf, String), FindConfigFileError> {
111 for d in dir.ancestors() {
112 let config_path = d.join("etwin.toml");
113 match fs::read_to_string(&config_path) {
114 Ok(toml) => return Ok((config_path, toml)),
115 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
116 Err(e) => return Err(FindConfigFileError::Other(dir, e)),
117 }
118 }
119 Err(FindConfigFileError::NotFound(dir))
120}
121
122pub fn parse_config(_file: &Path, config_toml: &str) -> Result<Config, toml::de::Error> {
123 let raw: Config = toml::from_str(config_toml)?;
124 Ok(raw)
125}
126
127pub fn find_config(dir: PathBuf) -> Result<Config, FindConfigError> {
128 match find_config_file(dir) {
129 Ok((file, config_toml)) => match parse_config(&file, &config_toml) {
130 Ok(config) => Ok(config),
131 Err(e) => Err(FindConfigError::ParseError(e)),
132 },
133 Err(FindConfigFileError::NotFound(dir)) => Err(FindConfigError::NotFound(dir)),
134 Err(FindConfigFileError::Other(dir, cause)) => Err(FindConfigError::Other(dir, cause)),
135 }
136}
137
138pub static DEFAULT: Lazy<Config> = Lazy::new(|| Config {
139 etwin: EtwinConfig {
140 http_port: 50320,
141 backend_port: 50321,
142 api: "memory".to_string(),
143 secret: "dev_secret".to_string(),
144 external_uri: Url::parse("http://localhost:50320/").unwrap(),
145 },
146 db: DbConfig {
147 host: "localhost".to_string(),
148 port: 5432,
149 name: "etwin.dev".to_string(),
150 admin_user: "etwin.dev.admin".to_string(),
151 admin_password: "dev".to_string(),
152 user: "etwin.dev.admin".to_string(),
153 password: "dev".to_string(),
154 },
155 clients: IndexMap::new(),
156 auth: AuthConfig {
157 twinoid: TwinoidAuthConfig {
158 client_id: "380".to_string(),
159 secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
160 },
161 },
162 forum: ForumConfig::default(),
163 mailer: None,
164});
165
166#[cfg(test)]
167mod test {
168 use crate::{parse_config, DEFAULT};
169
170 #[test]
171 fn test_default_config() {
172 const INPUT: &str = r#"
174[etwin]
175http_port = 50320
176backend_port = 50321
177api = "memory"
178secret = "dev_secret"
179external_uri = "http://localhost:50320"
180
181[db]
182host = "localhost"
183port = 5432
184name = "etwin.dev"
185admin_user = "etwin.dev.admin"
186admin_password = "dev"
187user = "etwin.dev.admin"
188password = "dev"
189
190[auth.twinoid]
191client_id = "380"
192secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
193 "#;
194 let path = std::env::current_dir().unwrap().join("etwin.toml");
195 let actual = parse_config(&path, INPUT);
196 let expected = Ok(DEFAULT.clone());
197 assert_eq!(actual, expected);
198 }
199}