Skip to content

Commit d588da3

Browse files
authored
Merge pull request #219 from qt2/password_validation
Add `validate_with` function to password prompt
2 parents f6f6e26 + 4b92988 commit d588da3

File tree

3 files changed

+97
-17
lines changed

3 files changed

+97
-17
lines changed

examples/password.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ fn main() {
44
let password = Password::with_theme(&ColorfulTheme::default())
55
.with_prompt("Password")
66
.with_confirmation("Repeat password", "Error: the passwords don't match.")
7+
.validate_with(|input: &String| -> Result<(), &str> {
8+
if input.len() > 3 {
9+
Ok(())
10+
} else {
11+
Err("Password must be longer than 3")
12+
}
13+
})
714
.interact()
815
.unwrap();
916

src/prompts/password.rs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
use std::io;
22

3-
use crate::theme::{SimpleTheme, TermThemeRenderer, Theme};
3+
use crate::{
4+
theme::{SimpleTheme, TermThemeRenderer, Theme},
5+
validate::PasswordValidator,
6+
};
47

58
use console::Term;
69
use zeroize::Zeroizing;
710

11+
type PasswordValidatorCallback<'a> = Box<dyn Fn(&String) -> Option<String> + 'a>;
12+
813
/// Renders a password input prompt.
914
///
1015
/// ## Example usage
@@ -25,6 +30,7 @@ pub struct Password<'a> {
2530
theme: &'a dyn Theme,
2631
allow_empty_password: bool,
2732
confirmation_prompt: Option<(String, String)>,
33+
validator: Option<PasswordValidatorCallback<'a>>,
2834
}
2935

3036
impl Default for Password<'static> {
@@ -40,7 +46,7 @@ impl Password<'static> {
4046
}
4147
}
4248

43-
impl Password<'_> {
49+
impl<'a> Password<'a> {
4450
/// Sets the password input prompt.
4551
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self {
4652
self.prompt = prompt.into();
@@ -73,6 +79,47 @@ impl Password<'_> {
7379
self
7480
}
7581

82+
/// Registers a validator.
83+
///
84+
/// # Example
85+
///
86+
/// ```no_run
87+
/// # use dialoguer::Password;
88+
/// let password: String = Password::new()
89+
/// .with_prompt("Enter password")
90+
/// .validate_with(|input: &String| -> Result<(), &str> {
91+
/// if input.len() > 8 {
92+
/// Ok(())
93+
/// } else {
94+
/// Err("Password must be longer than 8")
95+
/// }
96+
/// })
97+
/// .interact()
98+
/// .unwrap();
99+
/// ```
100+
pub fn validate_with<V>(&mut self, validator: V) -> &mut Self
101+
where
102+
V: PasswordValidator + 'a,
103+
V::Err: ToString,
104+
{
105+
let old_validator_func = self.validator.take();
106+
107+
self.validator = Some(Box::new(move |value: &String| -> Option<String> {
108+
if let Some(old) = &old_validator_func {
109+
if let Some(err) = old(value) {
110+
return Some(err);
111+
}
112+
}
113+
114+
match validator.validate(value) {
115+
Ok(()) => None,
116+
Err(err) => Some(err.to_string()),
117+
}
118+
}));
119+
120+
self
121+
}
122+
76123
/// Enables user interaction and returns the result.
77124
///
78125
/// If the user confirms the result is `true`, `false` otherwise.
@@ -89,28 +136,30 @@ impl Password<'_> {
89136
loop {
90137
let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?);
91138

139+
if let Some(ref validator) = self.validator {
140+
if let Some(err) = validator(&password) {
141+
render.error(&err)?;
142+
continue;
143+
}
144+
}
145+
92146
if let Some((ref prompt, ref err)) = self.confirmation_prompt {
93147
let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?);
94148

95-
if *password == *pw2 {
96-
render.clear()?;
97-
if self.report {
98-
render.password_prompt_selection(&self.prompt)?;
99-
}
100-
term.flush()?;
101-
return Ok((*password).clone());
149+
if *password != *pw2 {
150+
render.error(err)?;
151+
continue;
102152
}
153+
}
103154

104-
render.error(err)?;
105-
} else {
106-
render.clear()?;
107-
if self.report {
108-
render.password_prompt_selection(&self.prompt)?;
109-
}
110-
term.flush()?;
155+
render.clear()?;
111156

112-
return Ok((*password).clone());
157+
if self.report {
158+
render.password_prompt_selection(&self.prompt)?;
113159
}
160+
term.flush()?;
161+
162+
return Ok((*password).clone());
114163
}
115164
}
116165

@@ -139,6 +188,7 @@ impl<'a> Password<'a> {
139188
theme,
140189
allow_empty_password: false,
141190
confirmation_prompt: None,
191+
validator: None,
142192
}
143193
}
144194
}

src/validate.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,26 @@ where
2424
self(input)
2525
}
2626
}
27+
28+
/// Trait for password validators.
29+
#[allow(clippy::ptr_arg)]
30+
pub trait PasswordValidator {
31+
type Err;
32+
33+
/// Invoked with the value to validate.
34+
///
35+
/// If this produces `Ok(())` then the value is used and parsed, if
36+
/// an error is returned validation fails with that error.
37+
fn validate(&self, input: &String) -> Result<(), Self::Err>;
38+
}
39+
40+
impl<F, E> PasswordValidator for F
41+
where
42+
F: Fn(&String) -> Result<(), E>,
43+
{
44+
type Err = E;
45+
46+
fn validate(&self, input: &String) -> Result<(), Self::Err> {
47+
self(input)
48+
}
49+
}

0 commit comments

Comments
 (0)