use std::{
env, fmt,
fs::{create_dir_all, File},
io::{self, Read},
path::{Path, PathBuf},
};
use toml_edit::{Array, Document, Item, Value};
use crate::common::*;
#[cfg(not(target_os = "macos"))]
fn system_config_dir() -> Option<PathBuf> {
dirs::config_dir()
}
#[cfg(target_os = "macos")]
fn system_config_dir() -> Option<PathBuf> {
if let Some(preference_dir) = dirs::preference_dir() {
let old_config_dir = preference_dir.join("dbcrossbar");
if old_config_dir.is_dir() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
if let Some(system_config_dir) = dirs::config_dir() {
let new_config_dir = system_config_dir.join("dbcrossbar");
eprintln!(
"DEPRECATION WARNING: Please move `{}` to `{}`",
old_config_dir.display(),
new_config_dir.display(),
);
}
});
return Some(preference_dir);
}
}
dirs::config_dir()
}
pub(crate) fn config_dir() -> Result<PathBuf> {
match env::var_os("DBCROSSBAR_CONFIG_DIR") {
Some(dir) => Ok(PathBuf::from(dir)),
None => Ok(system_config_dir()
.ok_or_else(|| format_err!("could not find user config dir"))?
.join("dbcrossbar")),
}
}
pub(crate) fn config_file() -> Result<PathBuf> {
Ok(config_dir()?.join("dbcrossbar.toml"))
}
#[derive(Debug)]
pub struct Key<'a> {
key: &'a str,
}
impl Key<'_> {
pub fn temporary() -> Key<'static> {
Self::global("temporary")
}
pub(crate) fn global(key: &str) -> Key<'_> {
Key { key }
}
}
impl<'a> fmt::Display for Key<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.key.fmt(f)
}
}
#[derive(Debug)]
pub struct Configuration {
path: PathBuf,
doc: Document,
}
impl Configuration {
pub fn try_default() -> Result<Self> {
Self::from_path(&config_file()?)
}
pub(crate) fn from_path(path: &Path) -> Result<Self> {
match File::open(path) {
Ok(rdr) => Ok(Self::from_reader(path.to_owned(), rdr)
.with_context(|| format!("could not read file {}", path.display()))?),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Self {
path: path.to_owned(),
doc: Document::default(),
}),
Err(err) => {
Err(err).context(format!("could not open file {}", path.display()))
}
}
}
fn from_reader<R>(path: PathBuf, mut rdr: R) -> Result<Self>
where
R: Read + 'static,
{
let mut buf = String::new();
rdr.read_to_string(&mut buf)?;
let doc = buf.parse::<Document>()?;
Ok(Self { path, doc })
}
pub fn write(&self) -> Result<()> {
let parent = self.path.parent().ok_or_else(|| {
format_err!("cannot find parent directory of {}", self.path.display())
})?;
create_dir_all(parent)
.with_context(|| format!("cannot create {}", parent.display()))?;
let data = self.doc.to_string();
let mut f = File::create(&self.path)
.with_context(|| format!("cannot create {}", self.path.display()))?;
f.write_all(data.as_bytes())
.with_context(|| format!("error writing to {}", self.path.display()))?;
f.flush()
.with_context(|| format!("error writing to {}", self.path.display()))?;
Ok(())
}
pub fn temporaries(&self) -> Result<Vec<String>> {
self.string_array(&Key::global("temporary"))
}
fn string_array(&self, key: &Key<'_>) -> Result<Vec<String>> {
let mut temps = vec![];
if let Some(raw_value) = self.doc.as_table().get(key.key) {
if let Some(raw_array) = raw_value.as_array() {
for raw_item in raw_array.iter() {
if let Some(temp) = raw_item.as_str() {
temps.push(temp.to_owned());
} else {
return Err(format_err!(
"expected string, found {:?} in {}",
raw_item,
self.path.display(),
));
}
}
} else {
return Err(format_err!(
"expected array, found {:?} in {}",
raw_value,
self.path.display(),
));
}
}
Ok(temps)
}
fn raw_string_array_mut<'a, 'b>(
&mut self,
key: &'a Key<'b>,
) -> Result<&mut Array> {
let array_value = self
.doc
.as_table_mut()
.entry(key.key)
.or_insert(Item::Value(Value::Array(Array::default())));
match array_value.as_array_mut() {
Some(array) => Ok(array),
None => Err(format_err!(
"expected array for {} in {}",
key,
self.path.display(),
)),
}
}
pub fn add_to_string_array(&mut self, key: &Key<'_>, value: &str) -> Result<()> {
let raw_array = self.raw_string_array_mut(key)?;
for raw_item in raw_array.iter() {
if raw_item.as_str() == Some(value) {
return Ok(());
}
}
raw_array.push(value);
raw_array.fmt();
Ok(())
}
pub fn remove_from_string_array(
&mut self,
key: &Key<'_>,
value: &str,
) -> Result<()> {
let raw_array = self.raw_string_array_mut(key)?;
let mut indices = vec![];
for (idx, raw_item) in raw_array.iter().enumerate() {
if raw_item.as_str() == Some(value) {
indices.push(idx);
}
}
for idx in indices.iter().rev().cloned() {
raw_array.remove(idx);
}
raw_array.fmt();
Ok(())
}
}
#[test]
fn temporaries_can_be_added_and_removed() {
let temp = tempfile::Builder::new()
.prefix("dbcrossbar")
.suffix(".toml")
.tempfile()
.unwrap();
let path = temp.path();
let mut config = Configuration::from_path(path).unwrap();
let key = Key::global("temporary");
assert_eq!(config.temporaries().unwrap(), Vec::<String>::new());
config.add_to_string_array(&key, "s3://example/").unwrap();
assert_eq!(config.temporaries().unwrap(), &["s3://example/".to_owned()]);
config.write().unwrap();
config = Configuration::from_path(path).unwrap();
assert_eq!(config.temporaries().unwrap(), &["s3://example/".to_owned()]);
config
.remove_from_string_array(&key, "s3://example/")
.unwrap();
assert_eq!(config.temporaries().unwrap(), Vec::<String>::new());
}