etwin_config/
lib.rs

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    // language=toml
173    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}