blob: 7699aa2a24a5410225df111aa1f4638c7fe989e2 [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::nested::nested_set;
use anyhow::{anyhow, bail, Context, Result};
use serde_json::{Map, Value};
use std::{fs::File, io::BufReader, path::Path};
fn try_split_name_value_pairs(config: &String) -> Result<Option<Value>> {
let mut runtime_config = Map::new();
for pair in config.split(',') {
let s: Vec<&str> = pair.trim().split('=').collect();
if s.len() == 2 {
let key_vec: Vec<&str> = s[0].split('.').collect();
nested_set(
&mut runtime_config,
key_vec[0],
&key_vec[1..],
Value::String(s[1].to_string()),
);
} else {
bail!(
"--config must either be a file path, /
a valid JSON object, or comma separated key=value pairs."
)
}
}
Ok(Some(Value::Object(runtime_config)))
}
fn try_parse_json(config: &String) -> Result<Option<Value>> {
match serde_json::from_str(config) {
Ok(v) => Ok(Some(v)),
Err(_) => bail!("could not parse json from --config flag"),
}
}
fn try_read_file(config: &String) -> Result<Option<Value>> {
let path = Path::new(config);
if path.is_file() {
let file = File::open(path).map(|f| BufReader::new(f)).context("opening read buffer")?;
serde_json::from_reader(file).map_err(|e| anyhow!("Could not parse \"{}\": {}", config, e))
} else {
bail!("--config input is not a file path")
}
}
pub(crate) fn populate_runtime_config(config: &Option<String>) -> Result<Option<Value>> {
match config {
Some(c) => try_read_file(c)
.or_else(|_| try_parse_json(c))
.or_else(|_| try_split_name_value_pairs(c)),
None => Ok(None),
}
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_config_runtime() -> Result<()> {
let (key_1, value_1) = ("test 1", "test 2");
let (key_2, value_2) = ("test 3", "test 4");
let config = populate_runtime_config(&Some(format!(
"{}={}, {}={}",
key_1, value_1, key_2, value_2
)))?
.expect("expected test configuration");
let missing_key = "whatever";
assert_eq!(None, config.get(missing_key));
assert_eq!(Some(&Value::String(value_1.to_string())), config.get(key_1));
assert_eq!(Some(&Value::String(value_2.to_string())), config.get(key_2));
Ok(())
}
#[test]
fn test_dot_notation_config_runtime() -> Result<()> {
let (key_1, value_1) = ("test.nested", "test");
let (key_2, value_2) = ("test.another_nested", "another_test");
let config = populate_runtime_config(&Some(format!(
"{}={}, {}={}",
key_1, value_1, key_2, value_2
)))?
.expect("expected test configuration");
let missing_key = "whatever";
assert_eq!(None, config.get(missing_key));
let key_vec_1: Vec<&str> = key_1.split('.').collect();
if let Some(c) = config.get(key_vec_1[0]) {
assert_eq!(Some(&Value::String(value_1.to_string())), c.get(key_vec_1[1]));
} else {
bail!("failed to get nested config");
}
let key_vec_2: Vec<&str> = key_2.split('.').collect();
if let Some(c) = config.get(key_vec_2[0]) {
assert_eq!(Some(&Value::String(value_2.to_string())), c.get(key_vec_2[1]));
} else {
bail!("failed to get nested config");
}
Ok(())
}
#[test]
fn test_file_load() -> Result<()> {
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let mut file = tmp_file.as_file();
file.write_all(b"{\"test\": {\"nested\": true, \"another_nested\":false}}")
.context("writing configuration file")?;
file.sync_all().context("syncing configuration file to filesystem")?;
let config = populate_runtime_config(&tmp_file.path().to_str().map(|s| s.to_string()))?
.expect("config");
if let Some(c) = config.get("test") {
assert_eq!(Some(&Value::Bool(true)), c.get("nested"));
assert_eq!(Some(&Value::Bool(false)), c.get("another_nested"));
Ok(())
} else {
bail!("failed to get nested config");
}
}
}