use std::error::Error;
use std::io::{self, BufRead, Write};
use rusqlite::params;
use crate::ConfigEasyError;
use crate::action::{MenuAction, MenuActionCallback, validate_action_key};
use crate::identifier::validate_identifier;
use crate::row::SettingRow;
pub type Validator<'a> = dyn Fn(&str, &str) -> Result<(), Box<dyn Error + Send + Sync>> + 'a;
pub struct ConfigMenu<'a> {
conn: &'a rusqlite::Connection,
table: String,
key_column: String,
value_column: String,
order_by: Option<String>,
secret_keys: Vec<String>,
validator: Option<Box<Validator<'a>>>,
actions: Vec<MenuAction<'a>>,
}
impl<'a> ConfigMenu<'a> {
pub(crate) fn new(conn: &'a rusqlite::Connection) -> Self {
Self {
conn,
table: "settings".to_string(),
key_column: "key".to_string(),
value_column: "value".to_string(),
order_by: Some("key".to_string()),
secret_keys: Vec::new(),
validator: None,
actions: Vec::new(),
}
}
pub fn table(mut self, table: impl Into<String>) -> Self {
self.table = table.into();
self
}
pub fn key_column(mut self, key_column: impl Into<String>) -> Self {
self.key_column = key_column.into();
self
}
pub fn value_column(mut self, value_column: impl Into<String>) -> Self {
self.value_column = value_column.into();
self
}
pub fn order_by(mut self, order_by: impl Into<String>) -> Self {
self.order_by = Some(order_by.into());
self
}
pub fn secret_keys<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.secret_keys = keys.into_iter().map(Into::into).collect();
self
}
pub fn validator<F>(mut self, validator: F) -> Self
where
F: Fn(&str, &str) -> Result<(), Box<dyn Error + Send + Sync>> + 'a,
{
self.validator = Some(Box::new(validator));
self
}
pub fn action<F>(
mut self,
key: impl Into<String>,
label: impl Into<String>,
callback: F,
) -> Self
where
F: Fn(&rusqlite::Connection) -> Result<(), Box<dyn Error + Send + Sync>> + 'a,
{
self.actions.push(MenuAction {
key: key.into(),
label: label.into(),
callback: Box::new(callback) as Box<MenuActionCallback<'a>>,
});
self
}
pub fn run(&self) -> Result<(), ConfigEasyError> {
let stdin = io::stdin();
let mut stdout = io::stdout();
self.run_with(&mut stdin.lock(), &mut stdout)
}
fn run_with<R, W>(&self, input: &mut R, output: &mut W) -> Result<(), ConfigEasyError>
where
R: BufRead,
W: Write,
{
self.validate_config()?;
loop {
let rows = self.load_rows()?;
self.render(&rows, output)?;
let selection = read_trimmed_line(input)?;
if should_exit(&selection) {
return Ok(());
}
if let Some(action) = self.actions.iter().find(|action| action.key == selection) {
self.run_action(action)?;
continue;
}
match parse_selection(&selection, rows.len()) {
Ok(index) => match self.edit_row(&rows[index], input, output) {
Ok(()) => {}
Err(error @ ConfigEasyError::ValidationFailed { .. }) => {
writeln!(output, "{error}")?;
}
Err(error) => return Err(error),
},
Err(error) => writeln!(output, "{error}")?,
}
}
}
fn validate_config(&self) -> Result<(), ConfigEasyError> {
validate_identifier(&self.table)?;
validate_identifier(&self.key_column)?;
validate_identifier(&self.value_column)?;
if let Some(order_by) = &self.order_by {
validate_identifier(order_by)?;
}
let mut action_keys = Vec::new();
for action in &self.actions {
validate_action_key(&action.key)?;
if action_keys.iter().any(|key| key == &action.key) {
return Err(ConfigEasyError::DuplicateActionKey(action.key.clone()));
}
action_keys.push(action.key.clone());
}
Ok(())
}
fn load_rows(&self) -> Result<Vec<SettingRow>, ConfigEasyError> {
self.validate_config()?;
let order_by = self.order_by.as_deref().unwrap_or(&self.key_column);
let sql = format!(
"SELECT {}, {} FROM {} ORDER BY {}",
self.key_column, self.value_column, self.table, order_by
);
let mut statement = self.conn.prepare(&sql)?;
let rows = statement.query_map([], |row| {
Ok(SettingRow {
key: row.get(0)?,
value: row.get(1)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}
fn render<W: Write>(&self, rows: &[SettingRow], output: &mut W) -> Result<(), ConfigEasyError> {
writeln!(output, "Settings")?;
writeln!(output)?;
let key_width = rows.iter().map(|row| row.key.len()).max().unwrap_or(0);
for (index, row) in rows.iter().enumerate() {
writeln!(
output,
"{}) {:key_width$} {}",
index + 1,
row.key,
self.display_value(row),
)?;
}
writeln!(output)?;
write!(output, "{}", self.prompt())?;
output.flush()?;
Ok(())
}
fn prompt(&self) -> String {
if self.actions.is_empty() {
"Enter setting number to edit, or q to quit: ".to_string()
} else {
let actions = self
.actions
.iter()
.map(|action| format!("{} to {}", action.key, action.label))
.collect::<Vec<_>>()
.join(", ");
format!("Enter setting number to edit, {actions}, or q to quit: ")
}
}
fn display_value(&self, row: &SettingRow) -> String {
if self.secret_keys.iter().any(|key| key == &row.key) && !row.value.is_empty() {
"********".to_string()
} else {
row.value.clone()
}
}
fn edit_row<R, W>(
&self,
row: &SettingRow,
input: &mut R,
output: &mut W,
) -> Result<(), ConfigEasyError>
where
R: BufRead,
W: Write,
{
writeln!(output, "New value for {}:", row.key)?;
output.flush()?;
let new_value = read_trimmed_line(input)?;
self.validate_value(&row.key, &new_value)?;
self.update_row(&row.key, &new_value)?;
writeln!(output, "Updated {}.", row.key)?;
Ok(())
}
fn validate_value(&self, key: &str, value: &str) -> Result<(), ConfigEasyError> {
if let Some(validator) = &self.validator {
validator(key, value).map_err(|error| ConfigEasyError::ValidationFailed {
key: key.to_string(),
message: error.to_string(),
})?;
}
Ok(())
}
fn update_row(&self, key: &str, value: &str) -> Result<(), ConfigEasyError> {
self.validate_config()?;
let sql = format!(
"UPDATE {} SET {} = ?1 WHERE {} = ?2",
self.table, self.value_column, self.key_column
);
self.conn.execute(&sql, params![value, key])?;
Ok(())
}
fn run_action(&self, action: &MenuAction<'_>) -> Result<(), ConfigEasyError> {
(action.callback)(self.conn).map_err(|error| ConfigEasyError::ActionFailed {
key: action.key.clone(),
message: error.to_string(),
})
}
}
fn read_trimmed_line<R: BufRead>(input: &mut R) -> Result<String, ConfigEasyError> {
let mut line = String::new();
input.read_line(&mut line)?;
Ok(line.trim_end_matches(['\r', '\n']).trim().to_string())
}
fn should_exit(selection: &str) -> bool {
selection.is_empty() || selection == "q" || selection == "quit"
}
fn parse_selection(selection: &str, row_count: usize) -> Result<usize, ConfigEasyError> {
let selection = selection
.parse::<usize>()
.map_err(|_| ConfigEasyError::InvalidSelection)?;
if (1..=row_count).contains(&selection) {
Ok(selection - 1)
} else {
Err(ConfigEasyError::InvalidSelection)
}
}
#[cfg(test)]
mod tests;