mod shell;
mod completer;
mod formatter;
mod commands;
mod help_manager;
pub use shell::ReplShell;
pub use commands::MetaCommand;
pub use help_manager::HelpManager;
pub use commands::MetaCommandResult;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplConfig {
#[serde(default = "default_show_timing")]
pub show_timing: bool,
#[serde(default = "default_history_path")]
pub history_path: Option<String>,
#[serde(default = "default_max_history")]
pub max_history: usize,
#[serde(skip)]
pub config_path: Option<PathBuf>,
#[serde(default = "default_output_format")]
pub output_format: OutputFormat,
#[serde(default = "default_show_row_count")]
pub show_row_count: bool,
#[serde(default = "default_auto_commit")]
pub auto_commit: bool,
#[serde(default = "default_null_display")]
pub null_display: String,
#[serde(default = "default_max_column_width")]
pub max_column_width: usize,
}
fn default_show_timing() -> bool { true }
fn default_history_path() -> Option<String> { Some(".heliosdb_history".to_string()) }
fn default_max_history() -> usize { 1000 }
fn default_output_format() -> OutputFormat { OutputFormat::Table }
fn default_show_row_count() -> bool { true }
fn default_auto_commit() -> bool { true }
fn default_null_display() -> String { "NULL".to_string() }
fn default_max_column_width() -> usize { 50 }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
Table,
Json,
Csv,
Vertical,
}
impl Default for ReplConfig {
fn default() -> Self {
Self {
show_timing: default_show_timing(),
history_path: default_history_path(),
max_history: default_max_history(),
config_path: None,
output_format: default_output_format(),
show_row_count: default_show_row_count(),
auto_commit: default_auto_commit(),
null_display: default_null_display(),
max_column_width: default_max_column_width(),
}
}
}
impl ReplConfig {
pub fn with_config_path(mut self, path: impl AsRef<Path>) -> Self {
self.config_path = Some(path.as_ref().to_path_buf());
self
}
pub fn from_file(path: impl AsRef<Path>) -> crate::Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
#[derive(Deserialize)]
struct ConfigWrapper {
#[serde(default)]
repl: Option<ReplConfig>,
}
let mut config = if let Ok(wrapper) = toml::from_str::<ConfigWrapper>(&content) {
if let Some(repl_config) = wrapper.repl {
repl_config
} else {
toml::from_str::<ReplConfig>(&content)
.map_err(|e| crate::Error::config(format!("Failed to parse REPL config: {}", e)))?
}
} else {
toml::from_str::<ReplConfig>(&content)
.map_err(|e| crate::Error::config(format!("Failed to parse REPL config: {}", e)))?
};
config.config_path = Some(path.to_path_buf());
Ok(config)
}
pub fn reload(&self) -> crate::Result<Self> {
match &self.config_path {
Some(path) => Self::from_file(path),
None => Err(crate::Error::config(
"No configuration file path set. Start REPL with --config to enable reload."
)),
}
}
pub fn save_to_file(&self, path: impl AsRef<Path>) -> crate::Result<()> {
let content = toml::to_string_pretty(self)
.map_err(|e| crate::Error::config(format!("Failed to serialize REPL config: {}", e)))?;
std::fs::write(path, content)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_repl_config_default() {
let config = ReplConfig::default();
assert!(config.show_timing);
assert_eq!(config.history_path, Some(".heliosdb_history".to_string()));
assert_eq!(config.max_history, 1000);
assert!(config.config_path.is_none());
assert_eq!(config.output_format, OutputFormat::Table);
assert!(config.show_row_count);
assert!(config.auto_commit);
assert_eq!(config.null_display, "NULL");
assert_eq!(config.max_column_width, 50);
}
#[test]
fn test_repl_config_serialization() {
let config = ReplConfig::default();
let toml_str = toml::to_string(&config).expect("Failed to serialize config");
assert!(toml_str.contains("show_timing = true"));
assert!(toml_str.contains("output_format = \"table\""));
assert!(toml_str.contains("show_row_count = true"));
}
#[test]
fn test_repl_config_from_file() {
let toml_content = r#"
show_timing = false
max_history = 500
output_format = "json"
show_row_count = false
auto_commit = false
null_display = "<null>"
max_column_width = 100
"#;
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
temp_file.write_all(toml_content.as_bytes()).expect("Failed to write temp file");
let config = ReplConfig::from_file(temp_file.path()).expect("Failed to load config");
assert!(!config.show_timing);
assert_eq!(config.max_history, 500);
assert_eq!(config.output_format, OutputFormat::Json);
assert!(!config.show_row_count);
assert!(!config.auto_commit);
assert_eq!(config.null_display, "<null>");
assert_eq!(config.max_column_width, 100);
assert!(config.config_path.is_some());
}
#[test]
fn test_repl_config_from_file_with_repl_section() {
let toml_content = r#"
[storage]
memory_only = true
[repl]
show_timing = false
output_format = "csv"
max_column_width = 200
"#;
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
temp_file.write_all(toml_content.as_bytes()).expect("Failed to write temp file");
let config = ReplConfig::from_file(temp_file.path()).expect("Failed to load config");
assert!(!config.show_timing);
assert_eq!(config.output_format, OutputFormat::Csv);
assert_eq!(config.max_column_width, 200);
}
#[test]
fn test_repl_config_reload() {
let toml_content = r#"
show_timing = true
output_format = "table"
"#;
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
temp_file.write_all(toml_content.as_bytes()).expect("Failed to write temp file");
let config = ReplConfig::from_file(temp_file.path()).expect("Failed to load config");
assert!(config.show_timing);
let new_content = r#"
show_timing = false
output_format = "json"
"#;
std::fs::write(temp_file.path(), new_content).expect("Failed to update temp file");
let reloaded = config.reload().expect("Failed to reload config");
assert!(!reloaded.show_timing);
assert_eq!(reloaded.output_format, OutputFormat::Json);
}
#[test]
fn test_repl_config_reload_no_path() {
let config = ReplConfig::default();
let result = config.reload();
assert!(result.is_err());
}
#[test]
fn test_repl_config_save_to_file() {
let mut config = ReplConfig::default();
config.show_timing = false;
config.output_format = OutputFormat::Csv;
config.max_column_width = 150;
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
config.save_to_file(temp_file.path()).expect("Failed to save config");
let loaded = ReplConfig::from_file(temp_file.path()).expect("Failed to load config");
assert!(!loaded.show_timing);
assert_eq!(loaded.output_format, OutputFormat::Csv);
assert_eq!(loaded.max_column_width, 150);
}
#[test]
fn test_output_format_variants() {
assert_eq!(OutputFormat::Table, OutputFormat::Table);
assert_eq!(OutputFormat::Json, OutputFormat::Json);
assert_eq!(OutputFormat::Csv, OutputFormat::Csv);
assert_eq!(OutputFormat::Vertical, OutputFormat::Vertical);
}
#[test]
fn test_config_with_path() {
let config = ReplConfig::default()
.with_config_path("/some/path/config.toml");
assert_eq!(
config.config_path,
Some(PathBuf::from("/some/path/config.toml"))
);
}
}