Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions promkit/src/preset/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use crate::{
core::crossterm::style::ContentStyle,
validate::{ErrorMessageGenerator, Validator},
Prompt,
};

Expand Down Expand Up @@ -55,11 +54,12 @@ impl Password {
self
}

/// Configures a validator for the password input with a function to validate the input and another to configure the error message.
/// Configures a validator for the password input with a function or closure to
/// validate the input and another to generate the error message.
pub fn validator(
mut self,
validator: Validator<str>,
error_message_generator: ErrorMessageGenerator<str>,
validator: impl Fn(&str) -> bool + Send + Sync + 'static,
error_message_generator: impl Fn(&str) -> String + Send + Sync + 'static,
) -> Self {
self = Password(self.0.validator(validator, error_message_generator));
self
Expand Down
12 changes: 8 additions & 4 deletions promkit/src/preset/readline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
},
preset::Evaluator,
suggest::Suggest,
validate::{ErrorMessageGenerator, Validator, ValidatorManager},
validate::ValidatorManager,
widgets::{
listbox::{self, Listbox},
text::{self, Text},
Expand Down Expand Up @@ -261,11 +261,15 @@ impl Readline {
self
}

/// Configures a validator for the input with a function to validate the input and another to configure the error message.
/// Configures a validator for the input with a function or closure to validate
/// the input and another to generate the error message.
///
/// Closures can capture external variables, enabling validation against
/// runtime state (e.g., checking if a filename already exists).
pub fn validator(
mut self,
validator: Validator<str>,
error_message_generator: ErrorMessageGenerator<str>,
validator: impl Fn(&str) -> bool + Send + Sync + 'static,
error_message_generator: impl Fn(&str) -> String + Send + Sync + 'static,
) -> Self {
self.validator = Some(ValidatorManager::new(validator, error_message_generator));
self
Expand Down
69 changes: 57 additions & 12 deletions promkit/src/validate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
pub type Validator<T> = fn(&T) -> bool;
pub type ErrorMessageGenerator<T> = fn(&T) -> String;

/// A generic structure for validating inputs of any type.
///
/// This structure allows for the definition of custom validation logic
Expand All @@ -11,33 +8,36 @@ pub struct ValidatorManager<T: ?Sized> {
/// A function that takes a reference
/// to an input of type `T` and returns a boolean
/// indicating whether the input passes the validation.
validator: Validator<T>,
validator: Box<dyn Fn(&T) -> bool + Send + Sync>,
/// A function that takes a reference
/// to an input of type `T` and returns a `String`
/// that describes the validation error.
error_message_generator: ErrorMessageGenerator<T>,
error_message_generator: Box<dyn Fn(&T) -> String + Send + Sync>,
}

impl<T: ?Sized> ValidatorManager<T> {
/// Constructs a new `Validator` instance
/// Constructs a new `ValidatorManager` instance
/// with the specified validator and error message generator functions.
///
/// # Arguments
///
/// * `validator` - A function that takes a reference
/// * `validator` - A function or closure that takes a reference
/// to an input of type `T` and returns a boolean
/// indicating whether the input passes the validation.
/// * `error_message_generator` - A function that takes a reference
/// * `error_message_generator` - A function or closure that takes a reference
/// to an input of type `T` and returns a `String`
/// that describes the validation error.
///
/// # Returns
///
/// Returns a new instance of `Validator<T>`.
pub fn new(validator: Validator<T>, error_message_generator: ErrorMessageGenerator<T>) -> Self {
/// Returns a new instance of `ValidatorManager<T>`.
pub fn new(
validator: impl Fn(&T) -> bool + Send + Sync + 'static,
error_message_generator: impl Fn(&T) -> String + Send + Sync + 'static,
) -> Self {
Self {
validator,
error_message_generator,
validator: Box::new(validator),
error_message_generator: Box::new(error_message_generator),
}
}

Expand Down Expand Up @@ -72,3 +72,48 @@ impl<T: ?Sized> ValidatorManager<T> {
(self.error_message_generator)(input)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};

#[test]
fn function_pointer_validator() {
let vm = ValidatorManager::new(
|text: &str| text.len() > 3,
|text: &str| format!("Too short: {}", text.len()),
);
assert!(vm.validate("hello"));
assert!(!vm.validate("hi"));
assert_eq!(vm.generate_error_message("hi"), "Too short: 2");
}

#[test]
fn closure_captures_owned_data() {
let forbidden: Vec<String> = vec!["admin".into(), "root".into()];
let vm = ValidatorManager::new(
move |text: &str| !forbidden.contains(&text.to_string()),
|text: &str| format!("'{}' is not allowed", text),
);
assert!(!vm.validate("admin"));
assert!(vm.validate("user"));
}

#[test]
fn closure_captures_shared_state() {
let counter = Arc::new(Mutex::new(0u32));
let counter_clone = Arc::clone(&counter);
let vm = ValidatorManager::new(
move |_text: &str| {
let mut c = counter_clone.lock().unwrap();
*c += 1;
true
},
|_text: &str| String::new(),
);
vm.validate("a");
vm.validate("b");
assert_eq!(*counter.lock().unwrap(), 2);
}
}