use crate::domain::{ConfigKey, ConfigValue, Result};
use crate::ports::ConfigSource;
use std::collections::HashMap;
const MAX_ARG_KEY_LEN: usize = 256;
const MAX_ARG_VALUE_LEN: usize = 65536;
#[derive(Debug, Clone)]
pub struct CommandLineAdapter {
values: HashMap<String, String>,
}
impl CommandLineAdapter {
pub fn new() -> Self {
Self {
values: HashMap::new(),
}
}
pub fn from_args<S: AsRef<str>>(args: Vec<S>) -> Self {
let mut adapter = Self::new();
adapter.parse_args(args);
adapter
}
pub fn from_env_args() -> Self {
let args: Vec<String> = std::env::args().skip(1).collect();
Self::from_args(args)
}
fn parse_args<S: AsRef<str>>(&mut self, args: Vec<S>) {
let mut i = 0;
while i < args.len() {
let arg = args[i].as_ref();
if arg.starts_with("--") && arg.contains('=') {
if let Some((key, value)) = arg.strip_prefix("--").and_then(|s| s.split_once('=')) {
if key.len() <= MAX_ARG_KEY_LEN && value.len() <= MAX_ARG_VALUE_LEN {
self.values.insert(key.to_string(), value.to_string());
}
}
i += 1;
}
else if arg.starts_with("--") {
if let Some(key) = arg.strip_prefix("--") {
if i + 1 < args.len() {
let next_arg = args[i + 1].as_ref();
if !next_arg.starts_with('-') {
if key.len() <= MAX_ARG_KEY_LEN && next_arg.len() <= MAX_ARG_VALUE_LEN {
self.values.insert(key.to_string(), next_arg.to_string());
}
i += 2;
} else {
i += 1;
}
} else {
i += 1;
}
} else {
i += 1;
}
}
else if arg.starts_with('-') && arg.len() == 2 {
if let Some(key) = arg.strip_prefix('-') {
if i + 1 < args.len() {
let next_arg = args[i + 1].as_ref();
if !next_arg.starts_with('-') {
if key.len() <= MAX_ARG_KEY_LEN && next_arg.len() <= MAX_ARG_VALUE_LEN {
self.values.insert(key.to_string(), next_arg.to_string());
}
i += 2;
} else {
i += 1;
}
} else {
i += 1;
}
} else {
i += 1;
}
} else {
i += 1;
}
}
}
}
impl Default for CommandLineAdapter {
fn default() -> Self {
Self::new()
}
}
impl ConfigSource for CommandLineAdapter {
fn name(&self) -> &str {
"cli"
}
fn priority(&self) -> u8 {
3
}
fn get(&self, key: &ConfigKey) -> Result<Option<ConfigValue>> {
Ok(self
.values
.get(key.as_str())
.map(|v| ConfigValue::from(v.as_str())))
}
fn all_keys(&self) -> Result<Vec<ConfigKey>> {
Ok(self
.values
.keys()
.map(|k| ConfigKey::from(k.as_str()))
.collect())
}
fn reload(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_adapter_name() {
let adapter = CommandLineAdapter::new();
assert_eq!(adapter.name(), "cli");
}
#[test]
fn test_cli_adapter_priority() {
let adapter = CommandLineAdapter::new();
assert_eq!(adapter.priority(), 3);
}
#[test]
fn test_cli_adapter_empty() {
let adapter = CommandLineAdapter::new();
let key = ConfigKey::from("test.key");
let value = adapter.get(&key).unwrap();
assert!(value.is_none());
}
#[test]
fn test_cli_adapter_long_form_equals() {
let args = vec!["--database.host=localhost", "--database.port=5432"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("database.host");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "localhost");
let key = ConfigKey::from("database.port");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "5432");
}
#[test]
fn test_cli_adapter_long_form_space() {
let args = vec!["--host", "localhost", "--port", "8080"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("host");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "localhost");
let key = ConfigKey::from("port");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "8080");
}
#[test]
fn test_cli_adapter_short_form() {
let args = vec!["-h", "localhost", "-p", "8080"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("h");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "localhost");
let key = ConfigKey::from("p");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "8080");
}
#[test]
fn test_cli_adapter_mixed_formats() {
let args = vec!["--database.host=localhost", "--port", "5432", "-d", "mydb"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("database.host");
let value = adapter.get(&key).unwrap();
assert_eq!(value.unwrap().as_str(), "localhost");
let key = ConfigKey::from("port");
let value = adapter.get(&key).unwrap();
assert_eq!(value.unwrap().as_str(), "5432");
let key = ConfigKey::from("d");
let value = adapter.get(&key).unwrap();
assert_eq!(value.unwrap().as_str(), "mydb");
}
#[test]
fn test_cli_adapter_missing_value() {
let args = vec!["--host"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("host");
let value = adapter.get(&key).unwrap();
assert!(value.is_none());
}
#[test]
fn test_cli_adapter_flag_as_value() {
let args = vec!["--host", "--port", "8080"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("host");
let value = adapter.get(&key).unwrap();
assert!(value.is_none());
let key = ConfigKey::from("port");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "8080");
}
#[test]
fn test_cli_adapter_equals_in_value() {
let args = vec!["--connection-string=host=localhost;port=5432"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("connection-string");
let value = adapter.get(&key).unwrap();
assert_eq!(value.unwrap().as_str(), "host=localhost;port=5432");
}
#[test]
fn test_cli_adapter_all_keys() {
let args = vec!["--key1=value1", "--key2", "value2", "-k", "value3"];
let adapter = CommandLineAdapter::from_args(args);
let keys = adapter.all_keys().unwrap();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&ConfigKey::from("key1")));
assert!(keys.contains(&ConfigKey::from("key2")));
assert!(keys.contains(&ConfigKey::from("k")));
}
#[test]
fn test_cli_adapter_reload() {
let args = vec!["--test=value"];
let mut adapter = CommandLineAdapter::from_args(args);
assert!(adapter.reload().is_ok());
}
#[test]
fn test_cli_adapter_default() {
let adapter = CommandLineAdapter::default();
assert_eq!(adapter.name(), "cli");
assert_eq!(adapter.priority(), 3);
}
#[test]
fn test_cli_adapter_empty_value() {
let args = vec!["--key="];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("key");
let value = adapter.get(&key).unwrap();
assert!(value.is_some());
assert_eq!(value.unwrap().as_str(), "");
}
#[test]
fn test_cli_adapter_non_flag_arguments() {
let args = vec!["positional1", "--key", "value", "positional2"];
let adapter = CommandLineAdapter::from_args(args);
let keys = adapter.all_keys().unwrap();
assert_eq!(keys.len(), 1);
assert!(keys.contains(&ConfigKey::from("key")));
}
#[test]
fn test_cli_adapter_override_value() {
let args = vec!["--key=value1", "--key=value2"];
let adapter = CommandLineAdapter::from_args(args);
let key = ConfigKey::from("key");
let value = adapter.get(&key).unwrap();
assert_eq!(value.unwrap().as_str(), "value2");
}
}