use std::collections::{BTreeMap, HashSet};
use std::env;
use std::fs::File;
use std::path::PathBuf;
use std::time::Duration;
use apollo_infra_utils::path::resolve_project_relative_path;
use assert_matches::assert_matches;
use clap::Command;
use itertools::chain;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tempfile::{NamedTempFile, TempDir};
use url::Url;
use validator::Validate;
use crate::command::{get_command_matches, update_config_map_by_command_args};
use crate::converters::{
deserialize_milliseconds_to_duration,
deserialize_optional_list_with_url_and_headers,
serialize_optional_list_with_url_and_headers,
UrlAndHeaders,
};
use crate::dumping::{
combine_config_map_and_pointers,
generate_struct_pointer,
prepend_sub_config_name,
required_param_description,
ser_generated_param,
ser_optional_param,
ser_optional_sub_config,
ser_param,
ser_pointer_target_param,
ser_pointer_target_required_param,
ser_required_param,
set_pointing_param_paths,
SerializeConfig,
};
use crate::loading::{
load,
load_and_process_config,
split_pointers_map,
split_values_and_types,
update_config_map_by_pointers,
update_optional_values,
};
use crate::presentation::get_config_presentation;
use crate::{
ConfigError,
ParamPath,
ParamPrivacy,
ParamPrivacyInput,
SerializationType,
SerializedContent,
SerializedParam,
CONFIG_FILE_ARG,
};
lazy_static! {
static ref CUSTOM_CONFIG_PATH: PathBuf =
resolve_project_relative_path("crates/apollo_config/resources/custom_config_example.json")
.unwrap();
}
#[derive(Clone, Copy, Default, Serialize, Deserialize, Debug, PartialEq, Validate)]
struct InnerConfig {
#[validate(range(min = 0, max = 10))]
o: usize,
}
impl SerializeConfig for InnerConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
BTreeMap::from([ser_param("o", &self.o, "This is o.", ParamPrivacyInput::Public)])
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Validate)]
struct OuterConfig {
opt_elem: Option<usize>,
opt_config: Option<InnerConfig>,
#[validate(nested)]
inner_config: InnerConfig,
}
impl SerializeConfig for OuterConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
chain!(
ser_optional_param(
&self.opt_elem,
1,
"opt_elem",
"This is elem.",
ParamPrivacyInput::Public
),
ser_optional_sub_config(&self.opt_config, "opt_config"),
prepend_sub_config_name(self.inner_config.dump(), "inner_config"),
)
.collect()
}
}
#[test]
fn dump_and_load_config() {
let some_outer_config = OuterConfig {
opt_elem: Some(2),
opt_config: Some(InnerConfig { o: 3 }),
inner_config: InnerConfig { o: 4 },
};
let none_outer_config =
OuterConfig { opt_elem: None, opt_config: None, inner_config: InnerConfig { o: 5 } };
for outer_config in [some_outer_config, none_outer_config] {
let (mut dumped, _) = split_values_and_types(outer_config.dump());
update_optional_values(&mut dumped);
let loaded_config = load::<OuterConfig>(&dumped).unwrap();
assert_eq!(loaded_config, outer_config);
}
}
#[test]
fn test_validation() {
let outer_config =
OuterConfig { opt_elem: None, opt_config: None, inner_config: InnerConfig { o: 20 } };
assert!(outer_config.validate().is_err());
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
struct TypicalConfig {
#[serde(deserialize_with = "deserialize_milliseconds_to_duration")]
a: Duration,
b: String,
c: bool,
d: i64,
e: u64,
f: f64,
}
impl SerializeConfig for TypicalConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
BTreeMap::from([
ser_param(
"a",
&self.a.as_millis(),
"This is a as milliseconds.",
ParamPrivacyInput::Public,
),
ser_param("b", &self.b, "This is b.", ParamPrivacyInput::Public),
ser_param("c", &self.c, "This is c.", ParamPrivacyInput::Private),
ser_param("d", &self.d, "This is d.", ParamPrivacyInput::Public),
ser_param("e", &self.e, "This is e.", ParamPrivacyInput::Public),
ser_param("f", &self.f, "This is f.", ParamPrivacyInput::Public),
])
}
}
#[test]
fn test_update_dumped_config() {
let command = Command::new("Testing");
let dumped_config = TypicalConfig {
a: Duration::from_secs(1),
b: "bbb".to_owned(),
c: false,
d: -1,
e: 10,
f: 1.5,
}
.dump();
let args = vec!["Testing", "--a", "1234", "--b", "15", "--d", "-2", "--e", "20", "--f", "0.5"];
env::set_var("C", "true");
let args: Vec<String> = args.into_iter().map(|s| s.to_owned()).collect();
let arg_matches = get_command_matches(&dumped_config, command, args).unwrap();
let (mut config_map, required_map) = split_values_and_types(dumped_config);
update_config_map_by_command_args(&mut config_map, &required_map, &arg_matches).unwrap();
assert_eq!(json!(1234), config_map["a"]);
assert_eq!(json!("15"), config_map["b"]);
assert_eq!(json!(true), config_map["c"]);
assert_eq!(json!(-2), config_map["d"]);
assert_eq!(json!(20), config_map["e"]);
assert_eq!(json!(0.5), config_map["f"]);
let loaded_config: TypicalConfig = load(&config_map).unwrap();
assert_eq!(Duration::from_millis(1234), loaded_config.a);
}
#[test]
fn test_env_nested_params() {
let command = Command::new("Testing");
let dumped_config = OuterConfig {
opt_elem: Some(1),
opt_config: Some(InnerConfig { o: 2 }),
inner_config: InnerConfig { o: 3 },
}
.dump();
let args = vec!["Testing", "--opt_elem", "1234"];
env::set_var("OPT_CONFIG____IS_NONE__", "true");
env::set_var("INNER_CONFIG__O", "4");
let args: Vec<String> = args.into_iter().map(|s| s.to_owned()).collect();
let arg_matches = get_command_matches(&dumped_config, command, args).unwrap();
let (mut config_map, required_map) = split_values_and_types(dumped_config);
update_config_map_by_command_args(&mut config_map, &required_map, &arg_matches).unwrap();
assert_eq!(json!(1234), config_map["opt_elem"]);
assert_eq!(json!(true), config_map["opt_config.#is_none"]);
assert_eq!(json!(4), config_map["inner_config.o"]);
update_optional_values(&mut config_map);
let loaded_config: OuterConfig = load(&config_map).unwrap();
assert_eq!(Some(1234), loaded_config.opt_elem);
assert_eq!(None, loaded_config.opt_config);
assert_eq!(4, loaded_config.inner_config.o);
}
#[test]
fn test_config_presentation() {
let config = TypicalConfig {
a: Duration::from_secs(1),
b: "bbb".to_owned(),
c: false,
d: -1,
e: 10,
f: 0.5,
};
let presentation = get_config_presentation(&config, true).unwrap();
let keys: Vec<_> = presentation.as_object().unwrap().keys().collect();
assert_eq!(keys, vec!["a", "b", "c", "d", "e", "f"]);
let public_presentation = get_config_presentation(&config, false).unwrap();
let keys: Vec<_> = public_presentation.as_object().unwrap().keys().collect();
assert_eq!(keys, vec!["a", "b", "d", "e", "f"]);
}
#[test]
fn test_nested_config_presentation() {
let configs = vec![
OuterConfig {
opt_elem: Some(1),
opt_config: Some(InnerConfig { o: 2 }),
inner_config: InnerConfig { o: 3 },
},
OuterConfig {
opt_elem: None,
opt_config: Some(InnerConfig { o: 2 }),
inner_config: InnerConfig { o: 3 },
},
OuterConfig { opt_elem: Some(1), opt_config: None, inner_config: InnerConfig { o: 3 } },
];
for config in configs {
let presentation = get_config_presentation(&config, true).unwrap();
let keys: Vec<_> = presentation.as_object().unwrap().keys().collect();
assert_eq!(keys, vec!["inner_config", "opt_config", "opt_elem"]);
let public_presentation = get_config_presentation(&config, false).unwrap();
let keys: Vec<_> = public_presentation.as_object().unwrap().keys().collect();
assert_eq!(keys, vec!["inner_config", "opt_config", "opt_elem"]);
}
}
#[test]
fn test_pointers_flow() {
const TARGET_PARAM_NAME: &str = "a";
const TARGET_PARAM_DESCRIPTION: &str = "This is common a.";
const POINTING_PARAM_DESCRIPTION: &str = "This is a.";
const PUBLIC_POINTING_PARAM_NAME: &str = "public_a.a";
const PRIVATE_POINTING_PARAM_NAME: &str = "private_a.a";
const WHITELISTED_POINTING_PARAM_NAME: &str = "non_pointing.a";
const VALUE: usize = 5;
let config_map = BTreeMap::from([
ser_param(
PUBLIC_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Public,
),
ser_param(
PRIVATE_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Private,
),
ser_param(
WHITELISTED_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Private,
),
]);
let pointers = vec![(
ser_pointer_target_param(TARGET_PARAM_NAME, &json!(10), TARGET_PARAM_DESCRIPTION),
HashSet::from([
PUBLIC_POINTING_PARAM_NAME.to_string(),
PRIVATE_POINTING_PARAM_NAME.to_string(),
]),
)];
let non_pointer_params = HashSet::from([WHITELISTED_POINTING_PARAM_NAME.to_string()]);
let stored_map =
combine_config_map_and_pointers(config_map, &pointers, &non_pointer_params).unwrap();
assert_eq!(
stored_map[PUBLIC_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::PointerTarget(TARGET_PARAM_NAME.to_owned()),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map[PRIVATE_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::PointerTarget(TARGET_PARAM_NAME.to_owned()),
privacy: ParamPrivacy::Private,
})
);
assert_eq!(
stored_map[WHITELISTED_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::DefaultValue(json!(VALUE)),
privacy: ParamPrivacy::Private,
})
);
assert_eq!(
stored_map[TARGET_PARAM_NAME],
json!(SerializedParam {
description: TARGET_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::DefaultValue(json!(10)),
privacy: ParamPrivacy::TemporaryValue,
})
);
let serialized = serde_json::to_string(&stored_map).unwrap();
let loaded = serde_json::from_str(&serialized).unwrap();
let (loaded_config_map, loaded_pointers_map) = split_pointers_map(loaded);
let (mut config_map, _) = split_values_and_types(loaded_config_map);
update_config_map_by_pointers(&mut config_map, &loaded_pointers_map).unwrap();
assert_eq!(config_map[PUBLIC_POINTING_PARAM_NAME], json!(10));
assert_eq!(config_map[PUBLIC_POINTING_PARAM_NAME], config_map[PRIVATE_POINTING_PARAM_NAME]);
}
#[test]
fn test_required_pointers_flow() {
const REQUIRED_PARAM_NAME: &str = "b";
const REQUIRED_PARAM_DESCRIPTION: &str = "This is common required b.";
const POINTING_PARAM_DESCRIPTION: &str = "This is b.";
const PUBLIC_POINTING_PARAM_NAME: &str = "public_b.b";
const PRIVATE_POINTING_PARAM_NAME: &str = "private_b.b";
const WHITELISTED_POINTING_PARAM_NAME: &str = "non_pointing.b";
const VALUE: usize = 6;
let config_map = BTreeMap::from([
ser_param(
PUBLIC_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Public,
),
ser_param(
PRIVATE_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Private,
),
ser_param(
WHITELISTED_POINTING_PARAM_NAME,
&json!(VALUE),
POINTING_PARAM_DESCRIPTION,
ParamPrivacyInput::Private,
),
]);
let pointers = vec![(
ser_pointer_target_required_param(
REQUIRED_PARAM_NAME,
SerializationType::PositiveInteger,
REQUIRED_PARAM_DESCRIPTION,
),
HashSet::from([
PUBLIC_POINTING_PARAM_NAME.to_string(),
PRIVATE_POINTING_PARAM_NAME.to_string(),
]),
)];
let non_pointer_params = HashSet::from([WHITELISTED_POINTING_PARAM_NAME.to_string()]);
let stored_map =
combine_config_map_and_pointers(config_map, &pointers, &non_pointer_params).unwrap();
assert_eq!(
stored_map[PUBLIC_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::PointerTarget(REQUIRED_PARAM_NAME.to_owned()),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map[PRIVATE_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::PointerTarget(REQUIRED_PARAM_NAME.to_owned()),
privacy: ParamPrivacy::Private,
})
);
assert_eq!(
stored_map[WHITELISTED_POINTING_PARAM_NAME],
json!(SerializedParam {
description: POINTING_PARAM_DESCRIPTION.to_owned(),
content: SerializedContent::DefaultValue(json!(VALUE)),
privacy: ParamPrivacy::Private,
})
);
assert_eq!(
stored_map[REQUIRED_PARAM_NAME],
json!(SerializedParam {
description: required_param_description(REQUIRED_PARAM_DESCRIPTION).to_owned(),
content: SerializedContent::ParamType(SerializationType::PositiveInteger),
privacy: ParamPrivacy::TemporaryValue,
})
);
}
#[test]
#[should_panic(
expected = "The target param should_be_pointing.c should point to c, or to be whitelisted."
)]
fn test_missing_pointer_flow() {
const TARGET_PARAM_NAME: &str = "c";
const TARGET_PARAM_DESCRIPTION: &str = "This is common c.";
const PARAM_DESCRIPTION: &str = "This is c.";
const NON_POINTING_PARAM_NAME: &str = "should_be_pointing.c";
let config_map = BTreeMap::from([ser_param(
NON_POINTING_PARAM_NAME,
&json!(7),
PARAM_DESCRIPTION,
ParamPrivacyInput::Private,
)]);
let pointers = vec![(
ser_pointer_target_param(TARGET_PARAM_NAME, &json!(10), TARGET_PARAM_DESCRIPTION),
HashSet::new(),
)];
let non_pointer_params = HashSet::new();
combine_config_map_and_pointers(config_map, &pointers, &non_pointer_params).unwrap();
}
#[test]
fn test_replace_pointers() {
let (mut config_map, _) = split_values_and_types(BTreeMap::from([ser_param(
"a",
&json!(5),
"This is a.",
ParamPrivacyInput::Public,
)]));
let pointers_map =
BTreeMap::from([("b".to_owned(), "a".to_owned()), ("c".to_owned(), "a".to_owned())]);
update_config_map_by_pointers(&mut config_map, &pointers_map).unwrap();
assert_eq!(config_map["a"], config_map["b"]);
assert_eq!(config_map["a"], config_map["c"]);
let err = update_config_map_by_pointers(&mut BTreeMap::default(), &pointers_map).unwrap_err();
assert_matches!(err, ConfigError::PointerTargetNotFound { .. });
}
#[test]
fn test_struct_pointers() {
const TARGET_PREFIX: &str = "base";
let target_value =
RequiredConfig { param_path: "Not a default param_path.".to_owned(), num: 10 };
let config_map = StructPointersConfig::default().dump();
let pointers = generate_struct_pointer(
TARGET_PREFIX.to_owned(),
&target_value,
set_pointing_param_paths(&["a", "b"]),
);
let stored_map =
combine_config_map_and_pointers(config_map, &pointers, &HashSet::default()).unwrap();
assert_eq!(
stored_map["a.param_path"],
json!(SerializedParam {
description: required_param_description(RequiredConfig::param_path_description())
.to_owned(),
content: SerializedContent::PointerTarget(
format!("{TARGET_PREFIX}.param_path").to_owned()
),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map["a.num"],
json!(SerializedParam {
description: RequiredConfig::num_description().to_owned(),
content: SerializedContent::PointerTarget(format!("{TARGET_PREFIX}.num").to_owned()),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map["b.param_path"],
json!(SerializedParam {
description: required_param_description(RequiredConfig::param_path_description())
.to_owned(),
content: SerializedContent::PointerTarget(
format!("{TARGET_PREFIX}.param_path").to_owned()
),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map["b.num"],
json!(SerializedParam {
description: RequiredConfig::num_description().to_owned(),
content: SerializedContent::PointerTarget(format!("{TARGET_PREFIX}.num").to_owned()),
privacy: ParamPrivacy::Public,
})
);
assert_eq!(
stored_map[format!("{TARGET_PREFIX}.param_path").to_owned()],
json!(SerializedParam {
description: required_param_description(RequiredConfig::param_path_description())
.to_owned(),
content: SerializedContent::ParamType(SerializationType::String),
privacy: ParamPrivacy::TemporaryValue,
})
);
assert_eq!(
stored_map[format!("{TARGET_PREFIX}.num").to_owned()],
json!(SerializedParam {
description: RequiredConfig::num_description().to_owned(),
content: SerializedContent::DefaultValue(json!(10)),
privacy: ParamPrivacy::TemporaryValue,
})
);
}
#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct StructPointersConfig {
pub a: RequiredConfig,
pub b: RequiredConfig,
}
impl SerializeConfig for StructPointersConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
let mut dump = BTreeMap::new();
dump.append(&mut prepend_sub_config_name(self.a.dump(), "a"));
dump.append(&mut prepend_sub_config_name(self.b.dump(), "b"));
dump
}
}
#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct CustomConfig {
param_path: String,
#[serde(default)]
seed: usize,
}
impl SerializeConfig for CustomConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
BTreeMap::from([
ser_param(
"param_path",
&self.param_path,
"This is param_path.",
ParamPrivacyInput::Public,
),
ser_generated_param(
"seed",
SerializationType::PositiveInteger,
"A dummy seed with generated default = 0.",
ParamPrivacyInput::Public,
),
])
}
}
fn load_custom_config(args: Vec<&str>) -> CustomConfig {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("config.json");
CustomConfig { param_path: "default value".to_owned(), seed: 5 }
.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap())
.unwrap();
load_and_process_config::<CustomConfig>(
File::open(file_path).unwrap(),
Command::new("Program"),
args.into_iter().map(|s| s.to_owned()).collect(),
false,
)
.unwrap()
}
#[test]
fn test_load_default_config() {
let args = vec!["Testing"];
let param_path = load_custom_config(args).param_path;
assert_eq!(param_path, "default value");
}
#[test]
fn test_load_custom_config_file() {
let args = vec!["Testing", "-f", CUSTOM_CONFIG_PATH.to_str().unwrap()];
let param_path = load_custom_config(args).param_path;
assert_eq!(param_path, "custom value");
}
#[test]
fn test_load_custom_config_file_and_args() {
let args = vec![
"Testing",
CONFIG_FILE_ARG,
CUSTOM_CONFIG_PATH.to_str().unwrap(),
"--param_path",
"command value",
];
let param_path = load_custom_config(args).param_path;
assert_eq!(param_path, "command value");
}
#[test]
fn test_load_many_custom_config_files() {
let custom_config_path = CUSTOM_CONFIG_PATH.to_str().unwrap();
let cli_config_param = format!("{custom_config_path},{custom_config_path}");
let args = vec!["Testing", "-f", cli_config_param.as_str()];
let param_path = load_custom_config(args).param_path;
assert_eq!(param_path, "custom value");
}
#[test]
fn load_config_allows_optional_fields_can_be_prefixes_of_other_fields() {
#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct ConfigWithOptionalAndPrefixField {
foo: Option<String>,
foo_non_optional: String,
}
impl SerializeConfig for ConfigWithOptionalAndPrefixField {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
let mut res = BTreeMap::from([ser_param(
"foo_non_optional",
&self.foo_non_optional,
"This is foo_non_optional.",
ParamPrivacyInput::Public,
)]);
res.extend(ser_optional_param(
&self.foo,
"foo".to_string(),
"foo",
"This is foo.",
ParamPrivacyInput::Public,
));
res
}
}
let config_file = NamedTempFile::new().expect("Failed to create test config file");
let file_path = config_file.path();
ConfigWithOptionalAndPrefixField {
foo: None,
foo_non_optional: "bar non optional".to_string(),
}
.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap())
.unwrap();
load_and_process_config::<ConfigWithOptionalAndPrefixField>(
File::open(file_path).unwrap(),
Command::new("Program"),
vec![],
false,
)
.expect("Unexpected error from loading test config.");
}
#[test]
fn test_generated_type() {
let args = vec!["Testing"];
assert_eq!(load_custom_config(args).seed, 0);
let args = vec!["Testing", "--seed", "7"];
assert_eq!(load_custom_config(args).seed, 7);
}
#[test]
fn serialization_precision() {
let input =
"{\"value\":244116128358498188146337218061232635775543270890529169229936851982759783745}";
let serialized = serde_json::from_str::<serde_json::Value>(input).unwrap();
let deserialized = serde_json::to_string(&serialized).unwrap();
assert_eq!(input, deserialized);
}
#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct RequiredConfig {
param_path: String,
num: usize,
}
impl RequiredConfig {
pub const fn param_path_description() -> &'static str {
"This is param_path."
}
pub const fn num_description() -> &'static str {
"This is num."
}
}
impl SerializeConfig for RequiredConfig {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
BTreeMap::from([
ser_required_param(
"param_path",
SerializationType::String,
Self::param_path_description(),
ParamPrivacyInput::Public,
),
ser_param("num", &self.num, Self::num_description(), ParamPrivacyInput::Public),
])
}
}
fn load_required_param_path(args: Vec<&str>) -> String {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("config.json");
RequiredConfig { param_path: "default value".to_owned(), num: 3 }
.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap())
.unwrap();
let loaded_config = load_and_process_config::<CustomConfig>(
File::open(file_path).unwrap(),
Command::new("Program"),
args.into_iter().map(|s| s.to_owned()).collect(),
false,
)
.unwrap();
loaded_config.param_path
}
#[test]
fn test_negative_required_param() {
let dumped_config = RequiredConfig { param_path: "0".to_owned(), num: 3 }.dump();
let (config_map, _) = split_values_and_types(dumped_config);
let err = load::<RequiredConfig>(&config_map).unwrap_err();
assert_matches!(err, ConfigError::MissingParam { .. });
}
#[test]
fn test_required_param_from_command() {
let args = vec!["Testing", "--param_path", "1234"];
let param_path = load_required_param_path(args);
assert_eq!(param_path, "1234");
}
#[test]
fn test_required_param_from_file() {
let args = vec!["Testing", CONFIG_FILE_ARG, CUSTOM_CONFIG_PATH.to_str().unwrap()];
let param_path = load_required_param_path(args);
assert_eq!(param_path, "custom value");
}
#[test]
fn deeply_nested_optionals() {
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)]
struct Level0 {
level0_value: u8,
level1: Option<Level1>,
}
impl SerializeConfig for Level0 {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
let mut res = BTreeMap::from([ser_param(
"level0_value",
&self.level0_value,
"This is level0_value.",
ParamPrivacyInput::Public,
)]);
res.extend(ser_optional_sub_config(&self.level1, "level1"));
res
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)]
struct Level1 {
pub level1_value: u8,
pub level2: Option<Level2>,
}
impl SerializeConfig for Level1 {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
let mut res = BTreeMap::from([ser_param(
"level1_value",
&self.level1_value,
"This is level1_value.",
ParamPrivacyInput::Public,
)]);
res.extend(ser_optional_sub_config(&self.level2, "level2"));
res
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)]
struct Level2 {
pub level2_value: Option<u8>,
}
impl SerializeConfig for Level2 {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
ser_optional_param(
&self.level2_value,
1,
"level2_value",
"This is level2_value.",
ParamPrivacyInput::Public,
)
}
}
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("config2.json");
Level0 { level0_value: 1, level1: None }
.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap())
.unwrap();
let l0 = load_and_process_config::<Level0>(
File::open(file_path.clone()).unwrap(),
Command::new("Testing"),
Vec::new(),
false,
)
.unwrap();
assert_eq!(l0, Level0 { level0_value: 1, level1: None });
let l1 = load_and_process_config::<Level0>(
File::open(file_path.clone()).unwrap(),
Command::new("Testing"),
vec!["Testing".to_owned(), "--level1.#is_none".to_owned(), "false".to_owned()],
false,
)
.unwrap();
assert_eq!(
l1,
Level0 { level0_value: 1, level1: Some(Level1 { level1_value: 0, level2: None }) }
);
let l2 = load_and_process_config::<Level0>(
File::open(file_path.clone()).unwrap(),
Command::new("Testing"),
vec![
"Testing".to_owned(),
"--level1.#is_none".to_owned(),
"false".to_owned(),
"--level1.level2.#is_none".to_owned(),
"false".to_owned(),
],
false,
)
.unwrap();
assert_eq!(
l2,
Level0 {
level0_value: 1,
level1: Some(Level1 { level1_value: 0, level2: Some(Level2 { level2_value: None }) }),
}
);
let l2_value = load_and_process_config::<Level0>(
File::open(file_path).unwrap(),
Command::new("Testing"),
vec![
"Testing".to_owned(),
"--level1.#is_none".to_owned(),
"false".to_owned(),
"--level1.level2.#is_none".to_owned(),
"false".to_owned(),
"--level1.level2.level2_value.#is_none".to_owned(),
"false".to_owned(),
],
false,
)
.unwrap();
assert_eq!(
l2_value,
Level0 {
level0_value: 1,
level1: Some(Level1 {
level1_value: 0,
level2: Some(Level2 { level2_value: Some(1) }),
}),
}
);
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)]
struct TestConfigWithNestedJson {
#[serde(deserialize_with = "deserialize_optional_list_with_url_and_headers")]
list_of_maps: Option<Vec<UrlAndHeaders>>,
}
impl SerializeConfig for TestConfigWithNestedJson {
fn dump(&self) -> BTreeMap<ParamPath, SerializedParam> {
BTreeMap::from([ser_param(
"list_of_maps",
&serialize_optional_list_with_url_and_headers(&self.list_of_maps),
"A list of nested JSON values.",
ParamPrivacyInput::Public,
)])
}
}
#[test]
fn optional_list_nested_btreemaps() {
let config = TestConfigWithNestedJson {
list_of_maps: Some(vec![
UrlAndHeaders {
url: Url::parse("http://a.com/").unwrap(),
headers: BTreeMap::from([
("key1".to_owned(), "value 1".to_owned()),
("key2".to_owned(), "value 2".to_owned()),
]),
},
UrlAndHeaders {
url: Url::parse("http://b.com/").unwrap(),
headers: BTreeMap::from([
("key3".to_owned(), "value 3".to_owned()),
("key4".to_owned(), "value 4".to_owned()),
]),
},
UrlAndHeaders {
url: Url::parse("http://c.com/").unwrap(),
headers: BTreeMap::from([]),
},
UrlAndHeaders {
url: Url::parse("http://d.com/").unwrap(),
headers: BTreeMap::from([("key5".to_owned(), "value 5".to_owned())]),
},
]),
};
let dumped = config.dump();
let (config_map, _) = split_values_and_types(dumped);
let loaded_config = load::<TestConfigWithNestedJson>(&config_map).unwrap();
assert_eq!(loaded_config.list_of_maps, config.list_of_maps);
let serialized = serde_json::to_string(&config_map).unwrap();
assert_eq!(
serialized,
r#"{"list_of_maps":"http://a.com/,key1^value 1,key2^value 2|http://b.com/,key3^value 3,key4^value 4|http://c.com/|http://d.com/,key5^value 5"}"#
);
}