Repository navigation
CLI - Refactor yes/no prompts into a YesOrNo builder #6340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 7 commits
dbe30e0
4a6beeb
bd07870
e123249
c1be452
4432c84
c8b37b9
1b914c1
a7f4747
5702659
b0b99c2
16196f5
c2c15a5
1656d20
59ebb61
e7ffc83
91d62d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -329,20 +329,53 @@ pub fn host_or_url_to_host_and_protocol(host_or_url: &str) -> (&str, Option<&str | |
| } | ||
| } | ||
|
|
||
| /// Prompt the user for `y` or `n` from stdin. | ||
| /// | ||
| /// Return `false` unless the input is `y`. | ||
| pub fn y_or_n(force: bool, prompt: &str) -> anyhow::Result<bool> { | ||
| if force { | ||
| println!("Skipping confirmation due to --yes"); | ||
| return Ok(true); | ||
| /// A yes/no prompt with a required message, force setting, and default. | ||
|
bfops marked this conversation as resolved.
Outdated
|
||
| pub struct YesOrNo { | ||
| message: String, | ||
| force: Option<bool>, | ||
| default: Option<bool>, | ||
| } | ||
|
|
||
| impl YesOrNo { | ||
| pub fn new(message: impl Into<String>) -> Self { | ||
| Self { | ||
| message: message.into(), | ||
| force: None, | ||
| default: None, | ||
| } | ||
| } | ||
|
|
||
| pub fn force(mut self, force: bool) -> Self { | ||
| self.force = Some(force); | ||
| self | ||
| } | ||
|
|
||
| /// Set the response for empty input, including EOF. | ||
| pub fn default(mut self, default: bool) -> Self { | ||
| self.default = Some(default); | ||
| self | ||
| } | ||
|
|
||
| pub fn prompt(self) -> anyhow::Result<bool> { | ||
| assert!(!self.message.trim().is_empty(), "YesOrNo requires a message"); | ||
| let force = self.force.expect("YesOrNo requires an explicit force setting"); | ||
|
bfops marked this conversation as resolved.
|
||
| let default = self.default.expect("YesOrNo requires an explicit default"); | ||
| if force { | ||
| println!("Skipping confirmation due to --yes"); | ||
| return Ok(true); | ||
| } | ||
| let choices = if default { "Y/n" } else { "y/N" }; | ||
| print!("{} [{choices}]", self.message); | ||
| std::io::stdout().flush()?; | ||
| let mut input = String::new(); | ||
| std::io::stdin().read_line(&mut input)?; | ||
| let input = input.trim().to_lowercase(); | ||
| if input.is_empty() { | ||
| // Note that this includes EOF | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kinda think that we should fail on EOF, since non-interactive contexts should be using Thoughts?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, I probably wouldn't return Ok(default) here I would return an Err
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| return Ok(default); | ||
| } | ||
| Ok(input == "y" || input == "yes") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: this is the previous behavior, but since we now have an optional default, I could see an argument for requiring a clear yes or no, and ambiguous input causing a re-prompt. I'm happy to update that here/now if we think it's worth it. |
||
| } | ||
| let mut input = String::new(); | ||
| print!("{prompt} [y/N]"); | ||
| std::io::stdout().flush()?; | ||
| std::io::stdin().read_line(&mut input)?; | ||
| let input = input.trim().to_lowercase(); | ||
| Ok(input == "y" || input == "yes") | ||
| } | ||
|
|
||
| pub fn decode_identity(token: &String) -> anyhow::Result<String> { | ||
|
|
@@ -371,14 +404,16 @@ pub async fn get_login_token_or_log_in( | |
| return Ok(token.clone()); | ||
| } | ||
|
|
||
| // Note: We pass `force: false` to `y_or_n` because if we're running non-interactively we want to default to "no", not yes! | ||
| // Note: We leave `force` false because if we're running non-interactively we want to default to "no", not yes! | ||
| let full_login = interactive | ||
| && y_or_n( | ||
| false, | ||
| && YesOrNo::new( | ||
| // It would be "ideal" if we could print the `spacetimedb.com` by deriving it from the `default_auth_host` constant, | ||
| // but this will change _so_ infrequently that it's not even worth the time to write that code and test it. | ||
| "You are not logged in. Would you like to log in with spacetimedb.com?", | ||
| )?; | ||
| ) | ||
| .force(false) | ||
| .default(false) | ||
| .prompt()?; | ||
|
|
||
| if full_login { | ||
| let host = Url::parse(DEFAULT_AUTH_HOST)?; | ||
|
|
@@ -398,3 +433,31 @@ pub fn resolve_sibling_binary(bin_name: &str) -> anyhow::Result<PathBuf> { | |
| .with_extension(std::env::consts::EXE_EXTENSION); | ||
| Ok(bin_path) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod yes_or_no_tests { | ||
| use super::YesOrNo; | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "YesOrNo requires a message")] | ||
| fn empty_message_is_rejected_even_when_forced() { | ||
| YesOrNo::new(" ").default(false).force(true).prompt().unwrap(); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "YesOrNo requires an explicit default")] | ||
| fn missing_default_is_rejected_even_when_forced() { | ||
| YesOrNo::new("Continue?").force(true).prompt().unwrap(); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic(expected = "YesOrNo requires an explicit force setting")] | ||
| fn missing_force_is_rejected() { | ||
| YesOrNo::new("Continue?").default(false).prompt().unwrap(); | ||
| } | ||
|
|
||
| #[test] | ||
| fn force_overrides_explicit_default() { | ||
| assert!(YesOrNo::new("Continue?").default(false).force(true).prompt().unwrap()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I changed printing behavior here. Now in non-interactive mode we still print the database tree info and the prompt (followed by "skipping confirmation due to --yes").
That just seems clearer and better to me? Totally fine to revert it though.