1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! This library makes it simple to load configuration files from disk.
//!
//! Configuration can be loaded into any struct that implements
//! `serde::Deserialize` and `std::default::Default`.
//!
//! The library will load the first struct it finds in the following list,
//! falling back to the default if no files are found.
//!
//! 1. `./{name}`
//! 1. `./{name}.toml`
//! 1. `./.{name}`
//! 1. `./.{name}.toml`
//! 1. `~/.{name}`
//! 1. `~/.{name}.toml`
//! 1. `~/.config/{name}`
//! 1. `~/.config/{name}.toml`
//! 1. `~/.config/{name}/config`
//! 1. `~/.config/{name}/config.toml`
//! 1. `/etc/.config/{name}`
//! 1. `/etc/.config/{name}.toml`
//! 1. `/etc/.config/{name}/config`
//! 1. `/etc/.config/{name}/config.toml`
//!
//! # Example Usage
//!
//! ```rust
//! #[macro_use]
//! extern crate serde_derive;
//! extern crate loadconf;
//!
//! /// Sample configuration
//! #[derive(Deserialize)]
//! struct Config {
//!     /// Sample variable
//!     var: String,
//! }
//!
//! impl Default for Config {
//!     fn default() -> Config {
//!         Config { var: "Test configuration.".to_string() }
//!     }
//! }
//!
//! fn main() {
//!     use loadconf::Load;
//!
//!     // Just search for configuration files
//!     let config = Config::load("sample");
//!
//!     // Optionally use file specified on command line.
//!     use std::env;
//!     let mut args = env::args();
//!     args.next();
//!     let config = Config::fallback_load("sample", args.next());
//! }
//! ```

extern crate serde;
#[allow(unused_imports)]
#[macro_use]
extern crate serde_derive;
#[allow(unused_imports)]
extern crate tempdir;
extern crate toml;

mod error;

pub use error::Error;
use serde::de::DeserializeOwned;
use std::default::Default;
use std::env::home_dir;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};

/// Load a struct from a configuration file.
pub trait Load: Sized {
    /// Find a configuration file and load the contents, falling back to
    /// the default.
    ///
    /// # Panics
    ///
    /// This will panic if there are any issues reading or deserializing the file.
    /// To catch these errors, use `try_load` instead.
    fn load<S: AsRef<str>>(filename: S) -> Self {
        Load::try_load(filename).expect("Error reading configuration from file")
    }

    /// Find a configuration file and load the contents, falling back to
    /// the default. Errors if file can't be read or deserialized.
    fn try_load<S: AsRef<str>>(filename: S) -> Result<Self, Error> {
        Load::try_fallback_load::<S, &str>(filename, None)
    }

    /// Loads the configuration from the given path or falls back to search if
    /// the path is None.
    ///
    /// # Panics
    ///
    /// This will panic if there are any issues reading or deserializing the file.
    /// To catch these errors, use `try_load` instead.
    fn fallback_load<S: AsRef<str>, P: AsRef<Path>>(filename: S, path: Option<P>) -> Self {
        Load::try_fallback_load(filename, path).expect("Error reading configuration from file")
    }

    /// Loads the configuration from the given path or falls back to search if
    /// the path is None. Errors if file can't be read or deserialized.
    fn try_fallback_load<S: AsRef<str>, P: AsRef<Path>>(
        filename: S,
        path: Option<P>,
    ) -> Result<Self, Error>;
}

impl<C> Load for C
where
    C: Default + DeserializeOwned,
{
    fn try_fallback_load<S: AsRef<str>, P: AsRef<Path>>(
        filename: S,
        path: Option<P>,
    ) -> Result<C, Error> {
        if let Some(path) = path {
            read_from_file(path.as_ref())
        } else {
            let paths = path_list(filename.as_ref());

            match paths.iter().find(|p| p.exists()) {
                Some(path) => read_from_file(path),
                None => Ok(Default::default()),
            }
        }
    }
}

/// Read a configuration from a file.
fn read_from_file<P, C>(path: P) -> Result<C, Error>
where
    P: AsRef<Path>,
    C: Default + DeserializeOwned,
{
    let mut text = String::new();
    File::open(path)?.read_to_string(&mut text)?;
    Ok(toml::from_str(&text)?)
}

/// Generate a vector of all the paths to search for a configuration file.
fn path_list(name: &str) -> Vec<PathBuf> {
    let mut paths = Vec::new();

    // Add relative paths
    let mut relative_paths = vec![
        format!("{}", name),
        format!("{}.toml", name),
        format!(".{}", name),
        format!(".{}.toml", name),
    ];
    paths.append(&mut relative_paths);

    // Get the home directory as a string.
    let home = home_dir()
        .map(|h| h.into_os_string())
        .and_then(|p| p.into_string().ok());

    // Add home paths
    let mut home_paths = match home {
        Some(home) => {
            vec![
                format!("{}/.{}", home, name),
                format!("{}/.{}.toml", home, name),
                format!("{}/.config/{}", home, name),
                format!("{}/.config/{}.toml", home, name),
                format!("{}/.config/{}/config", home, name),
                format!("{}/.config/{}/config.toml", home, name),
            ]
        }
        None => vec![],
    };
    paths.append(&mut home_paths);

    // Add absolute paths
    let mut absolute_paths = vec![
        format!("/etc/.config/{}", name),
        format!("/etc/.config/{}.toml", name),
        format!("/etc/.config/{}/config", name),
        format!("/etc/.config/{}/config.toml", name),
    ];
    paths.append(&mut absolute_paths);

    paths
        .into_iter()
        .map(|p| AsRef::<Path>::as_ref(&p).to_path_buf())
        .collect()
}

#[cfg(test)]
mod test {

    /// Sample configuration
    #[derive(Debug, PartialEq, Eq, Deserialize)]
    struct Config {
        /// Sample variable
        var: String,
    }

    impl Default for Config {
        fn default() -> Config {
            Config { var: "Test configuration.".to_string() }
        }
    }

    /// Test that path list produces the expected path list.
    #[test]
    fn generate_path_list() {
        use std::env::set_var;
        use std::path::{Path, PathBuf};

        set_var("HOME", "/home/test");
        let paths = super::path_list("testcfg");

        let expected: Vec<PathBuf> = vec![
            "testcfg",
            "testcfg.toml",
            ".testcfg",
            ".testcfg.toml",
            "/home/test/.testcfg",
            "/home/test/.testcfg.toml",
            "/home/test/.config/testcfg",
            "/home/test/.config/testcfg.toml",
            "/home/test/.config/testcfg/config",
            "/home/test/.config/testcfg/config.toml",
            "/etc/.config/testcfg",
            "/etc/.config/testcfg.toml",
            "/etc/.config/testcfg/config",
            "/etc/.config/testcfg/config.toml",
        ].into_iter()
            .map(|p| AsRef::<Path>::as_ref(&p).to_path_buf())
            .collect();

        assert_eq!(paths, expected);
    }

    /// Test loading default configuration.
    #[test]
    fn load_default() {
        use super::Load;

        let config = Config::load("testcfg");
        assert_eq!(config, Config::default());
    }

    /// Test load configuration from a file.
    #[test]
    fn file_test() {
        use std::env::set_current_dir;
        use std::fs::OpenOptions;
        use std::io::Write;
        use super::Load;
        use tempdir::TempDir;

        // Change into temporary testi directory
        let temp_dir = TempDir::new("loadcfg-test")
            .expect("Could not create temporary directory for test");
        set_current_dir(temp_dir.path())
            .expect("Could not change into temporary directory for test");

        // Write the test configuration file.
        OpenOptions::new()
            .write(true)
            .create(true)
            .open(".testcfg.toml")
            .expect("Couldn't open test configuration file.")
            .write_all("var = \"Test configuration file\"\n".as_bytes())
            .expect("Couldn't write test configuration file.");

        // Load the file
        let config = Config::load("testcfg");
        let expected = Config { var: "Test configuration file".to_string() };
        assert_eq!(config, expected);
    }

    /// Test load configuration from a file specified directly.
    #[test]
    fn specified_file_test() {
        use std::env::set_current_dir;
        use std::fs::OpenOptions;
        use std::io::Write;
        use super::Load;
        use tempdir::TempDir;

        // Change into temporary testi directory
        let temp_dir = TempDir::new("loadcfg-test")
            .expect("Could not create temporary directory for test");
        set_current_dir(temp_dir.path())
            .expect("Could not change into temporary directory for test");

        // Write the test configuration file.
        OpenOptions::new()
            .write(true)
            .create(true)
            .open("given-config.toml")
            .expect("Couldn't open test configuration file.")
            .write_all("var = \"Test configuration file\"\n".as_bytes())
            .expect("Couldn't write test configuration file.");

        // Load the file
        let config = Config::fallback_load("testcfg", Some("given-config.toml"));
        let expected = Config { var: "Test configuration file".to_string() };
        assert_eq!(config, expected);
    }
}