Skip to content

Commit

Permalink
Allow FnMut for validate (#96)
Browse files Browse the repository at this point in the history
* Allow FnMut for validate

This allows mutable closures for validation, allowing stateful
validators.

This breaks BC. Input::interact*() now take &mut self instead of &self.
However, this shall not break most usres,
since (I think) users usually use Input in chained syntax
or call one of the &mut methods (like with_prompt) separately,
which would require a `let mut` anyway.

* Added example case with stateful validator

* Updated CHANGELOG.md

* Revert version bump
  • Loading branch information
SOF3 authored Nov 16, 2020
1 parent 2b5722d commit af66480
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 18 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.8.0

### Enhancements

* `Input::validate_with` can take a `FnMut` (allowing multiple references)

### Breaking

* `Input::interact*` methods take `&mut self` instead of `&self`

## 0.7.0

### Enhancements
Expand Down
14 changes: 9 additions & 5 deletions examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ fn main() {

let mail: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Your email")
.validate_with(|input: &String| -> Result<(), &str> {
if input.contains('@') {
Ok(())
} else {
Err("This is not a mail address")
.validate_with({
let mut force = None;
move |input: &String| -> Result<(), &str> {
if input.contains('@') || force.as_ref().map_or(false, |old| old == input) {
Ok(())
} else {
force = Some(input.clone());
Err("This is not a mail address; type the same value again to force use")
}
}
})
.interact_text()
Expand Down
20 changes: 10 additions & 10 deletions src/prompts/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct Input<'a, T> {
initial_text: Option<String>,
theme: &'a dyn Theme,
permit_empty: bool,
validator: Option<Box<dyn Fn(&T) -> Option<String> + 'a>>,
validator: Option<Box<dyn FnMut(&T) -> Option<String> + 'a>>,
}

impl<'a, T> Default for Input<'a, T>
Expand Down Expand Up @@ -139,15 +139,15 @@ where
/// .interact()
/// .unwrap();
/// ```
pub fn validate_with<V>(&mut self, validator: V) -> &mut Input<'a, T>
pub fn validate_with<V>(&mut self, mut validator: V) -> &mut Input<'a, T>
where
V: Validator<T> + 'a,
T: 'a,
{
let old_validator_func = self.validator.take();
let mut old_validator_func = self.validator.take();

self.validator = Some(Box::new(move |value: &T| -> Option<String> {
if let Some(old) = old_validator_func.as_ref() {
if let Some(old) = old_validator_func.as_mut() {
if let Some(err) = old(value) {
return Some(err);
}
Expand All @@ -168,12 +168,12 @@ where
/// while [`interact`](#method.interact) allows virtually any character to be used e.g arrow keys.
///
/// The dialog is rendered on stderr.
pub fn interact_text(&self) -> io::Result<T> {
pub fn interact_text(&mut self) -> io::Result<T> {
self.interact_text_on(&Term::stderr())
}

/// Like [`interact_text`](#method.interact_text) but allows a specific terminal to be set.
pub fn interact_text_on(&self, term: &Term) -> io::Result<T> {
pub fn interact_text_on(&mut self, term: &Term) -> io::Result<T> {
let mut render = TermThemeRenderer::new(term, self.theme);

loop {
Expand Down Expand Up @@ -265,7 +265,7 @@ where

match input.parse::<T>() {
Ok(value) => {
if let Some(ref validator) = self.validator {
if let Some(ref mut validator) = self.validator {
if let Some(err) = validator(&value) {
render.error(&err)?;
continue;
Expand Down Expand Up @@ -293,12 +293,12 @@ where
///
/// If the user confirms the result is `true`, `false` otherwise.
/// The dialog is rendered on stderr.
pub fn interact(&self) -> io::Result<T> {
pub fn interact(&mut self) -> io::Result<T> {
self.interact_on(&Term::stderr())
}

/// Like [`interact`](#method.interact) but allows a specific terminal to be set.
pub fn interact_on(&self, term: &Term) -> io::Result<T> {
pub fn interact_on(&mut self, term: &Term) -> io::Result<T> {
let mut render = TermThemeRenderer::new(term, self.theme);

loop {
Expand Down Expand Up @@ -336,7 +336,7 @@ where

match input.parse::<T>() {
Ok(value) => {
if let Some(ref validator) = self.validator {
if let Some(ref mut validator) = self.validator {
if let Some(err) = validator(&value) {
render.error(&err)?;
continue;
Expand Down
6 changes: 3 additions & 3 deletions src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ pub trait Validator<T> {
///
/// If this produces `Ok(())` then the value is used and parsed, if
/// an error is returned validation fails with that error.
fn validate(&self, input: &T) -> Result<(), Self::Err>;
fn validate(&mut self, input: &T) -> Result<(), Self::Err>;
}

impl<T, F: Fn(&T) -> Result<(), E>, E: Debug + Display> Validator<T> for F {
impl<T, F: FnMut(&T) -> Result<(), E>, E: Debug + Display> Validator<T> for F {
type Err = E;

fn validate(&self, input: &T) -> Result<(), Self::Err> {
fn validate(&mut self, input: &T) -> Result<(), Self::Err> {
self(input)
}
}

0 comments on commit af66480

Please sign in to comment.