config checking: some prompting works
This commit is contained in:
		
							parent
							
								
									9f38a41b46
								
							
						
					
					
						commit
						264cd364ed
					
				
					 1 changed files with 145 additions and 27 deletions
				
			
		|  | @ -2,7 +2,18 @@ use crate::key_value::KeyValueStore as KV; | |||
| use std::error::Error; | ||||
| 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: &[ | ||||
|         CfgGroup { name: "config", | ||||
|             description: "(internal values)", | ||||
|  | @ -49,11 +60,11 @@ pub fn populate_defaults(config: &KV) { | |||
|     for group in CONFIG_SPEC.groups { | ||||
|         for field in group.fields { | ||||
|             match field { | ||||
|                 CfgField::Silent { key, default, .. } => { | ||||
|                     config.default(&format!("{}-{}", group.name, key), default) | ||||
|                 CfgField::Silent { default, .. } => { | ||||
|                     config.default(&field.full_key(group.name), default) | ||||
|                 }, | ||||
|                 CfgField::Default { key, default, .. } => { | ||||
|                     config.default(&format!("{}-{}", group.name, key), default) | ||||
|                 CfgField::Default { 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"; | ||||
| const ANSI_FIELD: &str = "\x1b[1;33m"; | ||||
| const ANSI_RESET: &str = "\x1b[0m"; | ||||
| 
 | ||||
| /// Check all groups / fields and interactively edit as needed.
 | ||||
| ///
 | ||||
| /// Will report if the config is fine or has missing values.
 | ||||
| pub fn interactive_check(config: KV) -> Result<(), Box<dyn Error>> { | ||||
|     let mut all_valid : bool = true; | ||||
|     for group in CONFIG_SPEC.groups { | ||||
|         group_header( group.name, group.description ); | ||||
|         // TODO: skip category option?
 | ||||
|         for field in group.fields { | ||||
|             check_field( &config, group.name, field )?; | ||||
|         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 { | ||||
|                         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)
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
|     if all_valid { | ||||
|         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(()) } | ||||
|     show_field(config, grpname, field); | ||||
|     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 mut actions: Vec<&'static str> = Vec::new(); | ||||
|     if value.is_some() { actions.push("[K]eep as-is"); } | ||||
|     if field.is_password() { | ||||
|         match value { | ||||
|             Some(_) => println!("  current: *****"), | ||||
|             None => println!("  current: (empty)"), | ||||
|             Some(_) => { | ||||
|                 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: [K]eep as-is (if exists), [R]emove, [I]nput
 | ||||
|     } 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
 | ||||
|     } | ||||
|     // Action - 
 | ||||
|     // Action -
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /* ***** displaying various kinds of info ***** */ | ||||
| 
 | ||||
| fn group_header( name: &str, description: &str ) { | ||||
|     println!("=============================="); | ||||
|     println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description); | ||||
|     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 { | ||||
|     print!("New value: "); | ||||
|     io::stdout().flush().ok(); | ||||
|  | @ -111,6 +211,7 @@ fn prompt_single_line( ) -> String { | |||
|     input.trim().to_string() | ||||
| } | ||||
| 
 | ||||
| /// Read multiple lines of text, terminated by a line containing just a '.'.
 | ||||
| fn prompt_multiline( ) -> String { | ||||
|     println!("Enter new value: (end with '.' on a new line)"); | ||||
|     let mut acc = String::new(); | ||||
|  | @ -125,10 +226,13 @@ fn prompt_multiline( ) -> String { | |||
|     acc | ||||
| } | ||||
| 
 | ||||
| /// Read a password without echoing it.
 | ||||
| fn prompt_password( ) -> String { | ||||
|     rpassword::prompt_password("New password (not shown) : ").unwrap() | ||||
| } | ||||
| 
 | ||||
| /* ***** config structure definition ***** */ | ||||
| 
 | ||||
| /// Describes a field in the configuration.
 | ||||
| enum CfgField<'a> { | ||||
|     /// Silently writes a default to the DB if absent, never prompts for changes.
 | ||||
|  | @ -159,20 +263,30 @@ struct CfgSpec<'a> { | |||
| } | ||||
| 
 | ||||
| impl<'a> CfgField<'a> { | ||||
|     /// Silent fields don't get prompted or shown.
 | ||||
|     fn is_silent(&self) -> bool { | ||||
|         match self { | ||||
|             CfgField::Silent { .. } => true, | ||||
|             _ => false | ||||
|         } | ||||
|         matches!( self, CfgField::Silent { .. } ) | ||||
|     } | ||||
| 
 | ||||
|     /// Password fields will be censored when displayed.
 | ||||
|     fn is_password(&self) -> bool { | ||||
|         match self { | ||||
|             CfgField::Password { .. } => true, | ||||
|             _ => false | ||||
|         matches!( self, CfgField::Password { .. } ) | ||||
|     } | ||||
| 
 | ||||
|     /// 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 { | ||||
|         match self { | ||||
|             CfgField::Silent { key, .. } => key, | ||||
|  | @ -183,10 +297,12 @@ impl<'a> CfgField<'a> { | |||
|         }.to_string() | ||||
|     } | ||||
| 
 | ||||
|     /// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.)
 | ||||
|     fn full_key(&self, grpname: &str) -> String { | ||||
|         format!("{}-{}", grpname, self.key()) | ||||
|     } | ||||
| 
 | ||||
|     /// Gets `description` or "(none)".
 | ||||
|     fn description(&self) -> String { | ||||
|         match self { | ||||
|             CfgField::Silent { .. } => "(none)", | ||||
|  | @ -197,6 +313,8 @@ impl<'a> CfgField<'a> { | |||
|         }.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> { | ||||
|         match self { | ||||
|             CfgField::Silent { default, .. } => Some(default.to_string()), | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 nobody
						nobody