refactor: implement trait-based DI for TdClient and fix stack overflow

Implement complete trait-based dependency injection pattern for TdClient
to enable testing with FakeTdClient mock. Fix critical stack overflow bugs
caused by infinite recursion in trait implementations.

Breaking Changes:
- App is now generic: App<T: TdClientTrait = TdClient>
- All UI and input handlers are generic over TdClientTrait
- TdClient methods now accessed through trait interface

New Files:
- src/tdlib/trait.rs: TdClientTrait definition with 40+ methods
- src/tdlib/client_impl.rs: TdClientTrait impl for TdClient
- tests/helpers/fake_tdclient_impl.rs: TdClientTrait impl for FakeTdClient

Critical Fixes:
- Fix stack overflow in send_message, edit_message, delete_messages
- Fix stack overflow in forward_messages, current_chat_messages
- Fix stack overflow in current_pinned_message
- All methods now call message_manager directly to avoid recursion

Testing:
- FakeTdClient supports configurable auth_state for auth screen tests
- Added pinned message support in FakeTdClient
- All 196+ tests passing (188 tests + 8 benchmarks)

Dependencies:
- Added async-trait = "0.1"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-02 05:42:19 +03:00
parent ed5a4f9c72
commit 8e48d076de
38 changed files with 1053 additions and 161 deletions

View File

@@ -4,7 +4,7 @@ mod state;
pub use chat_state::ChatState;
pub use state::AppScreen;
use crate::tdlib::{ChatInfo, TdClient};
use crate::tdlib::{ChatInfo, TdClient, TdClientTrait};
use crate::types::{ChatId, MessageId};
use ratatui::widgets::ListState;
@@ -43,11 +43,11 @@ use ratatui::widgets::ListState;
/// // Open a chat
/// app.select_current_chat();
/// ```
pub struct App {
pub struct App<T: TdClientTrait = TdClient> {
// Core (config - readonly через getter)
config: crate::config::Config,
pub screen: AppScreen,
pub td_client: TdClient,
pub td_client: T,
/// Состояние чата - type-safe state machine (новое!)
pub chat_state: ChatState,
// Auth state (используются часто в UI)
@@ -77,27 +77,27 @@ pub struct App {
pub last_typing_sent: Option<std::time::Instant>,
}
impl App {
/// Creates a new App instance with the given configuration.
impl<T: TdClientTrait> App<T> {
/// Creates a new App instance with the given configuration and client.
///
/// Initializes TDLib client, sets up empty chat list, and configures
/// the app to start on the Loading screen.
/// Sets up empty chat list and configures the app to start on the Loading screen.
///
/// # Arguments
///
/// * `config` - Application configuration loaded from config.toml
/// * `td_client` - TDLib client instance (real or fake for tests)
///
/// # Returns
///
/// A new `App` instance ready to start authentication.
pub fn new(config: crate::config::Config) -> App {
pub fn with_client(config: crate::config::Config, td_client: T) -> App<T> {
let mut state = ListState::default();
state.select(Some(0));
App {
config,
screen: AppScreen::Loading,
td_client: TdClient::new(),
td_client,
chat_state: ChatState::Normal,
phone_input: String::new(),
code_input: String::new(),
@@ -174,7 +174,7 @@ impl App {
self.chat_state = ChatState::Normal;
// Очищаем данные в TdClient
self.td_client.set_current_chat_id(None);
self.td_client.current_chat_messages_mut().clear();
self.td_client.clear_current_chat_messages();
self.td_client.set_typing_status(None);
self.td_client.set_current_pinned_message(None);
}
@@ -215,9 +215,9 @@ impl App {
}
/// Получить выбранное сообщение
pub fn get_selected_message(&self) -> Option<&crate::tdlib::MessageInfo> {
pub fn get_selected_message(&self) -> Option<crate::tdlib::MessageInfo> {
self.chat_state.selected_message_index().and_then(|idx| {
self.td_client.current_chat_messages().get(idx)
self.td_client.current_chat_messages().get(idx).cloned()
})
}
@@ -397,12 +397,13 @@ impl App {
}
/// Получить сообщение, на которое отвечаем
pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::MessageInfo> {
pub fn get_replying_to_message(&self) -> Option<crate::tdlib::MessageInfo> {
self.chat_state.selected_message_id().and_then(|id| {
self.td_client
.current_chat_messages()
.iter()
.find(|m| m.id() == id)
.cloned()
})
}
@@ -431,7 +432,7 @@ impl App {
}
/// Получить сообщение для пересылки
pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::MessageInfo> {
pub fn get_forwarding_message(&self) -> Option<crate::tdlib::MessageInfo> {
if !self.chat_state.is_forward() {
return None;
}
@@ -440,6 +441,7 @@ impl App {
.current_chat_messages()
.iter()
.find(|m| m.id() == id)
.cloned()
})
}
@@ -991,3 +993,22 @@ impl App {
self.last_typing_sent = Some(std::time::Instant::now());
}
}
// Convenience constructor for real TdClient (production use)
impl App<TdClient> {
/// Creates a new App instance with the given configuration and a real TDLib client.
///
/// This is a convenience method for production use that automatically creates
/// a new TdClient instance.
///
/// # Arguments
///
/// * `config` - Application configuration loaded from config.toml
///
/// # Returns
///
/// A new `App<TdClient>` instance ready to start authentication.
pub fn new(config: crate::config::Config) -> App<TdClient> {
App::with_client(config, TdClient::new())
}
}