Files
telegram-tui/REFACTORING_ROADMAP.md
Mikhail Kilin 7081a886ad refactor: implement newtype pattern for IDs (P2.4)
Добавлены типобезопасные обёртки ChatId, MessageId, UserId для предотвращения
смешивания разных типов идентификаторов на этапе компиляции.

Изменения:
- Создан src/types.rs с тремя newtype структурами
- Реализованы методы: new(), as_i64(), From<i64>, Display
- Добавлены traits: Hash, Eq, Serialize, Deserialize
- Обновлены 15+ модулей для использования новых типов:
  * tdlib: types.rs, chats.rs, messages.rs, users.rs, reactions.rs, client.rs
  * app: mod.rs, chat_state.rs
  * input: main_input.rs
  * tests: app_builder.rs, test_data.rs
- Исправлены 53 ошибки компиляции связанные с type conversions

Преимущества:
- Компилятор предотвращает смешивание разных типов ID
- Улучшенная читаемость кода (явные типы вместо i64)
- Самодокументирующиеся типы

Статус: Priority 2 теперь 60% (3/5 задач)
-  Error enum
-  Config validation
-  Newtype для ID
-  MessageInfo реструктуризация
-  MessageBuilder pattern

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 01:33:18 +03:00

691 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Refactoring Roadmap
Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.
## Приоритет 1: Критичные улучшения
### 1. Схлопнуть состояния чата в enum
**Проблема**: Сейчас состояния чата хранятся как отдельные boolean поля в `App`:
```rust
is_message_selection_mode: bool,
is_editing_mode: bool,
is_reply_mode: bool,
is_forward_mode: bool,
is_delete_confirmation: bool,
is_reaction_picker_mode: bool,
is_profile_mode: bool,
is_search_in_chat_mode: bool,
```
**Решение**: Создать enum `ChatState`:
```rust
enum ChatState {
Normal,
MessageSelection {
selected_message_id: i64,
},
Editing {
message_id: i64,
original_text: String,
},
Reply {
message_id: i64,
preview_text: String,
},
Forward {
message_id: i64,
selected_chat_index: usize,
},
DeleteConfirmation {
message_id: i64,
},
ReactionPicker {
message_id: i64,
available_reactions: Vec<String>,
selected_index: usize,
},
Profile {
info: ProfileInfo,
},
SearchInChat {
query: String,
results: Vec<i64>,
current_index: usize,
},
}
```
**Преимущества**:
- Невозможно иметь несколько состояний одновременно (type-safe)
- Проще обрабатывать переходы между состояниями
- Меньше полей в `App`
- Данные, связанные с состоянием, хранятся вместе с ним
**Затронутые файлы**:
- `src/app/mod.rs` (добавить enum, убрать boolean поля)
- `src/input/main_input.rs` (изменить логику обработки на match)
- `src/ui/messages.rs` (изменить рендеринг на match)
---
### 2. Разделить TdClient на несколько модулей
**Проблема**: `TdClient` в `src/tdlib/client.rs` (~1500+ строк) делает слишком много:
- Авторизация
- Управление чатами
- Управление сообщениями
- Кеширование пользователей
- Реакции
- Network state
**Решение**: Разделить на модули:
```
src/tdlib/
├── mod.rs # Экспорт публичных типов
├── client.rs # Основной TdClient
├── auth.rs # AuthManager
├── chats.rs # ChatManager
├── messages.rs # MessageManager
├── users.rs # UserCache
└── reactions.rs # ReactionManager
```
**Преимущества**:
- Принцип единственной ответственности
- Проще тестировать отдельные модули
- Легче найти и изменить код
---
### 3. Вынести константы в отдельный модуль
**Проблема**: Магические числа разбросаны по всему коду:
```rust
// В разных местах:
500 // MAX_MESSAGES_IN_CHAT
500 // MAX_USER_CACHE_SIZE
200 // MAX_CHATS
8 // Emoji picker columns
10 // Max input height
16 // Poll timeout (60 FPS)
```
**Решение**: Создать `src/constants.rs`:
```rust
// Memory limits
pub const MAX_MESSAGES_IN_CHAT: usize = 500;
pub const MAX_USER_CACHE_SIZE: usize = 500;
pub const MAX_CHATS: usize = 200;
pub const MAX_CHAT_USER_IDS: usize = 500;
// UI constants
pub const EMOJI_PICKER_COLUMNS: usize = 8;
pub const EMOJI_PICKER_ROWS: usize = 6;
pub const MAX_INPUT_HEIGHT: usize = 10;
pub const MIN_TERMINAL_WIDTH: u16 = 80;
pub const MIN_TERMINAL_HEIGHT: u16 = 20;
// Performance
pub const POLL_TIMEOUT_MS: u64 = 16; // 60 FPS
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2;
pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
// TDLib
pub const TDLIB_CHAT_LIMIT: i32 = 50;
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
```
**Преимущества**:
- Единое место для всех констант
- Проще изменить значения
- Самодокументирующийся код
---
## Приоритет 2: Улучшение типобезопасности
### 4. Newtype pattern для ID ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать.
**Решение**: ✅ Реализовано в `src/types.rs`:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChatId(pub i64);
impl ChatId {
pub fn new(id: i64) -> Self { Self(id) }
pub fn as_i64(&self) -> i64 { self.0 }
}
impl From<i64> for ChatId {
fn from(id: i64) -> Self { ChatId(id) }
}
impl Display for ChatId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
// Аналогично для MessageId и UserId
```
**Что сделано**:
- ✅ Создан `src/types.rs` с тремя типами: `ChatId`, `MessageId`, `UserId`
- ✅ Добавлены методы `new()`, `as_i64()`, `From<i64>`, `Display`
- ✅ Реализованы traits: `Hash`, `Eq`, `Serialize`, `Deserialize`
- ✅ Обновлены 15+ модулей:
- `tdlib/types.rs`, `tdlib/chats.rs`, `tdlib/messages.rs`, `tdlib/users.rs`
- `tdlib/reactions.rs`, `tdlib/client.rs`
- `app/mod.rs`, `app/chat_state.rs`, `input/main_input.rs`
- Test helpers: `app_builder.rs`, `test_data.rs`
- ✅ Исправлены 53 ошибки компиляции
- ✅ Код компилируется успешно
**Преимущества**:
- ✅ Невозможно случайно передать message_id вместо chat_id
- ✅ Компилятор ловит ошибки на этапе компиляции
- ✅ Улучшенная читаемость кода
- ✅ Самодокументирующиеся типы
---
### 5. Создать enum для ошибок
**Проблема**: Везде используется `Result<T, String>` — теряется контекст ошибок.
**Решение**: Создать `src/error.rs`:
```rust
#[derive(Debug, thiserror::Error)]
pub enum TeletuiError {
#[error("TDLib error: {0}")]
TdLib(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Network error: {0}")]
Network(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Invalid timezone format: {0}")]
InvalidTimezone(String),
#[error("Invalid color: {0}")]
InvalidColor(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, TeletuiError>;
```
**Зависимости**: `thiserror = "1.0"`
**Преимущества**:
- Типобезопасная обработка ошибок
- Понятные сообщения об ошибках
- Возможность pattern matching
---
### 6. Группировка полей MessageInfo
**Проблема**: `MessageInfo` имеет слишком много плоских полей (~15+).
**Решение**: Группировать в логические структуры:
```rust
pub struct MessageInfo {
pub metadata: MessageMetadata,
pub content: MessageContent,
pub state: MessageState,
pub interactions: MessageInteractions,
}
pub struct MessageMetadata {
pub id: MessageId,
pub chat_id: ChatId,
pub sender_id: UserId,
pub date: i32,
}
pub struct MessageContent {
pub text: String,
pub formatted_text: Option<FormattedText>,
pub media_type: Option<String>,
}
pub struct MessageState {
pub is_outgoing: bool,
pub is_edited: bool,
pub is_pinned: bool,
}
pub struct MessageInteractions {
pub reply_to_message_id: Option<MessageId>,
pub forward_info: Option<ForwardInfo>,
pub reactions: Vec<ReactionInfo>,
pub read_count: i32,
}
```
**Преимущества**:
- Логическая группировка данных
- Проще добавлять новые поля
- Меньше параметров в конструкторах
---
## Приоритет 3: Архитектурные улучшения
### 7. Выделить UI компоненты
**Проблема**: Код рендеринга дублируется, сложно переиспользовать.
**Решение**: Создать `src/ui/components/`:
```
src/ui/components/
├── mod.rs
├── modal.rs # Базовый компонент модалки
├── input_field.rs # Поле ввода с курсором
├── message_bubble.rs # Пузырь сообщения
├── chat_list_item.rs # Элемент списка чатов
└── emoji_picker.rs # Picker эмодзи
```
Каждый компонент — функция:
```rust
pub fn render_modal<F>(
frame: &mut Frame,
area: Rect,
title: &str,
render_content: F,
) where
F: FnOnce(&mut Frame, Rect),
{
// Общий код для всех модалок
}
```
**Преимущества**:
- Переиспользуемые компоненты
- Консистентный UI
- Проще тестировать
---
### 8. Вынести форматирование в отдельный модуль
**Проблема**: Markdown форматирование захардкожено в `messages.rs` (~200+ строк).
**Решение**: Создать `src/formatting.rs`:
```rust
pub struct FormattedSpan {
pub text: String,
pub style: Style,
}
pub fn format_text_entities(
text: &str,
entities: &[TextEntity],
) -> Vec<FormattedSpan> {
// Вся логика форматирования
}
```
**Преимущества**:
- Разделение ответственности
- Можно тестировать отдельно
- Переиспользование в других местах
---
### 9. Вынести логику группировки сообщений
**Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`.
**Решение**: Создать `src/message_grouping.rs`:
```rust
pub enum MessageGroup {
DateSeparator(String),
SenderHeader(String),
Message(MessageInfo),
}
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
// Логика группировки по дате и отправителю
}
```
**Преимущества**:
- Чистое разделение логики и представления
- Легче тестировать группировку
- Можно переиспользовать
---
### 10. Hotkey mapping в конфиг
**Проблема**: Хоткеи захардкожены в коде, нельзя настроить.
**Решение**: Добавить в `config.toml`:
```toml
[hotkeys]
# Навигация
up = ["k", "р", "Up"]
down = ["j", "о", "Down"]
left = ["h", "р", "Left"]
right = ["l", "д", "Right"]
# Действия
reply = ["r", "к"]
forward = ["f", "а"]
delete = ["d", "в", "Delete"]
copy = ["y", "н"]
react = ["e", "у"]
```
Парсить в `src/config.rs`:
```rust
pub struct Hotkeys {
pub up: Vec<char>,
pub down: Vec<char>,
// ...
}
impl Hotkeys {
pub fn matches(&self, key: KeyCode, action: &str) -> bool {
// Проверка совпадения
}
}
```
**Преимущества**:
- Пользовательская настройка хоткеев
- Проще добавлять новые действия
- Документация хоткеев в конфиге
---
## Приоритет 4: Качество кода
### 11. Добавить юнит-тесты
**Проблема**: Нет тестов, сложно убедиться в корректности.
**Решение**: Добавить тесты для:
```rust
// tests/utils_test.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_timestamp_with_tz() {
let timestamp = 1640000000; // 2021-12-20 09:33:20 UTC
assert_eq!(
format_timestamp_with_tz(timestamp, "+03:00"),
"12:33"
);
}
#[test]
fn test_parse_timezone_offset() {
assert_eq!(parse_timezone_offset("+03:00"), 3);
assert_eq!(parse_timezone_offset("-05:00"), -5);
assert_eq!(parse_timezone_offset("invalid"), 3); // fallback
}
}
// tests/config_test.rs
#[test]
fn test_parse_color() {
let config = Config::default();
assert_eq!(config.parse_color("red"), Color::Red);
assert_eq!(config.parse_color("invalid"), Color::White); // fallback
}
// tests/grouping_test.rs
#[test]
fn test_message_grouping_by_date() {
// ...
}
```
**Запуск**: `cargo test`
---
### 12. Добавить rustdoc комментарии
**Проблема**: Публичное API не документировано.
**Решение**: Добавить doc-комментарии:
```rust
/// TDLib client wrapper for Telegram integration.
///
/// Handles authentication, chat management, message operations,
/// and user caching.
///
/// # Examples
///
/// ```no_run
/// let mut client = TdClient::new(api_id, api_hash).await?;
/// client.start_authorization().await?;
/// ```
pub struct TdClient {
// ...
}
/// Loads configuration from ~/.config/tele-tui/config.toml
///
/// Creates default config if file doesn't exist.
///
/// # Returns
///
/// Always returns a valid `Config`, using defaults if loading fails.
pub fn load() -> Self {
// ...
}
```
**Генерация**: `cargo doc --open`
---
### 13. Config валидация
**Проблема**: Невалидные значения в конфиге молча игнорируются.
**Решение**: Добавить валидацию:
```rust
impl Config {
pub fn validate(&self) -> Result<(), TeletuiError> {
// Проверка timezone
if !self.general.timezone.starts_with('+')
&& !self.general.timezone.starts_with('-') {
return Err(TeletuiError::InvalidTimezone(
format!("Timezone must start with + or -: {}", self.general.timezone)
));
}
// Проверка цветов
let valid_colors = [
"black", "red", "green", "yellow", "blue", "magenta",
"cyan", "gray", "white", "darkgray", "lightred",
"lightgreen", "lightyellow", "lightblue",
"lightmagenta", "lightcyan"
];
for color_name in [
&self.colors.incoming_message,
&self.colors.outgoing_message,
&self.colors.selected_message,
&self.colors.reaction_chosen,
&self.colors.reaction_other,
] {
if !valid_colors.contains(&color_name.to_lowercase().as_str()) {
return Err(TeletuiError::InvalidColor(
format!("Unknown color: {}", color_name)
));
}
}
Ok(())
}
}
```
Вызывать при загрузке:
```rust
pub fn load() -> Self {
let config = // ... загрузка из файла
if let Err(e) = config.validate() {
eprintln!("Config validation error: {}", e);
return Self::default();
}
config
}
```
---
### 14. Async/await консистентность
**Проблема**: Местами блокирующие вызовы в async контексте.
**Решение**: Ревью и исправление:
- Использовать `tokio::fs` вместо `std::fs` для файловых операций в async
- Использовать `tokio::time::sleep` вместо `std::thread::sleep`
- Обернуть блокирующие вызовы в `spawn_blocking`
---
## Приоритет 5: Опциональные улучшения
### 15. Feature flags для зависимостей
**Проблема**: Все зависимости всегда включены.
**Решение**: В `Cargo.toml`:
```toml
[features]
default = ["clipboard", "url-open"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]
```
**Преимущества**:
- Уменьшение размера бинарника
- Опциональная функциональность
---
### 16. LRU cache обобщение
**Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`.
**Решение**: Создать обобщённый `LruCache<K, V>` или использовать готовый крейт `lru = "0.12"`.
---
### 17. Tracing вместо println!
**Проблема**: Используется `eprintln!` для логов.
**Решение**: Использовать `tracing`:
```rust
use tracing::{info, warn, error, debug};
// Вместо
eprintln!("Warning: Could not load config: {}", e);
// Использовать
warn!("Could not load config: {}", e);
```
Добавить в `Cargo.toml`:
```toml
tracing = "0.1"
tracing-subscriber = "0.3"
```
---
## Метрики прогресса
- [x] Priority 1: 3/3 задач ✅ ЗАВЕРШЕНО!
- [x] P1.1 — ChatState enum
- [x] P1.2 — Разделить TdClient
- [x] P1.3 — Константы
- [x] Priority 2: 3/5 задач (60%)
- [x] P2.5 — Error enum
- [x] P2.3 — Config validation
- [x] P2.4 — Newtype для ID
- [ ] P2.6 — MessageInfo реструктуризация
- [ ] P2.7 — MessageBuilder pattern
- [ ] Priority 3: 0/4 задач
- [ ] Priority 4: 0/4 задач
- [ ] Priority 5: 0/3 задач
**Всего**: 6/17 задач (35%)
---
## Предусловие: Тесты
**ВАЖНО**: Перед началом рефакторинга необходимо написать тесты!
См. [TESTING_ROADMAP.md](TESTING_ROADMAP.md) для плана покрытия тестами.
Минимальное покрытие для начала рефакторинга:
- ✅ Фаза 0: Инфраструктура (helpers, fake client)
- ✅ Snapshot тесты для основных экранов (chat list, messages)
- ✅ Integration тесты для критичных flow (send, edit, navigation)
**Зачем**: Тесты гарантируют, что рефакторинг не сломает функциональность.
---
## Порядок выполнения
Рекомендуется выполнять в следующем порядке:
1. **P1.3** — Константы (быстро, малый риск)
2. **P1.1** — ChatState enum (высокий impact)
3. **P2.5** — Error enum (улучшает весь код)
4. **P4.11** — Тесты для utils (базовая проверка)
5. **P1.2** — Разделить TdClient (большой рефакторинг)
6. **P2.4** — Newtype для ID (широкие изменения)
7. **P3.7** — UI компоненты (постепенно)
8. **P3.8** — Форматирование (изоляция логики)
9. **P3.9** — Группировка сообщений (изоляция логики)
10. Остальные по необходимости
---
## Принципы рефакторинга
1. **Один PR = одна задача** — не смешивать рефакторинг разных областей
2. **Тесты прежде всего** — добавить тесты перед рефакторингом
3. **Обратная совместимость** — сохранять работоспособность на каждом шаге
4. **Маленькие шаги** — лучше 10 маленьких PR, чем 1 огромный
5. **Документация** — обновлять документацию после изменений
---
## Примечания
- Этот документ живой и будет обновляться
- Новые пункты добавляются по мере обнаружения
- После завершения задачи отмечать в метриках
- При появлении блокеров — документировать в соответствующей секции