blob: d887dc58ced478ab9e609b3e3640f5a1c9a9bfbe [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::api::query::SelectMode;
use crate::api::value::merge_map;
use crate::environment::Environment;
use crate::nested::{nested_get, nested_remove, nested_set};
use crate::ConfigLevel;
use anyhow::{bail, Context, Result};
use config_macros::include_default;
use fuchsia_lockfile::Lockfile;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
use std::fmt;
use std::fs::OpenOptions;
use std::io::{BufReader, BufWriter, ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use tracing::error;
/// The type of a configuration level's mapping.
pub type ConfigMap = Map<String, Value>;
/// An individually loaded configuration file, including the path it came from
/// if it was loaded from disk.
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigFile {
path: Option<PathBuf>,
contents: ConfigMap,
dirty: bool,
flush: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
default: ConfigMap,
global: Option<ConfigFile>,
user: Option<ConfigFile>,
build: Option<ConfigFile>,
runtime: ConfigMap,
}
pub(crate) struct PriorityIterator<'a> {
curr: Option<ConfigLevel>,
config: &'a Config,
}
impl<'a> Iterator for PriorityIterator<'a> {
type Item = Option<&'a ConfigMap>;
fn next(&mut self) -> Option<Self::Item> {
use ConfigLevel::*;
self.curr = ConfigLevel::next(self.curr);
match self.curr {
Some(Runtime) => Some(Some(&self.config.runtime)),
Some(Build) => Some(self.config.build.as_ref().map(|file| &file.contents)),
Some(User) => Some(self.config.user.as_ref().map(|file| &file.contents)),
Some(Global) => Some(self.config.global.as_ref().map(|file| &file.contents)),
Some(Default) => Some(Some(&self.config.default)),
None => None,
}
}
}
/// Reads a JSON formatted reader permissively, returning None if for whatever reason
/// the file couldn't be read.
///
/// If the JSON is malformed, it will just get overwritten if set is ever used.
/// (TODO: Validate above assumptions)
fn read_json<T: DeserializeOwned>(file: impl Read) -> Option<T> {
serde_json::from_reader(file).ok()
}
fn write_json<W: Write>(file: Option<W>, value: Option<&Value>) -> Result<()> {
match (value, file) {
(Some(v), Some(mut f)) => {
serde_json::to_writer_pretty(&mut f, v).context("writing config file")?;
f.flush().map_err(Into::into)
}
(_, _) => {
// If either value or file are None, then return Ok(()). File being none will
// presume the user doesn't want to save at this level.
Ok(())
}
}
}
struct MaybeFlushWriter<T> {
flush: bool,
writer: T,
}
impl<T: Write> Write for MaybeFlushWriter<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
if self.flush {
tracing::debug!("Flushing writer");
let ret = self.writer.flush();
tracing::debug!("Flushed writer");
ret
} else {
tracing::debug!("Skipped flushing writer (isolate detected)");
Ok(())
}
}
}
/// Atomically write to the file by creating a temporary file and passing it
/// to the closure, and atomically rename it to the destination file.
async fn with_writer<F>(path: Option<&Path>, f: F, flush: bool) -> Result<()>
where
F: FnOnce(Option<BufWriter<&mut MaybeFlushWriter<tempfile::NamedTempFile>>>) -> Result<()>,
{
if let Some(path) = path {
let path = Path::new(path);
let _lockfile = Lockfile::lock_for(path, std::time::Duration::from_secs(2)).await.map_err(|e| {
error!("Failed to create a lockfile for {path}. Check that {lockpath} doesn't exist and can be written to. Ownership information: {owner:#?}", path=path.display(), lockpath=e.lock_path.display(), owner=e.owner);
e
})?;
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let tmp = tempfile::NamedTempFile::new_in(parent)?;
let mut writer = MaybeFlushWriter { flush, writer: tmp };
tracing::debug!("Calling writer callback");
f(Some(BufWriter::new(&mut writer)))?;
tracing::debug!("Calling persist");
writer.writer.persist(path)?;
tracing::debug!("Persisted");
Ok(())
} else {
tracing::debug!("Calling writer callback with no persist");
let ret = f(None);
tracing::debug!("Called writer callback");
ret
}
}
impl ConfigFile {
#[cfg(test)]
fn from_map(path: Option<PathBuf>, contents: ConfigMap) -> Self {
Self { path, contents, dirty: false, flush: true }
}
fn from_buf(path: Option<PathBuf>, buffer: impl Read, flush: bool) -> Self {
let contents = read_json(buffer)
.as_ref()
.and_then(Value::as_object)
.cloned()
.unwrap_or_else(Map::default);
Self { path, contents, dirty: false, flush }
}
fn from_file(path: &Path) -> Result<Self> {
let file = OpenOptions::new().read(true).open(path);
match file {
Ok(buf) => Ok(Self::from_buf(Some(path.to_owned()), BufReader::new(buf), true)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Self {
path: Some(path.to_owned()),
contents: ConfigMap::default(),
dirty: false,
flush: true,
}),
Err(e) => Err(e.into()),
}
}
fn from_nonflushing_file(path: &Path) -> Result<Self> {
let file = OpenOptions::new().read(true).open(path);
match file {
Ok(buf) => Ok(Self::from_buf(Some(path.to_owned()), BufReader::new(buf), false)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Self {
path: Some(path.to_owned()),
contents: ConfigMap::default(),
dirty: false,
flush: false,
}),
Err(e) => Err(e.into()),
}
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn set(&mut self, key: &str, value: Value) -> Result<bool> {
let key_vec: Vec<&str> = key.split('.').collect();
let key = *key_vec.get(0).context("Can't set empty key")?;
let changed = nested_set(&mut self.contents, key, &key_vec[1..], value);
self.dirty = self.dirty || changed;
Ok(changed)
}
pub fn remove(&mut self, key: &str) -> Result<()> {
let key_vec: Vec<&str> = key.split('.').collect();
let key = *key_vec.get(0).context("Can't remove empty key")?;
self.dirty = true;
nested_remove(&mut self.contents, key, &key_vec[1..])
}
async fn save(&mut self) -> Result<()> {
tracing::debug!("Saving path {:?}", self.path);
// FIXME(81502): There is a race between the ffx CLI and the daemon service
// in updating the config. We can lose changes if both try to change the
// config at the same time. We can reduce the rate of races by only writing
// to the config if the value actually changed.
let ret = if self.is_dirty() {
self.dirty = false;
with_writer(
self.path.as_deref(),
|writer| write_json(writer, Some(&Value::Object(self.contents.clone()))),
self.flush,
)
.await
} else {
Ok(())
};
tracing::debug!("Saved path {:?}", self.path);
ret
}
}
#[cfg(test)]
impl Default for ConfigFile {
fn default() -> Self {
Self::from_map(None, Map::default())
}
}
impl Config {
fn new(
global: Option<ConfigFile>,
build: Option<ConfigFile>,
user: Option<ConfigFile>,
runtime: ConfigMap,
default_override: ConfigMap,
) -> Self {
let mut default = match include_default!() {
Value::Object(obj) => obj,
_ => panic!("Statically build default configuration was not an object"),
};
merge_map(&mut default, &default_override);
Self { user, build, global, runtime, default }
}
pub(crate) fn from_env(env: &Environment) -> Result<Self> {
let user_conf = env.get_user();
let build_conf = env.get_build();
let is_isolated = env.context().env_kind().is_isolated();
if !is_isolated {
tracing::debug!("Non isolated context {:?}", env.context().env_kind());
}
let from_file =
if is_isolated { ConfigFile::from_nonflushing_file } else { ConfigFile::from_file };
let user = user_conf.as_deref().map(from_file).transpose()?;
let build = build_conf.as_deref().map(from_file).transpose()?;
let global = env.get_global().map(from_file).transpose()?;
Ok(Self::new(
global,
build,
user,
env.get_runtime_args().clone(),
env.context().get_default_overrides(),
))
}
#[cfg(test)]
fn write<W: Write>(&self, global: Option<W>, build: Option<W>, user: Option<W>) -> Result<()> {
write_json(
user,
self.user.as_ref().map(|file| Value::Object(file.contents.clone())).as_ref(),
)?;
write_json(
build,
self.build.as_ref().map(|file| Value::Object(file.contents.clone())).as_ref(),
)?;
write_json(
global,
self.global.as_ref().map(|file| Value::Object(file.contents.clone())).as_ref(),
)?;
Ok(())
}
pub(crate) async fn save(&mut self) -> Result<()> {
let files = [&mut self.global, &mut self.build, &mut self.user];
// Try to save all files and only fail out if any of them fail afterwards (with the first error). This hopefully mitigates
// any weird partial-save issues, though there's no way to eliminate them altogether (short of filesystem
// transactions)
FuturesUnordered::from_iter(
files.into_iter().filter_map(|file| file.as_mut()).map(ConfigFile::save),
)
.fold(Ok(()), |res, i| async { res.and_then(|_| i) })
.await
}
pub fn get_level(&self, level: ConfigLevel) -> Option<&ConfigMap> {
match level {
ConfigLevel::Runtime => Some(&self.runtime),
ConfigLevel::User => self.user.as_ref().map(|file| &file.contents),
ConfigLevel::Build => self.build.as_ref().map(|file| &file.contents),
ConfigLevel::Global => self.global.as_ref().map(|file| &file.contents),
ConfigLevel::Default => Some(&self.default),
}
}
pub fn get_in_level(&self, key: &str, level: ConfigLevel) -> Option<Value> {
let key_vec: Vec<&str> = key.split('.').collect();
nested_get(self.get_level(level), key_vec.get(0)?, &key_vec[1..]).cloned()
}
pub fn get(&self, key: &str, select: SelectMode) -> Option<Value> {
let key_vec: Vec<&str> = key.split('.').collect();
match select {
SelectMode::First => {
self.iter().find_map(|c| nested_get(c, *key_vec.get(0)?, &key_vec[1..])).cloned()
}
SelectMode::All => {
let result: Vec<Value> = self
.iter()
.filter_map(|c| nested_get(c, *key_vec.get(0)?, &key_vec[1..]))
.cloned()
.collect();
if result.len() > 0 {
Some(Value::Array(result))
} else {
None
}
}
}
}
pub fn set(&mut self, key: &str, level: ConfigLevel, value: Value) -> Result<bool> {
let file = self.get_level_mut(level)?;
file.set(key, value)
}
pub fn remove(&mut self, key: &str, level: ConfigLevel) -> Result<()> {
let file = self.get_level_mut(level)?;
file.remove(key)
}
pub(crate) fn iter(&self) -> PriorityIterator<'_> {
PriorityIterator { curr: None, config: self }
}
fn get_level_mut(&mut self, level: ConfigLevel) -> Result<&mut ConfigFile> {
match level {
ConfigLevel::Runtime => bail!("No mutable access to runtime level configuration"),
ConfigLevel::User => self
.user
.as_mut()
.context("Tried to write to unconfigured user level configuration"),
ConfigLevel::Build => self
.build
.as_mut()
.context("Tried to write to unconfigured build level configuration"),
ConfigLevel::Global => self
.global
.as_mut()
.context("Tried to write to unconfigured global level configuration"),
ConfigLevel::Default => bail!("No mutable access to default level configuration"),
}
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"FFX configuration can come from several places and has an inherent priority assigned\n\
to the different ways the configuration is gathered. A configuration key can be set\n\
in multiple locations but the first value found is returned. The following output\n\
shows the locations checked in descending priority order.\n"
)?;
let mut iterator = self.iter();
while let Some(next) = iterator.next() {
if let Some(level) = iterator.curr {
match level {
ConfigLevel::Runtime => {
write!(f, "Runtime Configuration")?;
}
ConfigLevel::User => {
write!(f, "User Configuration")?;
}
ConfigLevel::Build => {
write!(f, "Build Configuration")?;
}
ConfigLevel::Global => {
write!(f, "Global Configuration")?;
}
ConfigLevel::Default => {
write!(f, "Default Configuration")?;
}
};
}
if let Some(value) = next {
writeln!(f, "")?;
writeln!(f, "{}", serde_json::to_string_pretty(&value).unwrap())?;
} else {
writeln!(f, ": {}", "none")?;
}
writeln!(f, "")?;
}
Ok(())
}
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use super::*;
use crate::nested::RecursiveMap;
use regex::Regex;
use serde_json::json;
const ERROR: &'static [u8] = b"0";
const USER: &'static [u8] = br#"
{
"name": "User"
}"#;
const BUILD: &'static [u8] = br#"
{
"name": "Build"
}"#;
const GLOBAL: &'static [u8] = br#"
{
"name": "Global"
}"#;
const DEFAULT: &'static [u8] = br#"
{
"name": "Default"
}"#;
const RUNTIME: &'static [u8] = br#"
{
"name": "Runtime"
}"#;
const MAPPED: &'static [u8] = br#"
{
"name": "TEST_MAP"
}"#;
const NESTED: &'static [u8] = br#"
{
"name": {
"nested": "Nested"
}
}"#;
const DEEP: &'static [u8] = br#"
{
"name": {
"nested": {
"deep": {
"name": "TEST_MAP"
}
}
}
}"#;
const LITERAL: &'static [u8] = b"[]";
#[test]
fn test_persistent_build() -> Result<()> {
let persistent_config = Config::new(
Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
Map::default(),
Map::default(),
);
let value = persistent_config.get("name", SelectMode::First);
assert!(value.is_some());
assert_eq!(value.unwrap(), Value::String(String::from("Build")));
let mut user_file_out = String::new();
let mut build_file_out = String::new();
let mut global_file_out = String::new();
unsafe {
persistent_config.write(
Some(BufWriter::new(global_file_out.as_mut_vec())),
Some(BufWriter::new(build_file_out.as_mut_vec())),
Some(BufWriter::new(user_file_out.as_mut_vec())),
)?;
}
// Remove whitespace
let mut user_file = String::from_utf8_lossy(USER).to_string();
let mut build_file = String::from_utf8_lossy(BUILD).to_string();
let mut global_file = String::from_utf8_lossy(GLOBAL).to_string();
user_file.retain(|c| !c.is_whitespace());
build_file.retain(|c| !c.is_whitespace());
global_file.retain(|c| !c.is_whitespace());
user_file_out.retain(|c| !c.is_whitespace());
build_file_out.retain(|c| !c.is_whitespace());
global_file_out.retain(|c| !c.is_whitespace());
assert_eq!(user_file, user_file_out);
assert_eq!(build_file, build_file_out);
assert_eq!(global_file, global_file_out);
Ok(())
}
#[test]
fn test_priority_iterator() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: serde_json::from_slice(RUNTIME)?,
};
let mut test_iter = test.iter();
assert_eq!(test_iter.next(), Some(Some(&test.runtime)));
assert_eq!(test_iter.next(), Some(test.build.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(test.user.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(test.global.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(Some(&test.default)));
assert_eq!(test_iter.next(), None);
Ok(())
}
#[test]
fn test_priority_iterator_with_nones() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: None,
global: None,
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let mut test_iter = test.iter();
assert_eq!(test_iter.next(), Some(Some(&test.runtime)));
assert_eq!(test_iter.next(), Some(test.build.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(test.user.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(test.global.as_ref().map(|file| &file.contents)));
assert_eq!(test_iter.next(), Some(Some(&test.default)));
assert_eq!(test_iter.next(), None);
Ok(())
}
#[test]
fn test_get() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let value = test.get("name", SelectMode::First);
assert!(value.is_some());
assert_eq!(value.unwrap(), Value::String(String::from("Build")));
let test_build = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: None,
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let value_build = test_build.get("name", SelectMode::First);
assert!(value_build.is_some());
assert_eq!(value_build.unwrap(), Value::String(String::from("User")));
let test_global = Config {
user: None,
build: None,
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let value_global = test_global.get("name", SelectMode::First);
assert!(value_global.is_some());
assert_eq!(value_global.unwrap(), Value::String(String::from("Global")));
let test_default = Config {
user: None,
build: None,
global: None,
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let value_default = test_default.get("name", SelectMode::First);
assert!(value_default.is_some());
assert_eq!(value_default.unwrap(), Value::String(String::from("Default")));
let test_none = Config {
user: None,
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
let value_none = test_none.get("name", SelectMode::First);
assert!(value_none.is_none());
Ok(())
}
#[test]
fn test_set_non_map_value() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(ERROR), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.set("name", ConfigLevel::User, Value::String(String::from("whatever")))?;
let value = test.get("name", SelectMode::First);
assert_eq!(value, Some(Value::String(String::from("whatever"))));
Ok(())
}
#[test]
fn test_get_nonexistent_config() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let value = test.get("field that does not exist", SelectMode::First);
assert!(value.is_none());
Ok(())
}
#[test]
fn test_set() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
test.set("name", ConfigLevel::Build, Value::String(String::from("build-test")))?;
let value = test.get("name", SelectMode::First);
assert!(value.is_some());
assert_eq!(value.unwrap(), Value::String(String::from("build-test")));
Ok(())
}
#[test]
fn test_set_twice_does_not_change_config() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
assert!(test.set(
"name",
ConfigLevel::Build,
Value::String(String::from("build-test1"))
)?);
assert_eq!(
test.get("name", SelectMode::First).unwrap(),
Value::String(String::from("build-test1"))
);
assert!(!test.set(
"name",
ConfigLevel::Build,
Value::String(String::from("build-test1"))
)?);
assert_eq!(
test.get("name", SelectMode::First).unwrap(),
Value::String(String::from("build-test1"))
);
assert!(test.set(
"name",
ConfigLevel::Build,
Value::String(String::from("build-test2"))
)?);
assert_eq!(
test.get("name", SelectMode::First).unwrap(),
Value::String(String::from("build-test2"))
);
Ok(())
}
#[test]
fn test_set_build_from_none() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::default()),
build: Some(ConfigFile::default()),
global: Some(ConfigFile::default()),
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
let value_none = test.get("name", SelectMode::First);
assert!(value_none.is_none());
let error_set =
test.set("name", ConfigLevel::Default, Value::String(String::from("default")));
assert!(error_set.is_err(), "Should not be able to set default values at runtime");
let value_default = test.get("name", SelectMode::First);
assert!(
value_default.is_none(),
"Default value should be unset after failed attempt to set it"
);
test.set("name", ConfigLevel::Global, Value::String(String::from("global")))?;
let value_global = test.get("name", SelectMode::First);
assert!(value_global.is_some());
assert_eq!(value_global.unwrap(), Value::String(String::from("global")));
test.set("name", ConfigLevel::User, Value::String(String::from("user")))?;
let value_user = test.get("name", SelectMode::First);
assert!(value_user.is_some());
assert_eq!(value_user.unwrap(), Value::String(String::from("user")));
test.set("name", ConfigLevel::Build, Value::String(String::from("build")))?;
let value_build = test.get("name", SelectMode::First);
assert!(value_build.is_some());
assert_eq!(value_build.unwrap(), Value::String(String::from("build")));
Ok(())
}
#[test]
fn test_remove() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
test.remove("name", ConfigLevel::User)?;
let user_value = test.get("name", SelectMode::First);
assert!(user_value.is_some());
assert_eq!(user_value.unwrap(), Value::String(String::from("Build")));
test.remove("name", ConfigLevel::Build)?;
let global_value = test.get("name", SelectMode::First);
assert!(global_value.is_some());
assert_eq!(global_value.unwrap(), Value::String(String::from("Global")));
test.remove("name", ConfigLevel::Global)?;
let default_value = test.get("name", SelectMode::First);
assert!(default_value.is_some());
assert_eq!(default_value.unwrap(), Value::String(String::from("Default")));
let error_removed = test.remove("name", ConfigLevel::Default);
assert!(error_removed.is_err(), "Should not be able to remove a default value");
let default_value = test.get("name", SelectMode::First);
assert_eq!(
default_value,
Some(Value::String(String::from("Default"))),
"value should still be default after trying to remove it (was {:?})",
default_value
);
Ok(())
}
#[test]
fn test_default() {
let test = Config::new(None, None, None, Map::default(), Map::default());
let default_value = test.get("log.enabled", SelectMode::First);
assert_eq!(
default_value.unwrap(),
Value::Array(vec![Value::String("$FFX_LOG_ENABLED".to_string()), Value::Bool(true)])
);
}
#[test]
fn test_display() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: ConfigMap::default(),
};
let output = format!("{}", test);
assert!(output.len() > 0);
let user_reg = Regex::new("\"name\": \"User\"").expect("test regex");
assert_eq!(1, user_reg.find_iter(&output).count());
let build_reg = Regex::new("\"name\": \"Build\"").expect("test regex");
assert_eq!(1, build_reg.find_iter(&output).count());
let global_reg = Regex::new("\"name\": \"Global\"").expect("test regex");
assert_eq!(1, global_reg.find_iter(&output).count());
let default_reg = Regex::new("\"name\": \"Default\"").expect("test regex");
assert_eq!(1, default_reg.find_iter(&output).count());
Ok(())
}
fn test_map(value: Value) -> Option<Value> {
value
.as_str()
.map(|s| match s {
"TEST_MAP" => Value::String("passed".to_string()),
_ => Value::String("failed".to_string()),
})
.or(Some(value))
}
#[test]
fn test_mapping() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(MAPPED), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
let test_mapping = "TEST_MAP".to_string();
let test_passed = "passed".to_string();
let mapped_value = test.get("name", SelectMode::First).as_ref().recursive_map(&test_map);
assert_eq!(mapped_value, Some(Value::String(test_passed)));
let identity_value = test.get("name", SelectMode::First);
assert_eq!(identity_value, Some(Value::String(test_mapping)));
Ok(())
}
#[test]
fn test_nested_get() -> Result<()> {
let test = Config {
user: None,
build: None,
global: None,
default: ConfigMap::default(),
runtime: serde_json::from_slice(NESTED)?,
};
let value = test.get("name.nested", SelectMode::First);
assert_eq!(value, Some(Value::String("Nested".to_string())));
Ok(())
}
#[test]
fn test_nested_get_should_return_sub_tree() -> Result<()> {
let test = Config {
user: None,
build: None,
global: None,
default: serde_json::from_slice(DEFAULT)?,
runtime: serde_json::from_slice(NESTED)?,
};
let value = test.get("name", SelectMode::First);
assert_eq!(value, Some(serde_json::from_str("{\"nested\": \"Nested\"}")?));
Ok(())
}
#[test]
fn test_nested_get_should_return_full_match() -> Result<()> {
let test = Config {
user: None,
build: None,
global: None,
default: serde_json::from_slice(NESTED)?,
runtime: serde_json::from_slice(RUNTIME)?,
};
let value = test.get("name.nested", SelectMode::First);
assert_eq!(value, Some(Value::String("Nested".to_string())));
Ok(())
}
#[test]
fn test_nested_get_should_map_values_in_sub_tree() -> Result<()> {
let test = Config {
user: None,
build: None,
global: None,
default: serde_json::from_slice(NESTED)?,
runtime: serde_json::from_slice(DEEP)?,
};
let value = test.get("name.nested", SelectMode::First).as_ref().recursive_map(&test_map);
assert_eq!(value, Some(serde_json::from_str("{\"deep\": {\"name\": \"passed\"}}")?));
Ok(())
}
#[test]
fn test_nested_set_from_none() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::default()),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.set("name.nested", ConfigLevel::User, Value::Bool(false))?;
let nested_value = test.get("name", SelectMode::First);
assert_eq!(nested_value, Some(serde_json::from_str("{\"nested\": false}")?));
Ok(())
}
#[test]
fn test_nested_set_from_already_populated_tree() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(NESTED), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.set("name.updated", ConfigLevel::User, Value::Bool(true))?;
let expected = json!({
"nested": "Nested",
"updated": true
});
let nested_value = test.get("name", SelectMode::First);
assert_eq!(nested_value, Some(expected));
Ok(())
}
#[test]
fn test_nested_set_override_literals() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(LITERAL), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.set("name.updated", ConfigLevel::User, Value::Bool(true))?;
let expected = json!({
"updated": true
});
let nested_value = test.get("name", SelectMode::First);
assert_eq!(nested_value, Some(expected));
test.set("name.updated", ConfigLevel::User, serde_json::from_slice(NESTED)?)?;
let nested_value = test.get("name.updated.name.nested", SelectMode::First);
assert_eq!(nested_value, Some(Value::String(String::from("Nested"))));
Ok(())
}
#[test]
fn test_nested_remove_from_none() -> Result<()> {
let mut test = Config {
user: None,
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
let result = test.remove("name.nested", ConfigLevel::User);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_nested_remove_throws_error_if_key_not_found() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(NESTED), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
let result = test.remove("name.unknown", ConfigLevel::User);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_nested_remove_deletes_literals() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(DEEP), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.remove("name.nested.deep.name", ConfigLevel::User)?;
let value = test.get("name", SelectMode::First);
assert_eq!(value, None);
Ok(())
}
#[test]
fn test_nested_remove_deletes_subtrees() -> Result<()> {
let mut test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(DEEP), true)),
build: None,
global: None,
default: ConfigMap::default(),
runtime: ConfigMap::default(),
};
test.remove("name.nested", ConfigLevel::User)?;
let value = test.get("name", SelectMode::First);
assert_eq!(value, None);
Ok(())
}
#[test]
fn test_additive_mode() -> Result<()> {
let test = Config {
user: Some(ConfigFile::from_buf(None, BufReader::new(USER), true)),
build: Some(ConfigFile::from_buf(None, BufReader::new(BUILD), true)),
global: Some(ConfigFile::from_buf(None, BufReader::new(GLOBAL), true)),
default: serde_json::from_slice(DEFAULT)?,
runtime: serde_json::from_slice(RUNTIME)?,
};
let value = test.get("name", SelectMode::All);
match value {
Some(Value::Array(v)) => {
assert_eq!(v.len(), 5);
let mut v = v.into_iter();
assert_eq!(v.next(), Some(Value::String("Runtime".to_string())));
assert_eq!(v.next(), Some(Value::String("Build".to_string())));
assert_eq!(v.next(), Some(Value::String("User".to_string())));
assert_eq!(v.next(), Some(Value::String("Global".to_string())));
assert_eq!(v.next(), Some(Value::String("Default".to_string())));
}
_ => anyhow::bail!("additive mode should return a Value::Array full of all values."),
}
Ok(())
}
}