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};
pub trait Load: Sized {
fn load<S: AsRef<str>>(filename: S) -> Self {
Load::try_load(filename).expect("Error reading configuration from file")
}
fn try_load<S: AsRef<str>>(filename: S) -> Result<Self, Error> {
Load::try_fallback_load::<S, &str>(filename, None)
}
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")
}
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()),
}
}
}
}
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)?)
}
fn path_list(name: &str) -> Vec<PathBuf> {
let mut paths = Vec::new();
let mut relative_paths = vec![
format!("{}", name),
format!("{}.toml", name),
format!(".{}", name),
format!(".{}.toml", name),
];
paths.append(&mut relative_paths);
let home = home_dir()
.map(|h| h.into_os_string())
.and_then(|p| p.into_string().ok());
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);
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 {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct Config {
var: String,
}
impl Default for Config {
fn default() -> Config {
Config { var: "Test configuration.".to_string() }
}
}
#[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]
fn load_default() {
use super::Load;
let config = Config::load("testcfg");
assert_eq!(config, Config::default());
}
#[test]
fn file_test() {
use std::env::set_current_dir;
use std::fs::OpenOptions;
use std::io::Write;
use super::Load;
use tempdir::TempDir;
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");
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.");
let config = Config::load("testcfg");
let expected = Config { var: "Test configuration file".to_string() };
assert_eq!(config, expected);
}
#[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;
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");
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.");
let config = Config::fallback_load("testcfg", Some("given-config.toml"));
let expected = Config { var: "Test configuration file".to_string() };
assert_eq!(config, expected);
}
}