use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub default_database: Option<PathBuf>,
#[serde(default)]
pub connection: ConnectionConfig,
#[serde(default)]
pub output: OutputSettings,
#[serde(default)]
pub performance: PerformanceConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub repl: ReplConfig,
pub data_directory: Option<PathBuf>,
pub default_keyspace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_history: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_completion: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_timing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_paging: Option<bool>,
#[serde(default)]
pub no_color: bool,
#[serde(default)]
pub schema_paths: Vec<PathBuf>,
#[serde(skip)]
pub execution_query: Option<String>,
#[serde(skip)]
pub execution_file: Option<PathBuf>,
pub output_mode: Option<String>,
pub query_limit: Option<usize>,
#[serde(skip)]
pub cassandra_version: Option<String>,
#[serde(skip)]
#[allow(dead_code)]
pub resolved_version: Option<cqlite_core::version_hints::ResolvedVersion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionConfig {
pub timeout_ms: u64,
pub retry_attempts: u32,
pub pool_size: u32,
}
impl Default for ConnectionConfig {
fn default() -> Self {
Self {
timeout_ms: 30000,
retry_attempts: 3,
pool_size: 10,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputSettings {
pub max_rows: Option<usize>,
pub pager: Option<String>,
pub colors: bool,
pub timestamp_format: String,
}
impl Default for OutputSettings {
fn default() -> Self {
Self {
max_rows: Some(1000),
pager: None,
colors: true,
timestamp_format: "%Y-%m-%d %H:%M:%S".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub query_timeout_ms: u64,
pub memory_limit_mb: Option<u64>,
pub cache_size_mb: u64,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
query_timeout_ms: 30000,
memory_limit_mb: None,
cache_size_mb: 64,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub file: Option<PathBuf>,
pub format: LogFormat,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
file: None,
format: LogFormat::Pretty,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum LogFormat {
Plain,
Json,
#[default]
Pretty,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplConfig {
pub enable_history: bool,
pub enable_completion: bool,
pub enable_colors: bool,
pub show_timing: bool,
pub page_size: usize,
pub enable_paging: bool,
pub max_history_size: usize,
pub prompt: String,
pub prompt_continuation: String,
pub history_file: Option<PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Self {
default_database: None,
connection: ConnectionConfig::default(),
output: OutputSettings::default(),
performance: PerformanceConfig::default(),
logging: LoggingConfig::default(),
repl: ReplConfig::default(),
data_directory: None,
default_keyspace: None,
enable_history: None,
enable_completion: None,
show_timing: None,
page_size: None,
enable_paging: None,
no_color: false,
schema_paths: Vec::new(),
execution_query: None,
execution_file: None,
output_mode: None,
query_limit: None,
cassandra_version: None,
resolved_version: None,
}
}
}
impl Config {
pub fn load(config_path: Option<PathBuf>, cli: &crate::cli_types::Cli) -> Result<Self> {
let mut builder = ConfigBuilder::from_defaults()
.with_user_config()? .with_project_config()?;
if let Some(path) = config_path {
builder = builder.with_explicit_config(path)?;
}
Ok(builder.with_env()?.with_flags(cli).build())
}
#[allow(dead_code)]
pub async fn resolve_version(
&mut self,
platform: std::sync::Arc<cqlite_core::Platform>,
) -> Result<()> {
use cqlite_core::version_hints::VersionHintResolver;
use std::path::PathBuf;
let default_path = PathBuf::from(".");
let data_dir = self
.data_directory
.as_deref()
.unwrap_or(default_path.as_path());
self.resolved_version = Some(
VersionHintResolver::resolve(self.cassandra_version.clone(), data_dir, platform)
.await?,
);
Ok(())
}
#[allow(dead_code)]
pub fn version_info(&self) -> Option<&cqlite_core::version_hints::ResolvedVersion> {
self.resolved_version.as_ref()
}
#[allow(dead_code)]
pub fn version_string(&self) -> String {
self.resolved_version
.as_ref()
.map(|rv| rv.version_or_unknown().to_string())
.unwrap_or_else(|| "not resolved".to_string())
}
#[allow(dead_code)]
pub fn version_source(&self) -> String {
self.resolved_version
.as_ref()
.map(|rv| rv.source.description().to_string())
.unwrap_or_else(|| "not resolved".to_string())
}
fn load_from_file(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config: Config = match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => {
toml::from_str(&content).with_context(|| "Failed to parse TOML config")?
}
Some("yaml") | Some("yml") => {
serde_yaml::from_str(&content).with_context(|| "Failed to parse YAML config")?
}
Some("json") => {
serde_json::from_str(&content).with_context(|| "Failed to parse JSON config")?
}
_ => return Err(anyhow::anyhow!("Unsupported config file format")),
};
Ok(config)
}
#[allow(dead_code)]
fn load_default() -> Result<Self> {
let config_paths = [
"cqlite.toml",
"cqlite.yaml",
"cqlite.yml",
"cqlite.json",
".cqlite.toml",
".cqlite.yaml",
".cqlite.yml",
".cqlite.json",
];
for path in &config_paths {
if Path::new(path).exists() {
return Self::load_from_file(Path::new(path));
}
}
if let Some(config_dir) = dirs::config_dir() {
let xdg_paths = [
config_dir.join("cqlite").join("config.toml"),
config_dir.join("cqlite").join("config.yaml"),
config_dir.join("cqlite").join("config.yml"),
config_dir.join("cqlite").join("config.json"),
];
for path in &xdg_paths {
if path.exists() {
return Self::load_from_file(path);
}
}
}
Ok(Self::default())
}
#[allow(dead_code)]
pub fn save_to_file(&self, path: &Path) -> Result<()> {
let content = match path.extension().and_then(|ext| ext.to_str()) {
Some("toml") => toml::to_string_pretty(self)
.with_context(|| "Failed to serialize config to TOML")?,
Some("yaml") | Some("yml") => {
serde_yaml::to_string(self).with_context(|| "Failed to serialize config to YAML")?
}
Some("json") => serde_json::to_string_pretty(self)
.with_context(|| "Failed to serialize config to JSON")?,
_ => return Err(anyhow::anyhow!("Unsupported config file format")),
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create config directory: {}", parent.display())
})?;
}
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {}", path.display()))?;
Ok(())
}
}
impl Default for ReplConfig {
fn default() -> Self {
Self {
enable_history: true,
enable_completion: true,
enable_colors: true,
show_timing: false,
page_size: 50,
enable_paging: true,
max_history_size: 1000,
prompt: "cqlite> ".to_string(),
prompt_continuation: " -> ".to_string(),
history_file: None,
}
}
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub color_enabled: bool,
pub limit: Option<usize>,
#[allow(dead_code)]
pub page_size: Option<usize>,
pub target: crate::output::OutputTarget,
pub overwrite: bool,
}
impl OutputConfig {
pub fn from_cli(
config: &Config,
no_color_flag: bool,
limit_flag: Option<usize>,
page_size_flag: Option<usize>,
output_flag: Option<std::path::PathBuf>,
overwrite_flag: bool,
) -> Self {
use crate::output::OutputTarget;
Self {
color_enabled: if no_color_flag {
false
} else {
config.output.colors
},
limit: limit_flag.or(config.query_limit),
page_size: page_size_flag.or(Some(config.repl.page_size)),
target: output_flag
.map(OutputTarget::File)
.unwrap_or(OutputTarget::Stdout),
overwrite: overwrite_flag,
}
}
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
color_enabled: true,
limit: None,
page_size: Some(50),
target: crate::output::OutputTarget::Stdout,
overwrite: false,
}
}
}
fn merge_partial_config(base: Config, overlay: Config) -> Config {
let final_no_color = overlay.no_color || base.no_color;
let final_output_colors = if final_no_color {
false
} else {
overlay.output.colors
};
Config {
data_directory: overlay.data_directory.or(base.data_directory),
default_keyspace: overlay.default_keyspace.or(base.default_keyspace),
schema_paths: if overlay.schema_paths.is_empty() {
base.schema_paths
} else {
overlay.schema_paths
},
output_mode: overlay.output_mode.or(base.output_mode),
query_limit: overlay.query_limit.or(base.query_limit),
connection: overlay.connection,
output: OutputSettings {
max_rows: overlay.output.max_rows.or(base.output.max_rows),
pager: overlay.output.pager.or(base.output.pager),
colors: final_output_colors,
timestamp_format: overlay.output.timestamp_format,
},
repl: ReplConfig {
enable_history: overlay.repl.enable_history,
enable_completion: overlay.repl.enable_completion,
enable_colors: overlay.repl.enable_colors,
show_timing: overlay.repl.show_timing,
page_size: overlay.repl.page_size,
enable_paging: overlay.repl.enable_paging,
max_history_size: overlay.repl.max_history_size,
prompt: overlay.repl.prompt,
prompt_continuation: overlay.repl.prompt_continuation,
history_file: overlay.repl.history_file.or(base.repl.history_file),
},
performance: overlay.performance,
logging: overlay.logging,
enable_history: overlay.enable_history.or(base.enable_history),
enable_completion: overlay.enable_completion.or(base.enable_completion),
show_timing: overlay.show_timing.or(base.show_timing),
page_size: overlay.page_size.or(base.page_size),
enable_paging: overlay.enable_paging.or(base.enable_paging),
no_color: final_no_color,
execution_query: base.execution_query,
execution_file: base.execution_file,
cassandra_version: overlay.cassandra_version.or(base.cassandra_version),
resolved_version: base.resolved_version,
default_database: base.default_database,
}
}
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn from_defaults() -> Self {
Self {
config: Config::default(),
}
}
#[allow(dead_code)]
pub fn with_file(mut self, path: Option<PathBuf>) -> Result<Self> {
if let Some(p) = path {
let loaded = Config::load_from_file(&p)?;
self.config = loaded;
}
Ok(self)
}
pub fn with_user_config(mut self) -> Result<Self> {
if let Some(user_path) = Self::user_config_path() {
if user_path.exists() {
let loaded = Config::load_from_file(&user_path).with_context(|| {
format!("Failed to load user config: {}", user_path.display())
})?;
self.config = merge_partial_config(self.config, loaded);
}
}
Ok(self)
}
pub fn with_project_config(mut self) -> Result<Self> {
let project_path = PathBuf::from("./.cqlite.toml");
if project_path.exists() {
let loaded = Config::load_from_file(&project_path)
.with_context(|| "Failed to load project config")?;
self.config = merge_partial_config(self.config, loaded);
}
Ok(self)
}
pub fn with_explicit_config(mut self, path: PathBuf) -> Result<Self> {
let loaded = Config::load_from_file(&path)
.with_context(|| format!("Failed to load config file: {}", path.display()))?;
self.config = merge_partial_config(self.config, loaded);
Ok(self)
}
fn user_config_path() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
dirs::home_dir().map(|h| h.join("Library/Application Support/cqlite/config.toml"))
}
#[cfg(target_os = "windows")]
{
dirs::config_dir().map(|d| d.join("cqlite").join("config.toml"))
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
std::env::var("XDG_CONFIG_HOME")
.ok()
.map(|p| PathBuf::from(p).join("cqlite/config.toml"))
.or_else(|| dirs::home_dir().map(|h| h.join(".config/cqlite/config.toml")))
}
}
pub fn with_env(mut self) -> Result<Self> {
use std::env;
if let Ok(val) = env::var("CQLITE_DATA_DIR") {
self.config.data_directory = Some(PathBuf::from(val));
}
if let Ok(val) = env::var("CQLITE_SCHEMA") {
let paths: Vec<PathBuf> = val.split(',').map(|s| PathBuf::from(s.trim())).collect();
self.config.schema_paths = paths; }
if let Ok(val) = env::var("CQLITE_LIMIT") {
let limit: usize = val.parse().with_context(|| "Invalid CQLITE_LIMIT value")?;
if limit == 0 {
return Err(anyhow::anyhow!("CQLITE_LIMIT must be greater than 0"));
}
self.config.query_limit = Some(limit);
}
if let Ok(val) = env::var("CQLITE_PAGE_SIZE") {
let page_size: usize = val
.parse()
.with_context(|| "Invalid CQLITE_PAGE_SIZE value")?;
if page_size == 0 {
return Err(anyhow::anyhow!("CQLITE_PAGE_SIZE must be greater than 0"));
}
self.config.repl.page_size = page_size;
}
if let Ok(val) = env::var("CQLITE_NO_COLOR") {
let no_color = matches!(val.to_lowercase().as_str(), "1" | "true" | "yes" | "on");
self.config.no_color = no_color;
self.config.output.colors = !no_color;
}
if let Ok(val) = env::var("CQLITE_OUT") {
self.config.output_mode = Some(val);
}
Ok(self)
}
pub fn with_flags(mut self, cli: &crate::cli_types::Cli) -> Self {
if let Some(ref schema) = cli.schema {
self.config.schema_paths = vec![schema.clone()];
}
if let Some(ref data_dir) = cli.data_dir {
self.config.data_directory = Some(data_dir.clone());
}
if let Some(ref query) = cli.execute {
self.config.execution_query = Some(query.clone());
}
if let Some(ref file) = cli.file {
self.config.execution_file = Some(file.clone());
}
if let Some(ref out) = cli.out {
self.config.output_mode = Some(out.as_str().to_string());
}
if let Some(limit) = cli.limit {
self.config.query_limit = Some(limit);
}
if let Some(page_size) = cli.page_size {
self.config.repl.page_size = page_size;
}
if cli.no_color {
self.config.no_color = true;
self.config.output.colors = false;
}
if let Some(ref version) = cli.cassandra_version {
self.config.cassandra_version = Some(version.clone());
}
self
}
pub fn build(self) -> Config {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli_types::Cli;
use clap::Parser;
use serial_test::serial;
use std::sync::Arc;
use tempfile::TempDir;
#[tokio::test]
async fn test_cassandra_version_flag_passed_to_config() {
let cli = Cli::parse_from(&[
"cqlite",
"--cassandra-version",
"5.0",
"--data-dir",
"/tmp/data",
]);
let config = Config::load(None, &cli).unwrap();
assert_eq!(config.cassandra_version, Some("5.0".to_string()));
}
#[tokio::test]
async fn test_version_resolution_user_override() {
let temp_dir = TempDir::new().unwrap();
let cli = Cli::parse_from(&[
"cqlite",
"--cassandra-version",
"5.0",
"--data-dir",
temp_dir.path().to_str().unwrap(),
]);
let mut config = Config::load(None, &cli).unwrap();
let core_config = cqlite_core::Config::default();
let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
config.resolve_version(platform).await.unwrap();
let version_info = config.version_info().unwrap();
assert_eq!(
version_info.source,
cqlite_core::version_hints::VersionSource::UserFlag
);
assert_eq!(version_info.version, Some("5.0".to_string()));
}
#[tokio::test]
async fn test_version_resolution_metadata_yml() {
let temp_dir = TempDir::new().unwrap();
let metadata_content = "cassandra_version: \"4.0\"\nkeyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
std::fs::write(&metadata_path, metadata_content).unwrap();
let cli = Cli::parse_from(&["cqlite", "--data-dir", temp_dir.path().to_str().unwrap()]);
let mut config = Config::load(None, &cli).unwrap();
let core_config = cqlite_core::Config::default();
let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
config.resolve_version(platform).await.unwrap();
let version_info = config.version_info().unwrap();
assert_eq!(
version_info.source,
cqlite_core::version_hints::VersionSource::DatasetMetadata
);
assert_eq!(version_info.version, Some("4.0".to_string()));
}
#[tokio::test]
async fn test_version_resolution_unknown() {
let temp_dir = TempDir::new().unwrap();
let cli = Cli::parse_from(&["cqlite", "--data-dir", temp_dir.path().to_str().unwrap()]);
let mut config = Config::load(None, &cli).unwrap();
let core_config = cqlite_core::Config::default();
let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
config.resolve_version(platform).await.unwrap();
let version_info = config.version_info().unwrap();
assert_eq!(
version_info.source,
cqlite_core::version_hints::VersionSource::Unknown
);
assert_eq!(version_info.version, None);
assert_eq!(version_info.version_or_unknown(), "unknown");
}
#[tokio::test]
async fn test_version_precedence_user_overrides_metadata() {
let temp_dir = TempDir::new().unwrap();
let metadata_content = "cassandra_version: \"4.0\"\nkeyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
std::fs::write(&metadata_path, metadata_content).unwrap();
let cli = Cli::parse_from(&[
"cqlite",
"--cassandra-version",
"5.0",
"--data-dir",
temp_dir.path().to_str().unwrap(),
]);
let mut config = Config::load(None, &cli).unwrap();
let core_config = cqlite_core::Config::default();
let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
config.resolve_version(platform).await.unwrap();
let version_info = config.version_info().unwrap();
assert_eq!(
version_info.source,
cqlite_core::version_hints::VersionSource::UserFlag
);
assert_eq!(version_info.version, Some("5.0".to_string()));
}
#[tokio::test]
async fn test_version_string_helpers() {
let temp_dir = TempDir::new().unwrap();
let cli = Cli::parse_from(&[
"cqlite",
"--cassandra-version",
"5.0",
"--data-dir",
temp_dir.path().to_str().unwrap(),
]);
let mut config = Config::load(None, &cli).unwrap();
assert_eq!(config.version_string(), "not resolved");
assert_eq!(config.version_source(), "not resolved");
let core_config = cqlite_core::Config::default();
let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
config.resolve_version(platform).await.unwrap();
assert_eq!(config.version_string(), "5.0");
assert!(config.version_source().contains("User-provided flag"));
}
#[test]
fn test_config_default_includes_version_fields() {
let config = Config::default();
assert_eq!(config.cassandra_version, None);
assert_eq!(config.resolved_version, None);
}
#[test]
fn test_config_builder_preserves_version_flag() {
let cli = Cli::parse_from(&["cqlite", "--cassandra-version", "4.0"]);
let config = ConfigBuilder::from_defaults().with_flags(&cli).build();
assert_eq!(config.cassandra_version, Some("4.0".to_string()));
}
#[test]
#[serial]
fn test_env_var_replaces_config_file_schema_paths() {
use std::env;
env::set_var("CQLITE_SCHEMA", "/env/path1,/env/path2");
let mut config = Config::default();
config.schema_paths = vec![PathBuf::from("/file/path1"), PathBuf::from("/file/path2")];
let builder = ConfigBuilder { config };
let result = builder.with_env().unwrap();
assert_eq!(result.config.schema_paths.len(), 2);
assert_eq!(result.config.schema_paths[0], PathBuf::from("/env/path1"));
assert_eq!(result.config.schema_paths[1], PathBuf::from("/env/path2"));
env::remove_var("CQLITE_SCHEMA");
}
#[test]
#[serial]
fn test_env_var_single_schema_path_replaces_multiple() {
use std::env;
env::set_var("CQLITE_SCHEMA", "/env/single/path");
let mut config = Config::default();
config.schema_paths = vec![
PathBuf::from("/file/path1"),
PathBuf::from("/file/path2"),
PathBuf::from("/file/path3"),
];
let builder = ConfigBuilder { config };
let result = builder.with_env().unwrap();
assert_eq!(result.config.schema_paths.len(), 1);
assert_eq!(
result.config.schema_paths[0],
PathBuf::from("/env/single/path")
);
env::remove_var("CQLITE_SCHEMA");
}
#[test]
#[serial]
fn test_cli_flag_overrides_env_var_schema() {
use std::env;
env::set_var("CQLITE_SCHEMA", "/env/path1,/env/path2");
let mut config = Config::default();
config.schema_paths = vec![PathBuf::from("/file/path")];
let builder = ConfigBuilder { config };
let result = builder.with_env().unwrap();
assert_eq!(result.config.schema_paths.len(), 2);
let cli = Cli::parse_from(&["cqlite", "--schema", "/cli/path"]);
let final_config = result.with_flags(&cli).build();
assert_eq!(final_config.schema_paths.len(), 1);
assert_eq!(final_config.schema_paths[0], PathBuf::from("/cli/path"));
env::remove_var("CQLITE_SCHEMA");
}
#[test]
#[serial]
fn test_schema_precedence_chain_complete() {
use std::env;
let mut config = Config::default();
config.schema_paths = vec![PathBuf::from("/file/path")];
env::set_var("CQLITE_SCHEMA", "/env/path");
let builder = ConfigBuilder { config };
let with_env = builder.with_env().unwrap();
assert_eq!(
with_env.config.schema_paths,
vec![PathBuf::from("/env/path")]
);
let cli = Cli::parse_from(&["cqlite", "--schema", "/cli/path"]);
let final_config = with_env.with_flags(&cli).build();
assert_eq!(final_config.schema_paths, vec![PathBuf::from("/cli/path")]);
env::remove_var("CQLITE_SCHEMA");
}
#[test]
#[serial]
fn test_no_env_var_preserves_file_schema() {
use std::env;
env::remove_var("CQLITE_SCHEMA");
let mut config = Config::default();
config.schema_paths = vec![PathBuf::from("/file/path1"), PathBuf::from("/file/path2")];
let builder = ConfigBuilder { config };
let result = builder.with_env().unwrap();
assert_eq!(result.config.schema_paths.len(), 2);
assert_eq!(result.config.schema_paths[0], PathBuf::from("/file/path1"));
assert_eq!(result.config.schema_paths[1], PathBuf::from("/file/path2"));
}
#[test]
#[serial]
fn test_env_var_with_whitespace_trimming() {
use std::env;
env::set_var("CQLITE_SCHEMA", " /path1 , /path2 , /path3 ");
let config = Config::default();
let builder = ConfigBuilder { config };
let result = builder.with_env().unwrap();
assert_eq!(result.config.schema_paths.len(), 3);
assert_eq!(result.config.schema_paths[0], PathBuf::from("/path1"));
assert_eq!(result.config.schema_paths[1], PathBuf::from("/path2"));
assert_eq!(result.config.schema_paths[2], PathBuf::from("/path3"));
env::remove_var("CQLITE_SCHEMA");
}
#[test]
#[serial]
fn test_output_config_uses_defaults_when_no_flags_or_env() {
use std::env;
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
env::remove_var("CQLITE_NO_COLOR");
let cli = Cli::parse_from(&["cqlite"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(&config, false, None, None, None, false);
assert!(output.color_enabled); assert_eq!(output.limit, None); assert_eq!(output.page_size, Some(50)); }
#[test]
#[serial]
fn test_output_config_env_vars_override_defaults() {
use std::env;
env::set_var("CQLITE_LIMIT", "100");
env::set_var("CQLITE_PAGE_SIZE", "25");
env::set_var("CQLITE_NO_COLOR", "true");
let cli = Cli::parse_from(&["cqlite"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(&config, false, None, None, None, false);
assert!(!output.color_enabled); assert_eq!(output.limit, Some(100)); assert_eq!(output.page_size, Some(25));
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
env::remove_var("CQLITE_NO_COLOR");
}
#[test]
#[serial]
fn test_output_config_cli_flags_override_env_vars() {
use std::env;
env::set_var("CQLITE_LIMIT", "100");
env::set_var("CQLITE_PAGE_SIZE", "25");
env::set_var("CQLITE_NO_COLOR", "false");
let cli = Cli::parse_from(&[
"cqlite",
"--limit",
"200",
"--page-size",
"10",
"--no-color",
]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(
&config,
cli.no_color,
cli.limit,
cli.page_size,
cli.output.clone(),
cli.overwrite,
);
assert!(!output.color_enabled); assert_eq!(output.limit, Some(200)); assert_eq!(output.page_size, Some(10));
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
env::remove_var("CQLITE_NO_COLOR");
}
#[test]
#[serial]
fn test_output_config_partial_cli_flags_preserve_env_vars() {
use std::env;
env::set_var("CQLITE_LIMIT", "100");
env::set_var("CQLITE_PAGE_SIZE", "25");
let cli = Cli::parse_from(&["cqlite", "--no-color"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(
&config,
cli.no_color,
cli.limit,
cli.page_size,
cli.output.clone(),
cli.overwrite,
);
assert!(!output.color_enabled); assert_eq!(output.limit, Some(100)); assert_eq!(output.page_size, Some(25));
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
}
#[test]
#[serial]
fn test_output_config_no_color_flag_false_preserves_env() {
use std::env;
env::set_var("CQLITE_NO_COLOR", "true");
let cli = Cli::parse_from(&["cqlite"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(&config, false, None, None, None, false);
assert!(!output.color_enabled);
env::remove_var("CQLITE_NO_COLOR");
}
#[test]
#[serial]
fn test_output_config_complete_precedence_chain() {
use std::env;
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
env::remove_var("CQLITE_NO_COLOR");
let cli = Cli::parse_from(&["cqlite"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(&config, false, None, None, None, false);
assert!(output.color_enabled);
assert_eq!(output.limit, None);
assert_eq!(output.page_size, Some(50));
env::set_var("CQLITE_LIMIT", "150");
env::set_var("CQLITE_PAGE_SIZE", "30");
env::set_var("CQLITE_NO_COLOR", "true");
let cli = Cli::parse_from(&["cqlite"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(&config, false, None, None, None, false);
assert!(!output.color_enabled);
assert_eq!(output.limit, Some(150));
assert_eq!(output.page_size, Some(30));
let cli = Cli::parse_from(&["cqlite", "--limit", "300", "--page-size", "15"]);
let config = Config::load(None, &cli).unwrap();
let output = OutputConfig::from_cli(
&config,
cli.no_color,
cli.limit,
cli.page_size,
cli.output.clone(),
cli.overwrite,
);
assert!(!output.color_enabled); assert_eq!(output.limit, Some(300)); assert_eq!(output.page_size, Some(15));
env::remove_var("CQLITE_LIMIT");
env::remove_var("CQLITE_PAGE_SIZE");
env::remove_var("CQLITE_NO_COLOR");
}
}