diff --git a/crates/apollo_config/src/command.rs b/crates/apollo_config/src/command.rs index c468075d3a5..0d7fd200970 100644 --- a/crates/apollo_config/src/command.rs +++ b/crates/apollo_config/src/command.rs @@ -1,110 +1,21 @@ -use std::collections::BTreeMap; use std::path::PathBuf; use clap::{value_parser, Arg, ArgMatches, Command}; -use serde_json::{json, Value}; -use crate::loading::update_config_map; -use crate::{ - ConfigError, - ConfigFormat, - ParamPath, - SerializationType, - SerializedParam, - CONFIG_FILE_ARG_NAME, - CONFIG_FILE_SHORT_ARG_NAME, - CONFIG_FORMAT_ARG_NAME, -}; +use crate::{ConfigError, CONFIG_FILE_ARG_NAME, CONFIG_FILE_SHORT_ARG_NAME}; pub(crate) fn get_command_matches( - config_map: &BTreeMap, command: Command, command_input: Vec, ) -> Result { - Ok(command.args(build_args_parser(config_map)).try_get_matches_from(command_input)?) -} - -// Takes matched arguments from the command line interface and env variables and updates the config -// map. -// Supports f64, u64, i64, bool and String. -pub(crate) fn update_config_map_by_command_args( - config_map: &mut BTreeMap, - types_map: &BTreeMap, - arg_match: &ArgMatches, -) -> Result<(), ConfigError> { - for param_path_id in arg_match.ids() { - let param_path = param_path_id.as_str(); - let new_value = get_arg_by_type(types_map, arg_match, param_path)?; - update_config_map(config_map, types_map, param_path, new_value)?; - } - Ok(()) -} - -// Builds the parser for the command line flags and env variables according to the types of the -// values in the config map. -fn build_args_parser(config_map: &BTreeMap) -> Vec { - let mut args_parser = vec![ - // Custom_config_file_path. - Arg::new(CONFIG_FILE_ARG_NAME) - .long(CONFIG_FILE_ARG_NAME) - .short(CONFIG_FILE_SHORT_ARG_NAME) - .value_delimiter(',') - .help("Optionally sets a config file to use") - .value_parser(value_parser!(PathBuf)) - .num_args(1..) // Allow multiple values - .action(clap::ArgAction::Append), // Collect multiple occurrences - // How the --config_file arguments are interpreted. Absent => ConfigFormat::Preset. - Arg::new(CONFIG_FORMAT_ARG_NAME) - .long(CONFIG_FORMAT_ARG_NAME) - .num_args(1) - .value_parser(value_parser!(ConfigFormat)) - .help( - "How the --config_file arguments are interpreted: 'preset' (flat dotted-key, \ - layered) or 'native' (the first file is nested serde, later files are flat \ - secret overrides)", - ), - ]; - - for (param_path, serialized_param) in config_map.iter() { - let Some(serialization_type) = serialized_param.content.get_serialization_type() else { - continue; // Pointer target - }; - let clap_parser = match serialization_type { - SerializationType::Boolean => clap::value_parser!(bool), - SerializationType::Float => clap::value_parser!(f64).into(), - SerializationType::NegativeInteger => clap::value_parser!(i64).into(), - SerializationType::PositiveInteger => clap::value_parser!(u64).into(), - SerializationType::String => clap::value_parser!(String), - }; - - let arg = Arg::new(param_path) - .long(param_path) - .env(to_env_var_name(param_path)) - .help(&serialized_param.description) - .value_parser(clap_parser) - .allow_negative_numbers(true); - - args_parser.push(arg); - } - args_parser -} - -// Converts clap arg_matches into json values. -fn get_arg_by_type( - types_map: &BTreeMap, - arg_match: &ArgMatches, - param_path: &str, -) -> Result { - let serialization_type = types_map.get(param_path).expect("missing type"); - match serialization_type { - SerializationType::Boolean => Ok(json!(arg_match.try_get_one::(param_path)?)), - SerializationType::Float => Ok(json!(arg_match.try_get_one::(param_path)?)), - SerializationType::NegativeInteger => Ok(json!(arg_match.try_get_one::(param_path)?)), - SerializationType::PositiveInteger => Ok(json!(arg_match.try_get_one::(param_path)?)), - SerializationType::String => Ok(json!(arg_match.try_get_one::(param_path)?)), - } -} - -fn to_env_var_name(param_path: &str) -> String { - param_path.replace("#is_none", "__is_none__").to_uppercase().replace('.', "__") + // The config file flag is the only argument; the config files themselves carry every value. + let config_file_arg = Arg::new(CONFIG_FILE_ARG_NAME) + .long(CONFIG_FILE_ARG_NAME) + .short(CONFIG_FILE_SHORT_ARG_NAME) + .value_delimiter(',') + .help("Sets the config files to use") + .value_parser(value_parser!(PathBuf)) + .num_args(1..) // Allow multiple values + .action(clap::ArgAction::Append); // Collect multiple occurrences + Ok(command.args([config_file_arg]).try_get_matches_from(command_input)?) } diff --git a/crates/apollo_config/src/config_test.rs b/crates/apollo_config/src/config_test.rs index a730620eed9..6ba4e2ee7bd 100644 --- a/crates/apollo_config/src/config_test.rs +++ b/crates/apollo_config/src/config_test.rs @@ -1,21 +1,15 @@ 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 tempfile::NamedTempFile; 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, @@ -27,7 +21,6 @@ use crate::dumping::{ generate_struct_pointer, prepend_sub_config_name, required_param_description, - ser_generated_param, ser_optional_param, ser_optional_sub_config, ser_param, @@ -37,14 +30,7 @@ use crate::dumping::{ 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::loading::{load, load_and_process_config}; use crate::presentation::get_config_presentation; use crate::{ ConfigError, @@ -57,12 +43,6 @@ use crate::{ 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))] @@ -100,24 +80,6 @@ impl SerializeConfig for OuterConfig { } } -#[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::(&dumped).unwrap(); - assert_eq!(loaded_config, outer_config); - } -} - #[test] fn test_validation() { let outer_config = @@ -154,67 +116,6 @@ impl SerializeConfig for TypicalConfig { } } -#[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 = 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 = 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 { @@ -260,93 +161,6 @@ fn test_nested_config_presentation() { } } -#[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 the pointing parameters are correctly set. - 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 the whitelisted parameter is correctly set. - 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 the pointed parameter is correctly set as a required parameter. - 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() { // Set up the config map and pointers. @@ -461,24 +275,6 @@ fn test_missing_pointer_flow() { 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"; @@ -568,85 +364,6 @@ impl SerializeConfig for StructPointersConfig { } } -#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)] -struct CustomConfig { - param_path: String, - #[serde(default)] - seed: usize, -} - -impl SerializeConfig for CustomConfig { - fn dump(&self) -> BTreeMap { - 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, - ), - ]) - } -} - -// Loads CustomConfig from args. -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::( - 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"); -} - #[derive(Deserialize, Debug, PartialEq)] struct NativeInnerConfig { a: usize, @@ -686,24 +403,13 @@ struct NativeOptionalConfig { top_field: String, } -// Loads a config in `--config_format native` mode. The schema file is unused by the native path but -// still parsed, so an empty object is supplied as a valid (empty) schema. +// Loads a config via the native loader: the first `--config_file` is the nested base, each +// subsequent one a flat dotted-key secret override. fn load_native_config Deserialize<'a>>( config_file_args: Vec<&str>, ) -> Result { - let schema_file = NamedTempFile::new().unwrap(); - std::fs::write(schema_file.path(), json!({}).to_string()).unwrap(); - - let args = chain!(["Testing", "--config_format", "native"], config_file_args) - .map(|arg| arg.to_owned()) - .collect(); - - load_and_process_config::( - File::open(schema_file.path()).unwrap(), - Command::new("Program"), - args, - false, - ) + let args = chain!(["Testing"], config_file_args).map(|arg| arg.to_owned()).collect(); + load_and_process_config::(Command::new("Program"), args, false) } #[test] @@ -758,6 +464,20 @@ fn test_native_config_with_empty_secrets() { ); } +#[test] +fn test_native_config_requires_two_config_files() { + let base_file = NamedTempFile::new().unwrap(); + let base_config = json!({"top": "base_top", "inner": {"a": 1, "b": "base_b"}}); + std::fs::write(base_file.path(), base_config.to_string()).unwrap(); + + // A single config file is rejected: the native loader requires a base and a secrets file. + let result = load_native_config::(vec![ + CONFIG_FILE_ARG, + base_file.path().to_str().unwrap(), + ]); + assert_matches!(result, Err(ConfigError::NativeModeRequiresTwoConfigFiles)); +} + #[test] fn test_native_config_without_config_file() { let result = load_native_config::(vec![]); @@ -864,62 +584,6 @@ fn test_native_config_creates_absent_leaf_in_existing_parent() { ); } -// Make sure that if we have a field `foo_bar` and an optional field called `foo` with a value of -// None, we don't remove the foo_bar field from the config. -// This test was added following bug #37984 (see bug for more details). -#[test] -fn load_config_allows_optional_fields_can_be_prefixes_of_other_fields() { - #[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)] - struct ConfigWithOptionalAndPrefixField { - foo: Option, - foo_non_optional: String, - } - impl SerializeConfig for ConfigWithOptionalAndPrefixField { - fn dump(&self) -> BTreeMap { - 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::( - 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 = @@ -958,178 +622,6 @@ impl SerializeConfig for RequiredConfig { } } -// Loads param_path of RequiredConfig from args. -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::( - 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::(&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, - } - - impl SerializeConfig for Level0 { - fn dump(&self) -> BTreeMap { - 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, - } - - impl SerializeConfig for Level1 { - fn dump(&self) -> BTreeMap { - 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, - } - - impl SerializeConfig for Level2 { - fn dump(&self) -> BTreeMap { - 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::( - 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::( - 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::( - 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::( - 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")] @@ -1175,14 +667,20 @@ fn optional_list_nested_btreemaps() { }, ]), }; - let dumped = config.dump(); - let (config_map, _) = split_values_and_types(dumped); + // Build the flat values map directly from the dump (a single serialized leaf), then load it. + let config_map: BTreeMap = config + .dump() + .into_iter() + .filter_map(|(param_path, serialized_param)| match serialized_param.content { + SerializedContent::DefaultValue(value) => Some((param_path, value)), + _ => None, + }) + .collect(); let loaded_config = load::(&config_map).unwrap(); - // println!("{:#?}", loaded_config); 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"}"# /* r#"{"list_of_maps":[{"url":"http://a.com/","headers":{"inner1":"1","inner2":"2"}},{"url":"http://b.com/","headers":{"inner3":"3","inner4":"4"}},{"url":"http://c.com/","headers":{}},{"url":"http://d.com/","headers":{"inner5":"5"}}]}"# */ + 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"}"# ); } diff --git a/crates/apollo_config/src/lib.rs b/crates/apollo_config/src/lib.rs index 8baaa21b8b8..147f90bc96b 100644 --- a/crates/apollo_config/src/lib.rs +++ b/crates/apollo_config/src/lib.rs @@ -7,15 +7,13 @@ //! # Example //! //! ``` -//! use std::collections::{BTreeMap, HashSet}; -//! use std::fs::File; -//! use std::path::Path; +//! use std::fs::write; //! -//! use apollo_config::dumping::{ser_param, SerializeConfig}; //! use apollo_config::loading::load_and_process_config; -//! use apollo_config::{ParamPath, ParamPrivacyInput, SerializedParam}; +//! use apollo_config::CONFIG_FILE_ARG; //! use clap::Command; //! use serde::{Deserialize, Serialize}; +//! use serde_json::json; //! use tempfile::TempDir; //! //! #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] @@ -23,25 +21,22 @@ //! key: usize, //! } //! -//! impl SerializeConfig for ConfigExample { -//! fn dump(&self) -> BTreeMap { -//! BTreeMap::from([ser_param( -//! "key", -//! &self.key, -//! "This is key description.", -//! ParamPrivacyInput::Public, -//! )]) -//! } -//! } -//! +//! // The native loader takes a nested base config and a flat dotted-key secret-overrides file. //! let dir = TempDir::new().unwrap(); -//! let file_path = dir.path().join("config.json"); -//! ConfigExample { key: 42 }.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap()); -//! let file = File::open(file_path).unwrap(); +//! let base_path = dir.path().join("config.json"); +//! write(&base_path, json!({ "key": 42 }).to_string()).unwrap(); +//! let secret_path = dir.path().join("secrets.json"); +//! write(&secret_path, json!({ "key": 770 }).to_string()).unwrap(); +//! //! let loaded_config = load_and_process_config::( -//! file, //! Command::new("Program"), -//! vec!["Program".to_owned(), "--key".to_owned(), "770".to_owned()], +//! vec![ +//! "Program".to_owned(), +//! CONFIG_FILE_ARG.to_owned(), +//! base_path.to_str().unwrap().to_owned(), +//! CONFIG_FILE_ARG.to_owned(), +//! secret_path.to_str().unwrap().to_owned(), +//! ], //! false, //! ) //! .unwrap(); @@ -64,21 +59,6 @@ pub const CONFIG_FILE_SHORT_ARG_NAME: char = 'f'; /// The config file arg name prepended with a double dash. pub const CONFIG_FILE_ARG: &str = formatcp!("--{}", CONFIG_FILE_ARG_NAME); -/// Arg name for selecting how the config files are interpreted. -pub const CONFIG_FORMAT_ARG_NAME: &str = "config_format"; - -/// Selects how the `--config_file` arguments are interpreted when loading a config. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)] -pub enum ConfigFormat { - /// Flat dotted-key files, layered through the full pipeline (pointers, env/CLI overrides, - /// `#is_none` marks). This is the historical behavior that will be deprecated. - #[default] - Preset, - /// The first file is a nested JSON object deserialized directly with serde; any subsequent - /// files are flat dotted-key secret overrides applied onto it. - Native, -} - /// A config indicator for optional parameters. pub const IS_NONE_MARK: &str = "#is_none"; /// A config indicator for a sub config. diff --git a/crates/apollo_config/src/loading.rs b/crates/apollo_config/src/loading.rs index 0cda6644ec2..4e64841f4b6 100644 --- a/crates/apollo_config/src/loading.rs +++ b/crates/apollo_config/src/loading.rs @@ -1,35 +1,19 @@ -//! Loads a configuration object, and set values for the fields in the following order of priority: -//! * Command line arguments. -//! * Environment variables (capital letters). -//! * Custom config files, separated by ',' (comma), from last to first. +//! Loads a configuration object from native config files: a nested base config followed by flat +//! dotted-key secret overrides. -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; use std::fs::File; use std::ops::IndexMut; use std::path::PathBuf; -use clap::parser::Values; use clap::Command; -use command::{get_command_matches, update_config_map_by_command_args}; -use itertools::any; +use command::get_command_matches; use serde::Deserialize; use serde_json::{json, Map, Value}; -use tracing::{debug, error, info, instrument}; +use tracing::{debug, info, instrument}; use crate::validators::validate_path_exists; -use crate::{ - command, - ConfigError, - ConfigFormat, - ParamPath, - SerializationType, - SerializedContent, - SerializedParam, - CONFIG_FILE_ARG_NAME, - CONFIG_FORMAT_ARG_NAME, - FIELD_SEPARATOR, - IS_NONE_MARK, -}; +use crate::{command, ConfigError, ParamPath, CONFIG_FILE_ARG_NAME, FIELD_SEPARATOR}; /// Deserializes config from flatten JSON. /// For an explanation of `for<'a> Deserialize<'a>` see @@ -49,7 +33,7 @@ pub fn load Deserialize<'a>>( Ok(serde_json::from_value(nested_map)?) } -/// Deserializes the config directly from nested JSON files (the `ConfigFormat::Native` path). +/// Deserializes the config directly from nested JSON files. /// /// Requires exactly two paths. The first is the base config: a nested JSON object matching the /// target struct's field hierarchy. The second is a flat dotted-key secret file whose entries are @@ -105,286 +89,22 @@ fn set_nested_value(nested_map: &mut Value, param_path: &str, value: Value) { } } -/// Deserializes a json config file, updates the values by the given arguments for the command, and -/// set values for the pointers. +/// Loads a config from native config files. +/// +/// Reads the `--config_file` arguments and deserializes the config directly from them via +/// [`load_native`], which requires exactly two: the first is the nested base config and the second +/// is a flat dotted-key secret override. `ignore_default_values` is retained for call-site +/// compatibility but has no effect on the native path (the base file is already a complete, +/// resolved config). pub fn load_and_process_config Deserialize<'a>>( - config_schema_file: File, command: Command, args: Vec, - ignore_default_values: bool, + _ignore_default_values: bool, ) -> Result { - let deserialized_config_schema: Map = - serde_json::from_reader(&config_schema_file)?; - // Store the pointers separately from the default values. The pointers will receive a value - // only at the end of the process. - let (config_map, pointers_map) = split_pointers_map(deserialized_config_schema.clone()); - // Take param paths with corresponding descriptions, and get the matching arguments. - let mut arg_matches = get_command_matches(&config_map, command, args)?; - - let config_format = - arg_matches.remove_one::(CONFIG_FORMAT_ARG_NAME).unwrap_or_default(); - if config_format == ConfigFormat::Native { - let custom_config_paths: Vec = arg_matches - .remove_many::(CONFIG_FILE_ARG_NAME) - .map(|paths| paths.collect()) - .unwrap_or_default(); - return load_native(custom_config_paths); - } - // Retaining values from the default config map for backward compatibility. - let (mut values_map, types_map) = split_values_and_types(config_map); - if ignore_default_values { - info!("Ignoring default values by overriding with an empty map."); - values_map = BTreeMap::new(); - } - // If the config_file arg is given, updates the values map according to this files. - if let Some(custom_config_paths) = arg_matches.remove_many::(CONFIG_FILE_ARG_NAME) { - update_config_map_by_custom_configs(&mut values_map, &types_map, custom_config_paths)?; - }; - // Updates the values map according to the args. - update_config_map_by_command_args(&mut values_map, &types_map, &arg_matches)?; - // Set values to the pointers. - update_config_map_by_pointers(&mut values_map, &pointers_map)?; - // Set values according to the is-none marks. - update_optional_values(&mut values_map); - // Build the Config object. - let load_result = load(&values_map); - // In case of an error, print the error and the missing keys. - if load_result.is_err() { - error!("Loading the config resulted with an error: {:?}", load_result.as_ref().err()); - // Obtain the loaded and schema keys. - let loaded_config_keys = values_map.keys().cloned().collect::>(); - let schema_keys = deserialized_config_schema.keys().cloned().collect::>(); - - // Obtain the loaded and schema keys that are not in the other. - let mut keys_only_in_loaded_config: HashSet<_> = - loaded_config_keys.difference(&schema_keys).collect(); - let mut keys_only_in_schema: HashSet<_> = - schema_keys.difference(&loaded_config_keys).collect(); - - // Address optional None value discrepancies: - // 1. Find None (null) values in the config entries. - let null_config_entries = values_map - .iter() - .filter(|(_, value)| value.is_null()) - .map(|(key, _)| key.clone()) - .collect::>(); - - // 2. Filter out None-value keys in the loaded config entries. - keys_only_in_loaded_config.retain(|item| !null_config_entries.contains(item.as_str())); - - // 3. Filter out None-value keys in the schema entries. - let optional_param_suffix = format!("{FIELD_SEPARATOR}{IS_NONE_MARK}"); - keys_only_in_schema.retain(|item| { - // Consider a schema key only if both: - // - it does NOT start with any prefix in `null_config_entries` (i.e., a None value) - // - it does NOT end with the optional param suffix (i.e., a None value indicator) - let has_prefix = null_config_entries.iter().any(|prefix| item.starts_with(prefix)); - let has_suffix = item.ends_with(optional_param_suffix.as_str()); - !has_prefix && !has_suffix - }); - - // Log the keys that are only in the loaded config. - if !keys_only_in_loaded_config.is_empty() { - error!( - "Detected loaded-schema config difference. - keys missing in schema: {:?}", - keys_only_in_loaded_config - ); - } - - // Log the keys that are only in the schema. - if !keys_only_in_schema.is_empty() { - error!( - "Detected loaded-schema config difference. - Keys missing in loaded config: {:?}", - keys_only_in_schema - ); - } - } - // Return the loaded config result. - load_result -} - -// Separates a json map into config map of the raw values and pointers map. -pub(crate) fn split_pointers_map( - json_map: Map, -) -> (BTreeMap, BTreeMap) { - let mut config_map: BTreeMap = BTreeMap::new(); - let mut pointers_map: BTreeMap = BTreeMap::new(); - for (param_path, stored_param) in json_map { - let Ok(ser_param) = serde_json::from_value::(stored_param.clone()) else { - unreachable!("Invalid type in the json config map") - }; - match ser_param.content { - SerializedContent::PointerTarget(pointer_target) => { - pointers_map.insert(param_path, pointer_target); - } - _ => { - config_map.insert(param_path, ser_param); - } - }; - } - (config_map, pointers_map) -} - -// Removes the description from the config map, and splits the config map into default values and -// types of the default and required values. -// The types map includes required params, that do not have a value yet. -pub(crate) fn split_values_and_types( - config_map: BTreeMap, -) -> (BTreeMap, BTreeMap) { - let mut values_map: BTreeMap = BTreeMap::new(); - let mut types_map: BTreeMap = BTreeMap::new(); - for (param_path, serialized_param) in config_map { - let Some(serialization_type) = serialized_param.content.get_serialization_type() else { - continue; - }; - types_map.insert(param_path.clone(), serialization_type); - - if let SerializedContent::DefaultValue(value) = serialized_param.content { - values_map.insert(param_path, value); - }; - } - (values_map, types_map) -} - -// Updates the config map by param path to value custom json files. -pub(crate) fn update_config_map_by_custom_configs( - config_map: &mut BTreeMap, - types_map: &BTreeMap, - custom_config_paths: Values, -) -> Result<(), ConfigError> { - for config_path in custom_config_paths { - info!("Loading custom config file: {:?}", config_path); - validate_path_exists(&config_path)?; - let file = std::fs::File::open(config_path)?; - let custom_config: Map = serde_json::from_reader(file)?; - for (param_path, json_value) in custom_config { - update_config_map(config_map, types_map, param_path.as_str(), json_value)?; - } - } - Ok(()) -} - -pub(crate) fn update_config_map_by_pointers( - config_map: &mut BTreeMap, - pointers_map: &BTreeMap, -) -> Result<(), ConfigError> { - let optional_param_suffix = format!("{FIELD_SEPARATOR}{IS_NONE_MARK}"); - - // Phase 1: Resolve only the optional flags and compute none entries (prefixes marked as None). - let mut none_entries = Vec::new(); - for (param_path, target_param_path) in pointers_map { - if let Some(prefix) = param_path.strip_suffix(optional_param_suffix.as_str()) { - let target_value = match config_map.get(target_param_path) { - Some(v) => v.clone(), - None => { - return Err(ConfigError::PointerTargetNotFound { - target_param: target_param_path.to_owned(), - }); - } - }; - // Record the value - config_map.insert(param_path.to_owned(), target_value.clone()); - // Record none entries where the flag is true - if target_value == json!(true) { - none_entries.push(prefix.to_owned()); - } - } - } - - // Phase 2: Resolve pointers for non-optional-flag params, skipping any pointer under - // optional-flag entries. - for (param_path, target_param_path) in pointers_map { - if param_path.ends_with(optional_param_suffix.as_str()) { - continue; - } - - // Check if param_path is under any none entry. - let is_under_none_entry = none_entries - .iter() - .any(|prefix| param_path.starts_with(format!("{prefix}{FIELD_SEPARATOR}").as_str())); - if is_under_none_entry { - continue; - } - - let target_value = match config_map.get(target_param_path) { - Some(v) => v.clone(), - None => { - return Err(ConfigError::PointerTargetNotFound { - target_param: target_param_path.to_owned(), - }); - } - }; - config_map.insert(param_path.to_owned(), target_value); - } - - Ok(()) -} - -// Removes the none marks, and sets null for the params marked as None instead of the inner params. -pub(crate) fn update_optional_values(config_map: &mut BTreeMap) { - let optional_params: Vec<_> = config_map - .keys() - .filter_map(|param_path| param_path.strip_suffix(&format!(".{IS_NONE_MARK}"))) - .map(|param_path| param_path.to_owned()) - .collect(); - let mut none_params = vec![]; - for optional_param in optional_params { - let value = config_map - .remove(&format!("{optional_param}.{IS_NONE_MARK}")) - .expect("Not found optional param"); - if value == json!(true) { - none_params.push(optional_param); - } - } - // Remove param paths that start with any None param. - config_map.retain(|param_path, _| { - !any(&none_params, |none_param| { - param_path.starts_with(format!("{none_param}{FIELD_SEPARATOR}").as_str()) - || param_path == none_param - }) - }); - - // Set null for the None params. - for none_param in &none_params { - let mut is_nested_in_outer_none_config = false; - for other_none_param in &none_params { - if none_param.starts_with(other_none_param) && none_param != other_none_param { - is_nested_in_outer_none_config = true; - } - } - if is_nested_in_outer_none_config { - continue; - } - config_map.insert(none_param.clone(), Value::Null); - } -} - -pub(crate) fn update_config_map( - config_map: &mut BTreeMap, - types_map: &BTreeMap, - param_path: &str, - new_value: Value, -) -> Result<(), ConfigError> { - let Some(serialization_type) = types_map.get(param_path) else { - return Err(ConfigError::UnexpectedParam { param_path: param_path.to_string() }); - }; - let is_type_matched = match serialization_type { - SerializationType::Boolean => new_value.is_boolean(), - SerializationType::Float => new_value.is_number(), - SerializationType::NegativeInteger => new_value.is_number(), - SerializationType::PositiveInteger => new_value.is_number(), - SerializationType::String => new_value.is_string(), - }; - if !is_type_matched { - return Err(ConfigError::ChangeRequiredParamType { - param_path: param_path.to_string(), - required: serialization_type.to_owned(), - given: new_value, - }); - } - - config_map.insert(param_path.to_owned(), new_value); - Ok(()) + let mut arg_matches = get_command_matches(command, args)?; + let custom_config_paths: Vec = arg_matches + .remove_many::(CONFIG_FILE_ARG_NAME) + .map(|paths| paths.collect()) + .unwrap_or_default(); + load_native(custom_config_paths) } diff --git a/crates/apollo_gateway/src/gateway_test.rs b/crates/apollo_gateway/src/gateway_test.rs index bce6a821d61..9560d0f0d5a 100644 --- a/crates/apollo_gateway/src/gateway_test.rs +++ b/crates/apollo_gateway/src/gateway_test.rs @@ -1,9 +1,7 @@ -use std::collections::HashSet; -use std::fs::File; use std::sync::{Arc, LazyLock}; -use apollo_config::dumping::SerializeConfig; use apollo_config::loading::load_and_process_config; +use apollo_config::CONFIG_FILE_ARG; use apollo_gateway_config::config::{ GatewayConfig, GatewayStaticConfig, @@ -836,20 +834,34 @@ fn test_full_cycle_dump_deserialize_authorized_declarer_accounts( #[case] authorized_declarer_accounts: Option>, ) { let original_config = GatewayConfig { - static_config: GatewayStaticConfig { authorized_declarer_accounts, ..Default::default() }, + static_config: GatewayStaticConfig { + authorized_declarer_accounts, + ..Default::default() + }, ..Default::default() }; - // Create a temporary file to dump the config. - let file_path = TempDir::new().unwrap().path().join("config.json"); - original_config.dump_to_file(&vec![], &HashSet::new(), file_path.to_str().unwrap()).unwrap(); + // The native loader takes a nested base config and a flat secret-overrides file. Serialize the + // config to the nested base file; serde is symmetric for `authorized_declarer_accounts` (its + // `serialize_with` emits the same comma-separated string its `deserialize_with` reads back). + let base_value = serde_json::to_value(&original_config).unwrap(); + + let dir = TempDir::new().unwrap(); + let base_path = dir.path().join("config.json"); + let secret_path = dir.path().join("secrets.json"); + std::fs::write(&base_path, base_value.to_string()).unwrap(); + std::fs::write(&secret_path, "{}").unwrap(); - // Load the config from the dumped config file. let loaded_config = load_and_process_config::( - File::open(file_path).unwrap(), // Config file to load. - Command::new(""), // Unused CLI context. - vec![], // No override CLI args. - false, // Use schema defaults. + Command::new(""), // Unused CLI context. + vec![ + String::new(), // Program name placeholder. + CONFIG_FILE_ARG.to_owned(), + base_path.to_str().unwrap().to_owned(), + CONFIG_FILE_ARG.to_owned(), + secret_path.to_str().unwrap().to_owned(), + ], + false, // Unused by the native loader. ) .unwrap(); diff --git a/crates/apollo_node/src/test_utils/node_runner.rs b/crates/apollo_node/src/test_utils/node_runner.rs index 5abdc5e54c4..cb8859f6ef2 100644 --- a/crates/apollo_node/src/test_utils/node_runner.rs +++ b/crates/apollo_node/src/test_utils/node_runner.rs @@ -91,14 +91,14 @@ async fn spawn_node_child_process( info!("Getting the node executable."); let node_executable = get_node_executable_path(); - // Interpret the config files with the native loader: the paths are a nested base config - // followed by a flat secrets file, matching `load_native`'s `[base, secret]` arity. - let config_file_args: Vec = ["--config_format".to_string(), "native".to_string()] + // The node loads its config natively: the paths are a nested base config followed by a flat + // secrets file, matching `load_native`'s `[base, secret]` arity. + let config_file_args: Vec = node_config_paths .into_iter() - .chain(node_config_paths.into_iter().flat_map(|path| { + .flat_map(|path| { let path_str = path.to_str().expect("Invalid path").to_string(); - vec![CONFIG_FILE_ARG.to_string(), path_str.clone()] - })) + vec![CONFIG_FILE_ARG.to_string(), path_str] + }) .collect(); info!("Running the node from: {}", node_executable); diff --git a/crates/apollo_node_config/src/config_utils.rs b/crates/apollo_node_config/src/config_utils.rs index 64fc1e93f26..ff335704d77 100644 --- a/crates/apollo_node_config/src/config_utils.rs +++ b/crates/apollo_node_config/src/config_utils.rs @@ -197,8 +197,8 @@ impl DeploymentBaseAppConfig { } /// Returns the nested config as JSON, matching the `SequencerNodeConfig` field hierarchy. - /// This is the artifact consumed by the `ConfigFormat::Native` loader (as the base config), - /// in contrast to the flat preset produced by `as_value`. + /// This is the artifact consumed by the native config loader (as the base config), in contrast + /// to the flat preset produced by `as_value`. pub fn as_native_value(&self) -> Value { serde_json::to_value(&self.config).expect("Should be able to serialize config to value") } diff --git a/crates/apollo_node_config/src/node_config.rs b/crates/apollo_node_config/src/node_config.rs index 124a1d9f062..447a780baa0 100644 --- a/crates/apollo_node_config/src/node_config.rs +++ b/crates/apollo_node_config/src/node_config.rs @@ -1,5 +1,4 @@ use std::collections::{BTreeMap, HashSet}; -use std::fs::File; use std::sync::LazyLock; use std::vec::Vec; @@ -28,7 +27,6 @@ use apollo_consensus_manager_config::config::ConsensusManagerConfig; use apollo_consensus_orchestrator_config::config::ContextDynamicConfig; use apollo_gateway_config::config::{GatewayConfig, GatewayDynamicConfig}; use apollo_http_server_config::config::{HttpServerConfig, HttpServerDynamicConfig}; -use apollo_infra_utils::path::resolve_project_relative_path; use apollo_l1_events_config::config::{L1EventsProviderConfig, L1EventsScraperConfig}; use apollo_l1_gas_price_config::config::{L1GasPriceProviderConfig, L1GasPriceScraperConfig}; use apollo_mempool_config::config::{MempoolConfig, MempoolDynamicConfig}; @@ -475,11 +473,9 @@ fn validate_node_dynamic_config(config: &NodeDynamicConfig) -> Result<(), Valida } impl SequencerNodeConfig { - /// Creates a config object, using the config schema and provided resources. + /// Creates a config object from the native config files named by `args`. pub fn load_and_process(args: Vec) -> Result { - let config_file_name = &resolve_project_relative_path(CONFIG_SCHEMA_PATH)?; - let default_config_file = File::open(config_file_name)?; - load_and_process_config(default_config_file, node_command(), args, true) + load_and_process_config(node_command(), args, true) } pub fn validate_node_config(&self) -> Result<(), ConfigError> {