irc_bot/core/
config.rs

1use super::aatxe;
2use super::pkg_info;
3use super::ErrorKind;
4use super::Result;
5use serde_yaml;
6use smallvec::SmallVec;
7use std::fs::File;
8use std::io::prelude::*;
9use std::io::BufReader;
10use std::path::Path;
11use std::sync::Arc;
12
13mod inner {
14    /// Configuration structure that can be deserialized by Serde.
15    ///
16    /// This is hidden from the consumer because Serde won't validate the configuration.
17    #[derive(Debug, Default, Deserialize)]
18    pub(super) struct Config {
19        pub(super) nickname: String,
20
21        #[serde(default)]
22        pub(super) username: String,
23
24        #[serde(default)]
25        pub(super) realname: String,
26
27        #[serde(default)]
28        pub(super) admins: Vec<super::Admin>,
29
30        pub(super) servers: Vec<super::Server>,
31    }
32}
33
34#[derive(Debug)]
35pub struct Config {
36    pub(crate) nickname: String,
37
38    pub(crate) username: String,
39
40    pub(crate) realname: String,
41
42    pub(crate) admins: SmallVec<[Admin; 8]>,
43
44    pub(crate) servers: SmallVec<[Arc<aatxe::Config>; 8]>,
45}
46
47#[derive(Clone, Debug, Deserialize)]
48pub struct Admin {
49    #[serde(default)]
50    pub nick: Option<String>,
51
52    #[serde(default)]
53    pub user: Option<String>,
54
55    #[serde(default)]
56    pub host: Option<String>,
57}
58
59#[derive(Clone, Debug, Deserialize)]
60struct Server {
61    pub host: String,
62
63    pub port: u16,
64
65    #[serde(default = "mk_true")]
66    pub tls: bool,
67
68    #[serde(default)]
69    pub channels: Vec<String>,
70}
71
72#[derive(Debug)]
73pub struct ConfigBuilder(Result<inner::Config>);
74
75impl Config {
76    pub fn try_from<T>(input: T) -> Result<Config>
77    where
78        T: IntoConfig,
79    {
80        input.into_config()
81    }
82
83    pub fn try_from_path<P>(path: P) -> Result<Config>
84    where
85        P: AsRef<Path>,
86    {
87        Self::try_from(File::open(path)?)
88    }
89
90    pub fn build() -> ConfigBuilder {
91        ConfigBuilder(Ok(Default::default()))
92    }
93}
94
95impl ConfigBuilder {
96    pub fn nickname<S>(self, nickname: S) -> Self
97    where
98        S: Into<String>,
99    {
100        let nickname = nickname.into();
101
102        if nickname.is_empty() {
103            return ConfigBuilder(
104                Err(ErrorKind::Config("nickname".into(), "is empty".into()).into()),
105            );
106        }
107
108        ConfigBuilder(self.0.map(|cfg| inner::Config { nickname, ..cfg }))
109    }
110
111    pub fn username<S>(self, username: S) -> Self
112    where
113        S: Into<String>,
114    {
115        ConfigBuilder(self.0.map(|cfg| inner::Config {
116            username: username.into(),
117            ..cfg
118        }))
119    }
120
121    pub fn realname<S>(self, realname: S) -> Self
122    where
123        S: Into<String>,
124    {
125        ConfigBuilder(self.0.map(|cfg| inner::Config {
126            realname: realname.into(),
127            ..cfg
128        }))
129    }
130}
131
132// TODO: Switch to `TryFrom` once rustc 1.18 is stable.
133pub trait IntoConfig {
134    fn into_config(self) -> Result<Config>;
135}
136
137impl IntoConfig for Config {
138    fn into_config(self) -> Result<Config> {
139        Ok(self)
140    }
141}
142
143impl IntoConfig for Result<Config> {
144    fn into_config(self) -> Result<Config> {
145        self
146    }
147}
148
149impl IntoConfig for ConfigBuilder {
150    fn into_config(self) -> Result<Config> {
151        self.0.and_then(cook_config)
152    }
153}
154
155impl<'a> IntoConfig for &'a str {
156    fn into_config(self) -> Result<Config> {
157        read_config(self)
158    }
159}
160
161impl IntoConfig for String {
162    fn into_config(self) -> Result<Config> {
163        read_config(&self)
164    }
165}
166
167impl<R> IntoConfig for BufReader<R>
168where
169    R: Read,
170{
171    fn into_config(mut self) -> Result<Config> {
172        let mut text = String::new();
173        self.read_to_string(&mut text)?;
174        text.into_config()
175    }
176}
177
178impl IntoConfig for File {
179    fn into_config(self) -> Result<Config> {
180        BufReader::new(self).into_config()
181    }
182}
183
184fn read_config(input: &str) -> Result<Config> {
185    serde_yaml::from_str(input)
186        .map_err(Into::into)
187        .and_then(cook_config)
188}
189
190fn cook_config(mut cfg: inner::Config) -> Result<Config> {
191    validate_config(&cfg)?;
192
193    fill_in_config_defaults(&mut cfg)?;
194
195    let nickname = cfg.nickname.to_owned();
196
197    let username = cfg.username.to_owned();
198
199    let realname = cfg.realname.to_owned();
200
201    let admins = cfg.admins.drain(..).collect();
202
203    let servers = cfg.servers
204        .drain(..)
205        .map(|server_cfg| {
206            Arc::new(aatxe::Config {
207                // TODO: Allow nickname etc. to be configured per-server.
208                nickname: Some(nickname.clone()),
209                username: Some(username.clone()),
210                realname: Some(realname.clone()),
211                server: Some(server_cfg.host),
212                port: Some(server_cfg.port),
213                use_ssl: Some(server_cfg.tls),
214                channels: Some(server_cfg.channels),
215                ..Default::default()
216            })
217        })
218        .collect();
219
220    Ok(Config {
221        nickname,
222        username,
223        realname,
224        admins,
225        servers,
226    })
227}
228
229fn validate_config(cfg: &inner::Config) -> Result<()> {
230    ensure!(
231        !cfg.nickname.is_empty(),
232        ErrorKind::Config("nickname".into(), "is empty".into())
233    );
234
235    ensure!(
236        !cfg.servers.is_empty(),
237        ErrorKind::Config("servers".into(), "is empty".into())
238    );
239
240    ensure!(
241        cfg.servers.len() == 1,
242        ErrorKind::Config(
243            "servers".into(),
244            "lists multiple servers, which is not yet supported".into(),
245        )
246    );
247
248    Ok(())
249}
250
251fn fill_in_config_defaults(cfg: &mut inner::Config) -> Result<()> {
252    if cfg.username.is_empty() {
253        cfg.username = cfg.nickname.clone();
254    }
255
256    if cfg.realname.is_empty() {
257        cfg.realname = pkg_info::BRIEF_CREDITS_STRING.clone();
258    }
259
260    Ok(())
261}
262
263fn mk_true() -> bool {
264    true
265}