use crate::error::{ConfigError, ConfigResult};
use crate::layer::{ConfigLayer, LayerPriority};
use crate::value::ConfigValue;
use clap::ArgMatches;
use std::collections::HashMap;
pub struct FlagConfigLayer {
matches: ArgMatches,
cached_values: HashMap<String, ConfigValue>,
flag_mappings: HashMap<String, String>,
}
impl FlagConfigLayer {
pub fn new(matches: ArgMatches) -> Self {
let mut layer = Self {
matches,
cached_values: HashMap::new(),
flag_mappings: HashMap::new(),
};
layer.cache_flag_values();
layer
}
pub fn with_mappings(matches: ArgMatches, mappings: HashMap<String, String>) -> Self {
let mut layer = Self {
matches,
cached_values: HashMap::new(),
flag_mappings: mappings,
};
layer.cache_flag_values();
layer
}
pub fn add_flag_mapping(
&mut self,
flag_name: impl Into<String>,
config_key: impl Into<String>,
) {
self.flag_mappings
.insert(flag_name.into(), config_key.into());
self.cache_flag_values();
}
pub fn remove_flag_mapping(&mut self, flag_name: &str) -> Option<String> {
let result = self.flag_mappings.remove(flag_name);
self.cache_flag_values();
result
}
pub fn flag_mappings(&self) -> &HashMap<String, String> {
&self.flag_mappings
}
fn cache_flag_values(&mut self) {
self.cached_values.clear();
for arg_id in self.matches.ids() {
let arg_name = arg_id.as_str();
let config_key = self
.flag_mappings
.get(arg_name)
.cloned()
.unwrap_or_else(|| self.normalize_flag_name(arg_name));
if let Some(config_value) = self.convert_arg_to_config_value(arg_name) {
self.cached_values.insert(config_key, config_value);
}
}
}
fn normalize_flag_name(&self, flag_name: &str) -> String {
flag_name.replace('-', ".").replace('_', ".").to_lowercase()
}
fn convert_arg_to_config_value(&self, arg_name: &str) -> Option<ConfigValue> {
if !self.matches.contains_id(arg_name) {
return None;
}
if let Ok(Some(&flag_val)) = self.matches.try_get_one::<bool>(arg_name) {
return Some(ConfigValue::Boolean(flag_val));
}
if let Ok(Some(values)) = self.matches.try_get_many::<String>(arg_name) {
let config_values: Vec<ConfigValue> =
values.map(|v| self.parse_string_value(v)).collect();
if config_values.len() == 1 {
return Some(config_values.into_iter().next().unwrap());
} else {
return Some(ConfigValue::Array(config_values));
}
}
if let Ok(Some(string_val)) = self.matches.try_get_one::<String>(arg_name) {
return Some(self.parse_string_value(string_val));
}
let count = self.matches.get_count(arg_name);
if count > 0 {
return Some(ConfigValue::Integer(count as i64));
}
if self.matches.get_flag(arg_name) {
return Some(ConfigValue::Boolean(true));
}
None
}
fn parse_string_value(&self, value: &str) -> ConfigValue {
match value.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => return ConfigValue::Boolean(true),
"false" | "0" | "no" | "off" => return ConfigValue::Boolean(false),
_ => {}
}
if let Ok(int_val) = value.parse::<i64>() {
return ConfigValue::Integer(int_val);
}
if let Ok(float_val) = value.parse::<f64>() {
return ConfigValue::Float(float_val);
}
ConfigValue::String(value.to_string())
}
pub fn matches(&self) -> &ArgMatches {
&self.matches
}
pub fn has_flag(&self, flag_name: &str) -> bool {
self.matches.contains_id(flag_name)
}
pub fn flag_count(&self, flag_name: &str) -> u8 {
self.matches.get_count(flag_name)
}
}
impl ConfigLayer for FlagConfigLayer {
fn get(&self, key: &str) -> ConfigResult<Option<ConfigValue>> {
Ok(self.cached_values.get(key).cloned())
}
fn set(&mut self, _key: &str, _value: ConfigValue) -> ConfigResult<()> {
Err(ConfigError::unsupported_operation(
"Cannot set values in flag configuration layer - flags are read-only",
))
}
fn keys(&self) -> Vec<String> {
self.cached_values.keys().cloned().collect()
}
fn source_name(&self) -> &str {
"command line flags"
}
fn priority(&self) -> LayerPriority {
LayerPriority::Flags
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::{Arg, Command};
fn create_test_app() -> Command {
Command::new("test")
.disable_help_flag(true) .arg(
Arg::new("host")
.long("host")
.short('h')
.value_name("HOST")
.action(clap::ArgAction::Set) .help("Database host"),
)
.arg(
Arg::new("port")
.long("port")
.short('p')
.value_name("PORT")
.action(clap::ArgAction::Set) .help("Database port"),
)
.arg(
Arg::new("verbose")
.long("verbose")
.short('v')
.action(clap::ArgAction::SetTrue)
.help("Enable verbose output"),
)
.arg(
Arg::new("include")
.long("include")
.short('i')
.value_name("PATH")
.action(clap::ArgAction::Append)
.help("Include paths"),
)
}
#[test]
fn test_flag_layer_creation() {
let app = create_test_app();
let args = vec!["test", "--host", "localhost", "--port", "5432", "--verbose"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
assert_eq!(flag_layer.source_name(), "command line flags");
assert_eq!(flag_layer.priority(), LayerPriority::Flags);
}
#[test]
fn test_string_flag_parsing() {
let app = create_test_app();
let args = vec!["test", "--host", "localhost", "--port", "5432"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
let host_value = flag_layer.get("host").unwrap();
assert_eq!(
host_value,
Some(ConfigValue::String("localhost".to_string()))
);
let port_value = flag_layer.get("port").unwrap();
assert_eq!(port_value, Some(ConfigValue::Integer(5432)));
}
#[test]
fn test_boolean_flag_parsing() {
let app = create_test_app();
let args = vec!["test", "--verbose"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
let verbose_value = flag_layer.get("verbose").unwrap();
assert_eq!(verbose_value, Some(ConfigValue::Boolean(true)));
let nonexistent_value = flag_layer.get("nonexistent").unwrap();
assert_eq!(nonexistent_value, None);
}
#[test]
fn test_count_flag_parsing() {
let app = Command::new("test").disable_help_flag(true).arg(
Arg::new("debug")
.long("debug")
.short('d')
.action(clap::ArgAction::Count)
.help("Debug level"),
);
let args = vec!["test", "-ddd"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
assert_eq!(flag_layer.flag_count("debug"), 3);
let debug_value = flag_layer.get("debug").unwrap();
assert_eq!(debug_value, Some(ConfigValue::Integer(3)));
}
#[test]
fn test_multi_value_flag_parsing() {
let app = create_test_app();
let args = vec!["test", "-i", "path1", "-i", "path2", "-i", "path3"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
let include_value = flag_layer.get("include").unwrap();
match include_value {
Some(ConfigValue::Array(arr)) => {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], ConfigValue::String("path1".to_string()));
assert_eq!(arr[1], ConfigValue::String("path2".to_string()));
assert_eq!(arr[2], ConfigValue::String("path3".to_string()));
}
_ => panic!("Expected array value, got: {:?}", include_value),
}
}
#[test]
fn test_flag_mappings() {
let app = create_test_app();
let args = vec!["test", "--host", "localhost"];
let matches = app.try_get_matches_from(args).unwrap();
let mut mappings = HashMap::new();
mappings.insert("host".to_string(), "database.host".to_string());
let flag_layer = FlagConfigLayer::with_mappings(matches, mappings);
let host_value = flag_layer.get("database.host").unwrap();
assert_eq!(
host_value,
Some(ConfigValue::String("localhost".to_string()))
);
let original_value = flag_layer.get("host").unwrap();
assert_eq!(original_value, None);
}
#[test]
fn test_flag_name_normalization() {
let app = Command::new("test").disable_help_flag(true).arg(
Arg::new("db_host")
.long("db-host")
.value_name("HOST")
.action(clap::ArgAction::Set),
);
let args = vec!["test", "--db-host", "localhost"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
println!("Available keys: {:?}", flag_layer.keys());
let host_value = flag_layer.get("db.host").unwrap();
assert_eq!(
host_value,
Some(ConfigValue::String("localhost".to_string()))
);
}
#[test]
fn test_value_type_parsing() {
let app = Command::new("test")
.disable_help_flag(true)
.arg(
Arg::new("string_val")
.long("string")
.value_name("VAL")
.action(clap::ArgAction::Set),
)
.arg(
Arg::new("int_val")
.long("int")
.value_name("VAL")
.action(clap::ArgAction::Set),
)
.arg(
Arg::new("float_val")
.long("float")
.value_name("VAL")
.action(clap::ArgAction::Set),
)
.arg(
Arg::new("bool_val")
.long("bool")
.value_name("VAL")
.action(clap::ArgAction::Set),
);
let args = vec![
"test", "--string", "hello", "--int", "42", "--float", "3.14", "--bool", "true",
];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
assert_eq!(
flag_layer.get("string.val").unwrap(),
Some(ConfigValue::String("hello".to_string()))
);
assert_eq!(
flag_layer.get("int.val").unwrap(),
Some(ConfigValue::Integer(42))
);
assert_eq!(
flag_layer.get("float.val").unwrap(),
Some(ConfigValue::Float(3.14))
);
assert_eq!(
flag_layer.get("bool.val").unwrap(),
Some(ConfigValue::Boolean(true))
);
}
#[test]
fn test_read_only_layer() {
let app = create_test_app();
let args = vec!["test", "--host", "localhost"];
let matches = app.try_get_matches_from(args).unwrap();
let mut flag_layer = FlagConfigLayer::new(matches);
let result = flag_layer.set("new_key", ConfigValue::String("value".to_string()));
assert!(result.is_err());
}
#[test]
fn test_keys_method() {
let app = create_test_app();
let args = vec!["test", "--host", "localhost", "--verbose"];
let matches = app.try_get_matches_from(args).unwrap();
let flag_layer = FlagConfigLayer::new(matches);
let keys = flag_layer.keys();
assert!(keys.contains(&"host".to_string()));
assert!(keys.contains(&"verbose".to_string()));
assert_eq!(keys.len(), 2);
}
}