| // 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, |
| crate::environment::Environment, |
| crate::nested::{nested_get, nested_remove, nested_set}, |
| crate::{ConfigLevel, ConfigQuery}, |
| anyhow::{bail, Context, Result}, |
| config_macros::include_default, |
| serde_json::{Map, Value}, |
| std::{ |
| fmt, |
| fs::{File, OpenOptions}, |
| io::{BufReader, BufWriter, Read, Write}, |
| path::Path, |
| }, |
| }; |
| |
| #[derive(Debug, Clone)] |
| pub struct Config { |
| default: Option<Value>, |
| build: Option<Value>, |
| global: Option<Value>, |
| user: Option<Value>, |
| runtime: Option<Value>, |
| } |
| |
| struct PriorityIterator<'a> { |
| curr: Option<ConfigLevel>, |
| config: &'a Config, |
| } |
| |
| impl<'a> Iterator for PriorityIterator<'a> { |
| type Item = &'a Option<Value>; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| match &self.curr { |
| None => { |
| self.curr = Some(ConfigLevel::Runtime); |
| Some(&self.config.runtime) |
| } |
| Some(level) => match level { |
| ConfigLevel::Runtime => { |
| self.curr = Some(ConfigLevel::User); |
| Some(&self.config.user) |
| } |
| ConfigLevel::User => { |
| self.curr = Some(ConfigLevel::Build); |
| Some(&self.config.build) |
| } |
| ConfigLevel::Build => { |
| self.curr = Some(ConfigLevel::Global); |
| Some(&self.config.global) |
| } |
| ConfigLevel::Global => { |
| self.curr = Some(ConfigLevel::Default); |
| Some(&self.config.default) |
| } |
| ConfigLevel::Default => 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(file: impl Read) -> Option<Value> { |
| serde_json::from_reader(file).ok() |
| } |
| |
| /// Takes an optional path-like object and maps it to a buffer reader of that |
| /// file. |
| fn reader(path: Option<impl AsRef<Path>>) -> Result<Option<BufReader<File>>> { |
| match path { |
| Some(p) => OpenOptions::new() |
| .read(true) |
| .write(true) |
| .create(true) |
| .open(&p) |
| .map(|f| Some(BufReader::new(f))) |
| .context("opening read buffer"), |
| None => Ok(None), |
| } |
| } |
| |
| 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(()) |
| } |
| } |
| } |
| |
| /// Atomically write to the file by creating a temporary file and passing it |
| /// to the closure, and atomically rename it to the destination file. |
| /// TODO(102542): This isn't really atomic unless same fs. Also, should be RAII. |
| fn with_writer<F>(path: Option<&str>, f: F) -> Result<()> |
| where |
| F: FnOnce(Option<BufWriter<&mut tempfile::NamedTempFile>>) -> Result<()>, |
| { |
| if let Some(path) = path { |
| let parent = Path::new(path).parent().unwrap_or_else(|| Path::new(".")); |
| let mut tmp = tempfile::NamedTempFile::new_in(parent)?; |
| |
| f(Some(BufWriter::new(&mut tmp)))?; |
| |
| tmp.persist(path)?; |
| |
| Ok(()) |
| } else { |
| f(None) |
| } |
| } |
| |
| impl Config { |
| fn new( |
| global: Option<Value>, |
| build: Option<Value>, |
| user: Option<Value>, |
| runtime: Option<Value>, |
| ) -> Self { |
| Self { user, build, global, runtime, default: include_default!() } |
| } |
| |
| pub(crate) fn from_env( |
| env: &Environment, |
| build_dir: Option<&String>, |
| runtime: Option<Value>, |
| ) -> Result<Self> { |
| let build_dir = build_dir.and_then(|b| env.build.as_ref().and_then(|c| c.get(b))); |
| |
| let user = reader(env.user.as_ref())?.and_then(read_json); |
| let build = reader(build_dir.as_ref())?.and_then(read_json); |
| let global = reader(env.global.as_ref())?.and_then(read_json); |
| |
| Ok(Self::new(global, build, user, runtime)) |
| } |
| |
| fn write<W: Write>(&self, global: Option<W>, build: Option<W>, user: Option<W>) -> Result<()> { |
| write_json(user, self.user.as_ref())?; |
| write_json(build, self.build.as_ref())?; |
| write_json(global, self.global.as_ref())?; |
| Ok(()) |
| } |
| |
| pub(crate) fn save( |
| &self, |
| global: Option<&String>, |
| build: Option<&String>, |
| user: Option<&String>, |
| ) -> Result<()> { |
| // First save the config to a temp file in the same location as the file, then atomically |
| // rename the file to the final location to avoid partially written files. |
| |
| with_writer(global.map(|s| s.as_str()), |global| { |
| with_writer(build.map(|s| s.as_str()), |build| { |
| with_writer(user.map(|s| s.as_str()), |user| self.write(global, build, user)) |
| }) |
| }) |
| } |
| |
| pub fn get(&self, key: &ConfigQuery<'_>) -> Option<Value> { |
| if let Some(name) = key.name { |
| // Check for nested config values if there's a '.' in the key |
| let key_vec: Vec<&str> = name.split('.').collect(); |
| if let Some(level) = key.level { |
| let config = match level { |
| ConfigLevel::Runtime => &self.runtime, |
| ConfigLevel::User => &self.user, |
| ConfigLevel::Build => &self.build, |
| ConfigLevel::Global => &self.global, |
| ConfigLevel::Default => &self.default, |
| }; |
| nested_get(config.as_ref(), key_vec[0], &key_vec[1..]).cloned() |
| } else { |
| match key.select { |
| SelectMode::First => self |
| .iter() |
| .find_map(|c| nested_get(c.as_ref(), key_vec[0], &key_vec[1..])) |
| .cloned(), |
| SelectMode::All => { |
| let result: Vec<Value> = self |
| .iter() |
| .filter_map(|c| nested_get(c.as_ref(), key_vec[0], &key_vec[1..])) |
| .cloned() |
| .collect(); |
| if result.len() > 0 { |
| Some(Value::Array(result)) |
| } else { |
| None |
| } |
| } |
| } |
| } |
| } else { |
| if let Some(level) = key.level { |
| let config = match level { |
| ConfigLevel::Runtime => &self.runtime, |
| ConfigLevel::User => &self.user, |
| ConfigLevel::Build => &self.build, |
| ConfigLevel::Global => &self.global, |
| ConfigLevel::Default => &self.default, |
| }; |
| config.clone() |
| } else { |
| // Not really supported now. Maybe in the future. |
| None |
| } |
| } |
| } |
| |
| fn validate_write_query<'a>(query: &'a ConfigQuery<'a>) -> Result<(&'a str, &'a ConfigLevel)> { |
| match query { |
| ConfigQuery { name: None, .. } => { |
| bail!("Name of configuration is required to write to a value") |
| } |
| ConfigQuery { level: None, .. } => { |
| bail!("Level of configuration is required to write to a value") |
| } |
| ConfigQuery { level: Some(level), .. } if level == &ConfigLevel::Default => { |
| bail!("Cannot override defaults") |
| } |
| ConfigQuery { name: Some(key), level: Some(level), .. } => Ok((*key, level)), |
| } |
| } |
| |
| pub fn set(&mut self, query: &ConfigQuery<'_>, value: Value) -> Result<bool> { |
| let (key, level) = Self::validate_write_query(query)?; |
| |
| let key_vec: Vec<&str> = key.split('.').collect(); |
| let config_changed = |
| nested_set(&mut self.get_level_map(level), key_vec[0], &key_vec[1..], value); |
| Ok(config_changed) |
| } |
| |
| pub fn remove(&mut self, query: &ConfigQuery<'_>) -> Result<()> { |
| let (key, level) = Self::validate_write_query(query)?; |
| let key_vec: Vec<&str> = key.split('.').collect(); |
| nested_remove(&mut self.get_level_map(&level), key_vec[0], &key_vec[1..]) |
| } |
| |
| fn iter(&self) -> PriorityIterator<'_> { |
| PriorityIterator { curr: None, config: self } |
| } |
| |
| fn get_level_map(&mut self, level: &ConfigLevel) -> &mut Map<String, Value> { |
| let config = match level { |
| ConfigLevel::Runtime => &mut self.runtime, |
| ConfigLevel::User => &mut self.user, |
| ConfigLevel::Build => &mut self.build, |
| ConfigLevel::Global => &mut self.global, |
| ConfigLevel::Default => &mut self.default, |
| }; |
| // Ensure current value is always a map. |
| match config { |
| Some(v) => { |
| if !v.is_object() { |
| // This must be a map. Will override any literals or arrays. |
| *config = Some(Value::Object(Map::new())); |
| } |
| } |
| None => *config = Some(Value::Object(Map::new())), |
| } |
| // Ok to expect as this is ensured above. |
| config |
| .as_mut() |
| .expect("uninitialzed configuration") |
| .as_object_mut() |
| .expect("unable to initialize configuration map") |
| } |
| } |
| |
| 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; |
| use std::io::{BufReader, BufWriter}; |
| |
| const ERROR: &'static str = "0"; |
| |
| const USER: &'static str = r#" |
| { |
| "name": "User" |
| }"#; |
| |
| const BUILD: &'static str = r#" |
| { |
| "name": "Build" |
| }"#; |
| |
| const GLOBAL: &'static str = r#" |
| { |
| "name": "Global" |
| }"#; |
| |
| const DEFAULT: &'static str = r#" |
| { |
| "name": "Default" |
| }"#; |
| |
| const RUNTIME: &'static str = r#" |
| { |
| "name": "Runtime" |
| }"#; |
| |
| const MAPPED: &'static str = r#" |
| { |
| "name": "TEST_MAP" |
| }"#; |
| |
| const NESTED: &'static str = r#" |
| { |
| "name": { |
| "nested": "Nested" |
| } |
| }"#; |
| |
| const DEEP: &'static str = r#" |
| { |
| "name": { |
| "nested": { |
| "deep": { |
| "name": "TEST_MAP" |
| } |
| } |
| } |
| }"#; |
| |
| #[test] |
| fn test_persistent_build() -> Result<()> { |
| let mut user_file = String::from(USER); |
| let mut build_file = String::from(BUILD); |
| let mut global_file = String::from(GLOBAL); |
| |
| let persistent_config = Config::new( |
| read_json(BufReader::new(global_file.as_bytes())), |
| read_json(BufReader::new(build_file.as_bytes())), |
| read_json(BufReader::new(user_file.as_bytes())), |
| None, |
| ); |
| |
| let value = persistent_config.get(&"name".into()); |
| assert!(value.is_some()); |
| assert_eq!(value.unwrap(), Value::String(String::from("User"))); |
| |
| 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 |
| 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(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: Some(serde_json::from_str(RUNTIME)?), |
| }; |
| |
| let mut test_iter = test.iter(); |
| assert_eq!(test_iter.next(), Some(&test.runtime)); |
| assert_eq!(test_iter.next(), Some(&test.user)); |
| assert_eq!(test_iter.next(), Some(&test.build)); |
| assert_eq!(test_iter.next(), Some(&test.global)); |
| assert_eq!(test_iter.next(), Some(&test.default)); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_priority_iterator_with_nones() -> Result<()> { |
| let test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: None, |
| global: None, |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| |
| let mut test_iter = test.iter(); |
| assert_eq!(test_iter.next(), Some(&test.runtime)); |
| assert_eq!(test_iter.next(), Some(&test.user)); |
| assert_eq!(test_iter.next(), Some(&test.build)); |
| assert_eq!(test_iter.next(), Some(&test.global)); |
| assert_eq!(test_iter.next(), Some(&test.default)); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_get() -> Result<()> { |
| let test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| |
| let value = test.get(&"name".into()); |
| assert!(value.is_some()); |
| assert_eq!(value.unwrap(), Value::String(String::from("User"))); |
| |
| let test_build = Config { |
| user: None, |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| |
| let value_build = test_build.get(&"name".into()); |
| assert!(value_build.is_some()); |
| assert_eq!(value_build.unwrap(), Value::String(String::from("Build"))); |
| |
| let test_global = Config { |
| user: None, |
| build: None, |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| |
| let value_global = test_global.get(&"name".into()); |
| 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: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| |
| let value_default = test_default.get(&"name".into()); |
| 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: None, runtime: None }; |
| |
| let value_none = test_none.get(&"name".into()); |
| assert!(value_none.is_none()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_set_non_map_value() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(ERROR)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("whatever")))?; |
| let value = test.get(&"name".into()); |
| assert_eq!(value, Some(Value::String(String::from("whatever")))); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_get_nonexistent_config() -> Result<()> { |
| let test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| let value = test.get(&"field that does not exist".into()); |
| assert!(value.is_none()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_set() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("user-test")))?; |
| let value = test.get(&"name".into()); |
| assert!(value.is_some()); |
| assert_eq!(value.unwrap(), Value::String(String::from("user-test"))); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_set_twice_does_not_change_config() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| assert!(test.set( |
| &("name", &ConfigLevel::User).into(), |
| Value::String(String::from("user-test1")) |
| )?); |
| assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test1"))); |
| |
| assert!(!test.set( |
| &("name", &ConfigLevel::User).into(), |
| Value::String(String::from("user-test1")) |
| )?); |
| assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test1"))); |
| |
| assert!(test.set( |
| &("name", &ConfigLevel::User).into(), |
| Value::String(String::from("user-test2")) |
| )?); |
| assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test2"))); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_set_build_from_none() -> Result<()> { |
| let mut test = |
| Config { user: None, build: None, global: None, default: None, runtime: None }; |
| let value_none = test.get(&"name".into()); |
| assert!(value_none.is_none()); |
| let error_set = test |
| .set(&("name", &ConfigLevel::Default).into(), 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".into()); |
| assert!( |
| value_default.is_none(), |
| "Default value should be unset after failed attempt to set it" |
| ); |
| test.set(&("name", &ConfigLevel::Global).into(), Value::String(String::from("global")))?; |
| let value_global = test.get(&"name".into()); |
| assert!(value_global.is_some()); |
| assert_eq!(value_global.unwrap(), Value::String(String::from("global"))); |
| test.set(&("name", &ConfigLevel::Build).into(), Value::String(String::from("build")))?; |
| let value_build = test.get(&"name".into()); |
| assert!(value_build.is_some()); |
| assert_eq!(value_build.unwrap(), Value::String(String::from("build"))); |
| test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("user")))?; |
| let value_user = test.get(&"name".into()); |
| assert!(value_user.is_some()); |
| assert_eq!(value_user.unwrap(), Value::String(String::from("user"))); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_remove() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| test.remove(&("name", &ConfigLevel::User).into())?; |
| let user_value = test.get(&"name".into()); |
| assert!(user_value.is_some()); |
| assert_eq!(user_value.unwrap(), Value::String(String::from("Build"))); |
| test.remove(&("name", &ConfigLevel::Build).into())?; |
| let global_value = test.get(&"name".into()); |
| assert!(global_value.is_some()); |
| assert_eq!(global_value.unwrap(), Value::String(String::from("Global"))); |
| test.remove(&("name", &ConfigLevel::Global).into())?; |
| let default_value = test.get(&"name".into()); |
| assert!(default_value.is_some()); |
| assert_eq!(default_value.unwrap(), Value::String(String::from("Default"))); |
| let error_removed = test.remove(&("name", &ConfigLevel::Default).into()); |
| assert!(error_removed.is_err(), "Should not be able to remove a default value"); |
| let default_value = test.get(&"name".into()); |
| 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, None); |
| let default_value = test.get(&"log.enabled".into()); |
| 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(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: None, |
| }; |
| 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(serde_json::from_str(MAPPED)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| let test_mapping = "TEST_MAP".to_string(); |
| let test_passed = "passed".to_string(); |
| let mapped_value = test.get(&"name".into()).as_ref().recursive_map(&test_map); |
| assert_eq!(mapped_value, Some(Value::String(test_passed))); |
| let identity_value = test.get(&"name".into()); |
| 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: None, |
| runtime: Some(serde_json::from_str(NESTED)?), |
| }; |
| let value = test.get(&"name.nested".into()); |
| 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: Some(serde_json::from_str(DEFAULT)?), |
| runtime: Some(serde_json::from_str(NESTED)?), |
| }; |
| let value = test.get(&"name".into()); |
| 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: Some(serde_json::from_str(NESTED)?), |
| runtime: Some(serde_json::from_str(RUNTIME)?), |
| }; |
| let value = test.get(&"name.nested".into()); |
| 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: Some(serde_json::from_str(NESTED)?), |
| runtime: Some(serde_json::from_str(DEEP)?), |
| }; |
| let value = test.get(&"name.nested".into()).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: None, build: None, global: None, default: None, runtime: None }; |
| test.set(&("name.nested", &ConfigLevel::User).into(), Value::Bool(false))?; |
| let nested_value = test.get(&"name".into()); |
| 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(serde_json::from_str(NESTED)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| test.set(&("name.updated", &ConfigLevel::User).into(), Value::Bool(true))?; |
| let expected = json!({ |
| "nested": "Nested", |
| "updated": true |
| }); |
| let nested_value = test.get(&"name".into()); |
| assert_eq!(nested_value, Some(expected)); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_nested_set_override_literals() -> Result<()> { |
| let mut test = Config { |
| user: Some(json!([])), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| test.set(&("name.updated", &ConfigLevel::User).into(), Value::Bool(true))?; |
| let expected = json!({ |
| "updated": true |
| }); |
| let nested_value = test.get(&"name".into()); |
| assert_eq!(nested_value, Some(expected)); |
| test.set(&("name.updated", &ConfigLevel::User).into(), serde_json::from_str(NESTED)?)?; |
| let nested_value = test.get(&"name.updated.name.nested".into()); |
| 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: None, runtime: None }; |
| let result = test.remove(&("name.nested", &ConfigLevel::User).into()); |
| assert!(result.is_err()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_nested_remove_throws_error_if_key_not_found() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(NESTED)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| let result = test.remove(&("name.unknown", &ConfigLevel::User).into()); |
| assert!(result.is_err()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_nested_remove_deletes_literals() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(DEEP)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| test.remove(&("name.nested.deep.name", &ConfigLevel::User).into())?; |
| let value = test.get(&"name".into()); |
| assert_eq!(value, None); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_nested_remove_deletes_subtrees() -> Result<()> { |
| let mut test = Config { |
| user: Some(serde_json::from_str(DEEP)?), |
| build: None, |
| global: None, |
| default: None, |
| runtime: None, |
| }; |
| test.remove(&("name.nested", &ConfigLevel::User).into())?; |
| let value = test.get(&"name".into()); |
| assert_eq!(value, None); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_additive_mode() -> Result<()> { |
| let test = Config { |
| user: Some(serde_json::from_str(USER)?), |
| build: Some(serde_json::from_str(BUILD)?), |
| global: Some(serde_json::from_str(GLOBAL)?), |
| default: Some(serde_json::from_str(DEFAULT)?), |
| runtime: Some(serde_json::from_str(RUNTIME)?), |
| }; |
| let value = test.get(&("name", &SelectMode::All).into()); |
| match value { |
| Some(Value::Array(v)) => { |
| assert_eq!(v.len(), 5); |
| assert_eq!(v[0], Value::String("Runtime".to_string())); |
| assert_eq!(v[1], Value::String("User".to_string())); |
| assert_eq!(v[2], Value::String("Build".to_string())); |
| assert_eq!(v[3], Value::String("Global".to_string())); |
| assert_eq!(v[4], Value::String("Default".to_string())); |
| } |
| _ => bail!("additive mode should return a Value::Array full of all values."), |
| } |
| Ok(()) |
| } |
| } |