config checking: some prompting works
This commit is contained in:
parent
9f38a41b46
commit
264cd364ed
|
@ -2,7 +2,18 @@ use crate::key_value::KeyValueStore as KV;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
const CONFIG_SPEC: CfgSpec<'_> = CfgSpec {
|
/// Text shown for an empty field.
|
||||||
|
const EMPTY_VALUE : &str = "(empty)";
|
||||||
|
/// Text shown in place of a password.
|
||||||
|
const HIDDEN_PASSWORD : &str = "*****";
|
||||||
|
|
||||||
|
// highlight some of the info
|
||||||
|
const ANSI_GROUP: &str = "\x1b[1;31m";
|
||||||
|
const ANSI_FIELD: &str = "\x1b[1;33m";
|
||||||
|
const ANSI_NOTICE: &str= "\x1b[33m";
|
||||||
|
const ANSI_RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
|
const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
|
||||||
groups: &[
|
groups: &[
|
||||||
CfgGroup { name: "config",
|
CfgGroup { name: "config",
|
||||||
description: "(internal values)",
|
description: "(internal values)",
|
||||||
|
@ -49,11 +60,11 @@ pub fn populate_defaults(config: &KV) {
|
||||||
for group in CONFIG_SPEC.groups {
|
for group in CONFIG_SPEC.groups {
|
||||||
for field in group.fields {
|
for field in group.fields {
|
||||||
match field {
|
match field {
|
||||||
CfgField::Silent { key, default, .. } => {
|
CfgField::Silent { default, .. } => {
|
||||||
config.default(&format!("{}-{}", group.name, key), default)
|
config.default(&field.full_key(group.name), default)
|
||||||
},
|
},
|
||||||
CfgField::Default { key, default, .. } => {
|
CfgField::Default { default, .. } => {
|
||||||
config.default(&format!("{}-{}", group.name, key), default)
|
config.default(&field.full_key(group.name), default)
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -61,48 +72,137 @@ pub fn populate_defaults(config: &KV) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ANSI_GROUP: &str = "\x1b[1;31m";
|
/// Check all groups / fields and interactively edit as needed.
|
||||||
const ANSI_FIELD: &str = "\x1b[1;33m";
|
///
|
||||||
const ANSI_RESET: &str = "\x1b[0m";
|
/// Will report if the config is fine or has missing values.
|
||||||
|
|
||||||
pub fn interactive_check(config: KV) -> Result<(), Box<dyn Error>> {
|
pub fn interactive_check(config: KV) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut all_valid : bool = true;
|
||||||
for group in CONFIG_SPEC.groups {
|
for group in CONFIG_SPEC.groups {
|
||||||
group_header( group.name, group.description );
|
group_header( group.name, group.description );
|
||||||
// TODO: skip category option?
|
let todo = needs_info(&config, group);
|
||||||
|
let choices = if !todo.is_empty() {
|
||||||
|
println!( "{}The following fields need adjustment: {}{}", ANSI_NOTICE, todo.join(", "), ANSI_RESET );
|
||||||
|
&["[E]dit (default)", "[L]ist all", "[S]kip group"]
|
||||||
|
} else {
|
||||||
|
println!( "{}This group looks fine. OK to [S]kip, unless you want to adjust values here.{}", ANSI_NOTICE, ANSI_RESET );
|
||||||
|
&["[S]kip group (default)", "[E]dit", "[L]ist all", ]
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let choice = prompt_action( choices );
|
||||||
|
match choice {
|
||||||
|
'L' => { show_group(&config, group); },
|
||||||
|
'E' => {
|
||||||
for field in group.fields {
|
for field in group.fields {
|
||||||
check_field( &config, group.name, field )?;
|
check_field( &config, group.name, field, &mut all_valid )?;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
'S' => {
|
||||||
|
if !todo.is_empty() { all_valid = false; }
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
_ => { return Err("Wat.".into()); }, // shouldn't be able to happen (prompt already checks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if all_valid {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("WARNING: Config is incomplete. Re-run to fix.".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_field( config: &KV, grpname: &str, field: &CfgField ) -> Result<(), Box<dyn Error>> {
|
fn check_field( config: &KV, grpname: &str, field: &CfgField, ok: &mut bool ) -> Result<(), Box<dyn Error>> {
|
||||||
if field.is_silent() { return Ok(()) }
|
if field.is_silent() { return Ok(()) }
|
||||||
|
show_field(config, grpname, field);
|
||||||
let key = field.full_key(grpname);
|
let key = field.full_key(grpname);
|
||||||
println!( "{}{}{} - {}", ANSI_FIELD, &key, ANSI_RESET, field.description() );
|
|
||||||
println!( " default: {}", field.default().unwrap_or("(empty)".to_string()));
|
|
||||||
let value = config.get(&key).ok();
|
let value = config.get(&key).ok();
|
||||||
|
let mut actions: Vec<&'static str> = Vec::new();
|
||||||
|
if value.is_some() { actions.push("[K]eep as-is"); }
|
||||||
if field.is_password() {
|
if field.is_password() {
|
||||||
match value {
|
match value {
|
||||||
Some(_) => println!(" current: *****"),
|
Some(_) => {
|
||||||
None => println!(" current: (empty)"),
|
println!(" current: {}", HIDDEN_PASSWORD);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!(" current: {}", EMPTY_VALUE);
|
||||||
|
actions.push("[K]eep as-is"); // we allow leaving the password empty if you don't want to set it
|
||||||
|
},
|
||||||
};
|
};
|
||||||
//todo!()
|
//todo!()
|
||||||
// TODO: [K]eep as-is (if exists), [R]emove, [I]nput
|
// TODO: [K]eep as-is (if exists), [R]emove, [I]nput
|
||||||
} else {
|
} else {
|
||||||
println!( " current: {}", value.unwrap_or("(empty)".to_string()) );
|
println!( " current: {}", value.unwrap_or(EMPTY_VALUE.to_string()) );
|
||||||
// TODO: [K]eep as-is, [R]eset to default, [I]nput, [L]ong (multiline) input
|
// TODO: [K]eep as-is, [R]eset to default, [I]nput, [L]ong (multiline) input
|
||||||
}
|
}
|
||||||
// Action -
|
// Action -
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ***** displaying various kinds of info ***** */
|
||||||
|
|
||||||
fn group_header( name: &str, description: &str ) {
|
fn group_header( name: &str, description: &str ) {
|
||||||
println!("==============================");
|
println!("==============================");
|
||||||
println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description);
|
println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description);
|
||||||
println!("");
|
println!("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_group( config: &KV, group: &CfgGroup ) {
|
||||||
|
for field in group.fields {
|
||||||
|
show_field( &config, group.name, field );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_field( config: &KV, grpname: &str, field: &CfgField ) {
|
||||||
|
if field.is_silent() { return }
|
||||||
|
let key = field.full_key(grpname);
|
||||||
|
println!( "{}{}{} - {}", ANSI_FIELD, &key, ANSI_RESET, field.description() );
|
||||||
|
println!( " default: {}", field.default().unwrap_or(EMPTY_VALUE.to_string()));
|
||||||
|
let value = config.get(&key).ok()
|
||||||
|
.map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s })
|
||||||
|
.unwrap_or(EMPTY_VALUE.to_string());
|
||||||
|
println!( " current: {}", value );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ***** basic validation ***** */
|
||||||
|
|
||||||
|
fn needs_info( config: &KV, group: &CfgGroup ) -> Vec<String> {
|
||||||
|
let mut acc = Vec::new();
|
||||||
|
for field in group.fields {
|
||||||
|
if field.is_action_required(config, group.name) {
|
||||||
|
acc.push( field.key() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ***** prompting (with actual I/O) ***** */
|
||||||
|
|
||||||
|
/// Ask for a choice between several options. First option is the default,
|
||||||
|
/// format options with the first character in brackets like so: `"[E]xample"`.
|
||||||
|
fn prompt_action( choices: &[&str] ) -> char {
|
||||||
|
let prompt_message = choices.join(", ");
|
||||||
|
let default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default();
|
||||||
|
loop {
|
||||||
|
println!( "{}", prompt_message );
|
||||||
|
print!( "Select: " );
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
|
||||||
|
// Read line and take first non-space character
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input).unwrap();
|
||||||
|
let input = input.trim().to_uppercase().chars().next().unwrap_or(default_choice);
|
||||||
|
|
||||||
|
// Check list of choices for match
|
||||||
|
if choices.iter().any(|&choice| choice.to_uppercase().chars().nth(1).unwrap_or_default() == input) {
|
||||||
|
return input;
|
||||||
|
} else {
|
||||||
|
println!("Invalid choice. Try again!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a single line of text.
|
||||||
fn prompt_single_line( ) -> String {
|
fn prompt_single_line( ) -> String {
|
||||||
print!("New value: ");
|
print!("New value: ");
|
||||||
io::stdout().flush().ok();
|
io::stdout().flush().ok();
|
||||||
|
@ -111,6 +211,7 @@ fn prompt_single_line( ) -> String {
|
||||||
input.trim().to_string()
|
input.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read multiple lines of text, terminated by a line containing just a '.'.
|
||||||
fn prompt_multiline( ) -> String {
|
fn prompt_multiline( ) -> String {
|
||||||
println!("Enter new value: (end with '.' on a new line)");
|
println!("Enter new value: (end with '.' on a new line)");
|
||||||
let mut acc = String::new();
|
let mut acc = String::new();
|
||||||
|
@ -125,10 +226,13 @@ fn prompt_multiline( ) -> String {
|
||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a password without echoing it.
|
||||||
fn prompt_password( ) -> String {
|
fn prompt_password( ) -> String {
|
||||||
rpassword::prompt_password("New password (not shown) : ").unwrap()
|
rpassword::prompt_password("New password (not shown) : ").unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ***** config structure definition ***** */
|
||||||
|
|
||||||
/// Describes a field in the configuration.
|
/// Describes a field in the configuration.
|
||||||
enum CfgField<'a> {
|
enum CfgField<'a> {
|
||||||
/// Silently writes a default to the DB if absent, never prompts for changes.
|
/// Silently writes a default to the DB if absent, never prompts for changes.
|
||||||
|
@ -159,20 +263,30 @@ struct CfgSpec<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CfgField<'a> {
|
impl<'a> CfgField<'a> {
|
||||||
|
/// Silent fields don't get prompted or shown.
|
||||||
fn is_silent(&self) -> bool {
|
fn is_silent(&self) -> bool {
|
||||||
match self {
|
matches!( self, CfgField::Silent { .. } )
|
||||||
CfgField::Silent { .. } => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Password fields will be censored when displayed.
|
||||||
fn is_password(&self) -> bool {
|
fn is_password(&self) -> bool {
|
||||||
match self {
|
matches!( self, CfgField::Password { .. } )
|
||||||
CfgField::Password { .. } => true,
|
}
|
||||||
_ => false
|
|
||||||
|
/// Reports if the field needs changing or is ok as-is.
|
||||||
|
///
|
||||||
|
/// (Because of the default population at boot, Silent and Default must
|
||||||
|
/// *necessarily* always have a value. Optional is optional. Only
|
||||||
|
/// Password and RandomId might need actions if they are missing.)
|
||||||
|
fn is_action_required(&self, config: &KV, grpname: &str) -> bool {
|
||||||
|
if matches!( self, CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. } ) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
config.get(self.full_key(grpname).as_str()).ok().is_none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.)
|
||||||
fn key(&self) -> String {
|
fn key(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
CfgField::Silent { key, .. } => key,
|
CfgField::Silent { key, .. } => key,
|
||||||
|
@ -183,10 +297,12 @@ impl<'a> CfgField<'a> {
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.)
|
||||||
fn full_key(&self, grpname: &str) -> String {
|
fn full_key(&self, grpname: &str) -> String {
|
||||||
format!("{}-{}", grpname, self.key())
|
format!("{}-{}", grpname, self.key())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets `description` or "(none)".
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
CfgField::Silent { .. } => "(none)",
|
CfgField::Silent { .. } => "(none)",
|
||||||
|
@ -197,6 +313,8 @@ impl<'a> CfgField<'a> {
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a description of the default value if one exists.
|
||||||
|
/// (Either the value itself, or a description of the generator making a default value.)
|
||||||
fn default(&self) -> Option<String> {
|
fn default(&self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
CfgField::Silent { default, .. } => Some(default.to_string()),
|
CfgField::Silent { default, .. } => Some(default.to_string()),
|
||||||
|
|
Loading…
Reference in a new issue