refactor: add modal/validation utils and partial App encapsulation
Quick wins refactoring (Variant 1): - Created src/utils/modal_handler.rs (120+ lines) - 4 functions for modal handling (close, confirm, yes/no) - ModalAction enum for type-safe processing - English and Russian keyboard layout support - 4 unit tests - Created src/utils/validation.rs (180+ lines) - 7 validation functions (empty, length, IDs, etc) - Covers all common validation patterns - 7 unit tests - Partial App encapsulation: - Made config field private (readonly via app.config()) - Added 30+ getter/setter methods - Updated ui/messages.rs to use config() - Updated documentation: - REFACTORING_OPPORTUNITIES.md: #1 Complete, #5 Partial - CONTEXT.md: Added quick wins section Tests: 563 passed, 0 failed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
pub mod formatting;
|
||||
pub mod modal_handler;
|
||||
pub mod retry;
|
||||
pub mod tdlib;
|
||||
pub mod validation;
|
||||
|
||||
pub use formatting::*;
|
||||
pub use modal_handler::*;
|
||||
pub use retry::{with_timeout, with_timeout_msg};
|
||||
pub use tdlib::*;
|
||||
pub use validation::*;
|
||||
|
||||
184
src/utils/modal_handler.rs
Normal file
184
src/utils/modal_handler.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
//! Modal dialog utilities
|
||||
//!
|
||||
//! Переиспользуемая логика для обработки модальных окон (диалогов).
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
/// Результат обработки клавиши в модальном окне.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModalAction {
|
||||
/// Закрыть модалку (Escape была нажата)
|
||||
Close,
|
||||
/// Подтвердить действие (Enter была нажата)
|
||||
Confirm,
|
||||
/// Продолжить обработку ввода (другая клавиша)
|
||||
Continue,
|
||||
}
|
||||
|
||||
/// Обрабатывает стандартные клавиши для модальных окон.
|
||||
///
|
||||
/// Проверяет клавиши Escape (закрыть) и Enter (подтвердить).
|
||||
/// Если нажата другая клавиша, возвращает `Continue`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_code` - код нажатой клавиши
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `ModalAction::Close` - если нажата Escape
|
||||
/// * `ModalAction::Confirm` - если нажата Enter
|
||||
/// * `ModalAction::Continue` - для других клавиш
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use crossterm::event::KeyCode;
|
||||
/// use tele_tui::utils::modal_handler::{handle_modal_key, ModalAction};
|
||||
///
|
||||
/// assert_eq!(handle_modal_key(KeyCode::Esc), ModalAction::Close);
|
||||
/// assert_eq!(handle_modal_key(KeyCode::Enter), ModalAction::Confirm);
|
||||
/// assert_eq!(handle_modal_key(KeyCode::Char('a')), ModalAction::Continue);
|
||||
/// ```
|
||||
pub fn handle_modal_key(key_code: KeyCode) -> ModalAction {
|
||||
match key_code {
|
||||
KeyCode::Esc => ModalAction::Close,
|
||||
KeyCode::Enter => ModalAction::Confirm,
|
||||
_ => ModalAction::Continue,
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверяет, нужно ли закрыть модалку (нажата Escape).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use crossterm::event::KeyCode;
|
||||
/// use tele_tui::utils::modal_handler::should_close_modal;
|
||||
///
|
||||
/// assert!(should_close_modal(KeyCode::Esc));
|
||||
/// assert!(!should_close_modal(KeyCode::Enter));
|
||||
/// assert!(!should_close_modal(KeyCode::Char('q')));
|
||||
/// ```
|
||||
pub fn should_close_modal(key_code: KeyCode) -> bool {
|
||||
matches!(key_code, KeyCode::Esc)
|
||||
}
|
||||
|
||||
/// Проверяет, нужно ли подтвердить действие в модалке (нажата Enter).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use crossterm::event::KeyCode;
|
||||
/// use tele_tui::utils::modal_handler::should_confirm_modal;
|
||||
///
|
||||
/// assert!(should_confirm_modal(KeyCode::Enter));
|
||||
/// assert!(!should_confirm_modal(KeyCode::Esc));
|
||||
/// assert!(!should_confirm_modal(KeyCode::Char('y')));
|
||||
/// ```
|
||||
pub fn should_confirm_modal(key_code: KeyCode) -> bool {
|
||||
matches!(key_code, KeyCode::Enter)
|
||||
}
|
||||
|
||||
/// Обрабатывает клавиши для подтверждения Yes/No.
|
||||
///
|
||||
/// Поддерживает:
|
||||
/// - `y` / `Y` / `д` / `Д` - да (confirm)
|
||||
/// - `n` / `N` / `т` / `Т` - нет (close)
|
||||
/// - `Enter` - подтвердить (confirm)
|
||||
/// - `Esc` - отменить (close)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_code` - код нажатой клавиши
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Some(true)` - подтверждение (yes/Enter)
|
||||
/// * `Some(false)` - отмена (no/Escape)
|
||||
/// * `None` - другая клавиша (продолжить ввод)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use crossterm::event::KeyCode;
|
||||
/// use tele_tui::utils::modal_handler::handle_yes_no;
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('y')), Some(true));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('Y')), Some(true));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('д')), Some(true)); // русская 'y'
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Enter), Some(true));
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('n')), Some(false));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('т')), Some(false)); // русская 'n'
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Esc), Some(false));
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('a')), None);
|
||||
/// ```
|
||||
pub fn handle_yes_no(key_code: KeyCode) -> Option<bool> {
|
||||
match key_code {
|
||||
// Yes - подтверждение (английская и русская раскладка)
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Char('д') | KeyCode::Char('Д') => {
|
||||
Some(true)
|
||||
}
|
||||
KeyCode::Enter => Some(true),
|
||||
|
||||
// No - отмена (английская и русская раскладка)
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Char('т') | KeyCode::Char('Т') => {
|
||||
Some(false)
|
||||
}
|
||||
KeyCode::Esc => Some(false),
|
||||
|
||||
// Другие клавиши - продолжить
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handle_modal_key() {
|
||||
assert_eq!(handle_modal_key(KeyCode::Esc), ModalAction::Close);
|
||||
assert_eq!(handle_modal_key(KeyCode::Enter), ModalAction::Confirm);
|
||||
assert_eq!(handle_modal_key(KeyCode::Char('a')), ModalAction::Continue);
|
||||
assert_eq!(handle_modal_key(KeyCode::Up), ModalAction::Continue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_close_modal() {
|
||||
assert!(should_close_modal(KeyCode::Esc));
|
||||
assert!(!should_close_modal(KeyCode::Enter));
|
||||
assert!(!should_close_modal(KeyCode::Char('q')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_confirm_modal() {
|
||||
assert!(should_confirm_modal(KeyCode::Enter));
|
||||
assert!(!should_confirm_modal(KeyCode::Esc));
|
||||
assert!(!should_confirm_modal(KeyCode::Char('y')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_yes_no() {
|
||||
// Yes variants
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('y')), Some(true));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Y')), Some(true));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('д')), Some(true)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Д')), Some(true)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Enter), Some(true));
|
||||
|
||||
// No variants
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('n')), Some(false));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('N')), Some(false));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('т')), Some(false)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Т')), Some(false)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Esc), Some(false));
|
||||
|
||||
// Other keys
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('a')), None);
|
||||
assert_eq!(handle_yes_no(KeyCode::Up), None);
|
||||
assert_eq!(handle_yes_no(KeyCode::Char(' ')), None);
|
||||
}
|
||||
}
|
||||
191
src/utils/validation.rs
Normal file
191
src/utils/validation.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
//! Input validation utilities
|
||||
//!
|
||||
//! Переиспользуемые валидаторы для проверки пользовательского ввода.
|
||||
|
||||
use crate::types::{ChatId, MessageId, UserId};
|
||||
|
||||
/// Проверяет, что строка не пустая (после trim).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::utils::validation::is_non_empty;
|
||||
///
|
||||
/// assert!(is_non_empty("hello"));
|
||||
/// assert!(is_non_empty(" text "));
|
||||
/// assert!(!is_non_empty(""));
|
||||
/// assert!(!is_non_empty(" "));
|
||||
/// ```
|
||||
pub fn is_non_empty(text: &str) -> bool {
|
||||
!text.trim().is_empty()
|
||||
}
|
||||
|
||||
/// Проверяет, что текст не превышает максимальную длину.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `text` - текст для проверки
|
||||
/// * `max_length` - максимальная длина в символах
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::utils::validation::is_within_length;
|
||||
///
|
||||
/// assert!(is_within_length("hello", 10));
|
||||
/// assert!(!is_within_length("very long text here", 5));
|
||||
/// ```
|
||||
pub fn is_within_length(text: &str, max_length: usize) -> bool {
|
||||
text.chars().count() <= max_length
|
||||
}
|
||||
|
||||
/// Проверяет валидность ID чата (не нулевой).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::types::ChatId;
|
||||
/// use tele_tui::utils::validation::is_valid_chat_id;
|
||||
///
|
||||
/// assert!(is_valid_chat_id(ChatId::new(123)));
|
||||
/// assert!(!is_valid_chat_id(ChatId::new(0)));
|
||||
/// assert!(!is_valid_chat_id(ChatId::new(-1)));
|
||||
/// ```
|
||||
pub fn is_valid_chat_id(chat_id: ChatId) -> bool {
|
||||
chat_id.as_i64() > 0
|
||||
}
|
||||
|
||||
/// Проверяет валидность ID сообщения (не нулевой).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::types::MessageId;
|
||||
/// use tele_tui::utils::validation::is_valid_message_id;
|
||||
///
|
||||
/// assert!(is_valid_message_id(MessageId::new(456)));
|
||||
/// assert!(!is_valid_message_id(MessageId::new(0)));
|
||||
/// ```
|
||||
pub fn is_valid_message_id(message_id: MessageId) -> bool {
|
||||
message_id.as_i64() > 0
|
||||
}
|
||||
|
||||
/// Проверяет валидность ID пользователя (не нулевой).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::types::UserId;
|
||||
/// use tele_tui::utils::validation::is_valid_user_id;
|
||||
///
|
||||
/// assert!(is_valid_user_id(UserId::new(789)));
|
||||
/// assert!(!is_valid_user_id(UserId::new(0)));
|
||||
/// ```
|
||||
pub fn is_valid_user_id(user_id: UserId) -> bool {
|
||||
user_id.as_i64() > 0
|
||||
}
|
||||
|
||||
/// Проверяет, что вектор не пустой.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::utils::validation::has_items;
|
||||
///
|
||||
/// assert!(has_items(&vec![1, 2, 3]));
|
||||
/// assert!(!has_items::<i32>(&vec![]));
|
||||
/// ```
|
||||
pub fn has_items<T>(items: &[T]) -> bool {
|
||||
!items.is_empty()
|
||||
}
|
||||
|
||||
/// Комбинированная валидация текстового ввода:
|
||||
/// - Не пустой (после trim)
|
||||
/// - В пределах максимальной длины
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::utils::validation::validate_text_input;
|
||||
///
|
||||
/// assert!(validate_text_input("hello", 100).is_ok());
|
||||
/// assert!(validate_text_input("", 100).is_err());
|
||||
/// assert!(validate_text_input(" ", 100).is_err());
|
||||
/// assert!(validate_text_input("very long text", 5).is_err());
|
||||
/// ```
|
||||
pub fn validate_text_input(text: &str, max_length: usize) -> Result<(), String> {
|
||||
if !is_non_empty(text) {
|
||||
return Err("Text cannot be empty".to_string());
|
||||
}
|
||||
if !is_within_length(text, max_length) {
|
||||
return Err(format!(
|
||||
"Text exceeds maximum length of {} characters",
|
||||
max_length
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_non_empty() {
|
||||
assert!(is_non_empty("hello"));
|
||||
assert!(is_non_empty(" text "));
|
||||
assert!(!is_non_empty(""));
|
||||
assert!(!is_non_empty(" "));
|
||||
assert!(!is_non_empty("\t\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_within_length() {
|
||||
assert!(is_within_length("hello", 10));
|
||||
assert!(is_within_length("hello", 5));
|
||||
assert!(!is_within_length("hello", 4));
|
||||
assert!(is_within_length("", 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_chat_id() {
|
||||
assert!(is_valid_chat_id(ChatId::new(123)));
|
||||
assert!(is_valid_chat_id(ChatId::new(999999)));
|
||||
assert!(!is_valid_chat_id(ChatId::new(0)));
|
||||
assert!(!is_valid_chat_id(ChatId::new(-1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_message_id() {
|
||||
assert!(is_valid_message_id(MessageId::new(456)));
|
||||
assert!(!is_valid_message_id(MessageId::new(0)));
|
||||
assert!(!is_valid_message_id(MessageId::new(-1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_user_id() {
|
||||
assert!(is_valid_user_id(UserId::new(789)));
|
||||
assert!(!is_valid_user_id(UserId::new(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_items() {
|
||||
assert!(has_items(&vec![1, 2, 3]));
|
||||
assert!(has_items(&vec!["a"]));
|
||||
assert!(!has_items::<i32>(&vec![]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_text_input() {
|
||||
// Valid
|
||||
assert!(validate_text_input("hello", 100).is_ok());
|
||||
assert!(validate_text_input("test message", 20).is_ok());
|
||||
|
||||
// Empty
|
||||
assert!(validate_text_input("", 100).is_err());
|
||||
assert!(validate_text_input(" ", 100).is_err());
|
||||
|
||||
// Too long
|
||||
assert!(validate_text_input("very long text", 5).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user