refactor: create timeout/retry utilities to reduce code duplication (P1.1)
Created new utility modules to eliminate repeated timeout/retry patterns: - src/utils/retry.rs: with_timeout() and with_timeout_msg() helpers - src/utils/formatting.rs: timestamp formatting utilities (from utils.rs) - src/utils/tdlib.rs: TDLib log configuration utilities (from utils.rs) Refactored src/input/main_input.rs: - Replaced 18+ instances of timeout(Duration, op).await pattern - Simplified error handling from nested Ok(Ok(...))/Ok(Err(...))/Err(...) to cleaner Ok(...)/Err(...) with custom timeout messages - Added type annotations for compiler type inference Benefits: - Reduced code duplication from ~20 instances to 2 utility functions - Cleaner, more readable error handling - Easier to maintain timeout logic in one place - All 59 tests passing Progress: REFACTORING_OPPORTUNITIES.md #1 (Дублирование кода) - Частично выполнено Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
236
src/utils/formatting.rs
Normal file
236
src/utils/formatting.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
/// Форматирование timestamp в время HH:MM с учётом timezone offset
|
||||
/// timezone_str: строка формата "+03:00" или "-05:00"
|
||||
pub fn format_timestamp_with_tz(timestamp: i32, timezone_str: &str) -> String {
|
||||
let secs = timestamp as i64;
|
||||
|
||||
// Парсим timezone offset (например "+03:00" -> 3 часа)
|
||||
let offset_hours = parse_timezone_offset(timezone_str);
|
||||
|
||||
let hours = ((secs % 86400) / 3600) as i32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
|
||||
// Применяем timezone offset
|
||||
let local_hours = ((hours + offset_hours) % 24 + 24) % 24;
|
||||
|
||||
format!("{:02}:{:02}", local_hours, minutes)
|
||||
}
|
||||
|
||||
/// Парсит timezone строку типа "+03:00" в количество часов
|
||||
fn parse_timezone_offset(tz: &str) -> i32 {
|
||||
// Простой парсинг "+03:00" или "-05:00"
|
||||
if tz.len() >= 3 {
|
||||
let sign = if tz.starts_with('-') { -1 } else { 1 };
|
||||
let hours_str = &tz[1..3];
|
||||
if let Ok(hours) = hours_str.parse::<i32>() {
|
||||
return sign * hours;
|
||||
}
|
||||
}
|
||||
3 // fallback к MSK
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в дату для разделителя
|
||||
pub fn format_date(timestamp: i32) -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let msg_day = timestamp as i64 / 86400;
|
||||
let today = now / 86400;
|
||||
|
||||
if msg_day == today {
|
||||
"Сегодня".to_string()
|
||||
} else if msg_day == today - 1 {
|
||||
"Вчера".to_string()
|
||||
} else {
|
||||
// Простое форматирование даты
|
||||
let days_since_epoch = timestamp as i64 / 86400;
|
||||
// Приблизительный расчёт даты (без учёта високосных годов)
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 0;
|
||||
let mut day = day_of_year as i32;
|
||||
|
||||
for (i, &m) in months.iter().enumerate() {
|
||||
if day < m {
|
||||
month = i + 1;
|
||||
break;
|
||||
}
|
||||
day -= m;
|
||||
}
|
||||
|
||||
format!("{:02}.{:02}.{}", day + 1, month, year)
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить день из timestamp для группировки
|
||||
pub fn get_day(timestamp: i32) -> i64 {
|
||||
timestamp as i64 / 86400
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
|
||||
pub fn format_datetime(timestamp: i32) -> String {
|
||||
let secs = timestamp as i64;
|
||||
|
||||
// Время
|
||||
let hours = ((secs % 86400) / 3600) as u32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
let local_hours = (hours + 3) % 24; // MSK
|
||||
|
||||
// Дата
|
||||
let days_since_epoch = secs / 86400;
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 1;
|
||||
let mut day = day_of_year as i32;
|
||||
|
||||
for (i, &m) in months.iter().enumerate() {
|
||||
if day < m {
|
||||
month = i + 1;
|
||||
break;
|
||||
}
|
||||
day -= m;
|
||||
}
|
||||
|
||||
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
|
||||
}
|
||||
|
||||
/// Форматирование "был(а) онлайн" из timestamp
|
||||
pub fn format_was_online(timestamp: i32) -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
let diff = now - timestamp;
|
||||
|
||||
if diff < 60 {
|
||||
"был(а) только что".to_string()
|
||||
} else if diff < 3600 {
|
||||
let mins = diff / 60;
|
||||
format!("был(а) {} мин. назад", mins)
|
||||
} else if diff < 86400 {
|
||||
let hours = diff / 3600;
|
||||
format!("был(а) {} ч. назад", hours)
|
||||
} else {
|
||||
// Показываем дату
|
||||
let datetime = chrono::DateTime::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.format("%d.%m %H:%M").to_string())
|
||||
.unwrap_or_else(|| "давно".to_string());
|
||||
format!("был(а) {}", datetime)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_positive_offset() {
|
||||
// 2021-12-20 11:33:20 UTC (1640000000)
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// +03:00 должно дать 14:33 (11 + 3)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+03:00"), "14:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_negative_offset() {
|
||||
// 2021-12-20 11:33:20 UTC
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// -05:00 должно дать 06:33 (11 - 5)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "-05:00"), "06:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_zero_offset() {
|
||||
// 2021-12-20 11:33:20 UTC
|
||||
let timestamp = 1640000000;
|
||||
|
||||
// +00:00 должно дать UTC время 11:33
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+00:00"), "11:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_midnight_wrap() {
|
||||
// Тест перехода через полночь
|
||||
let timestamp = 82800; // 23:00 UTC (первый день эпохи)
|
||||
|
||||
// +02:00 должно дать 01:00 (следующего дня)
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "+02:00"), "01:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz_invalid_fallback() {
|
||||
let timestamp = 1640000000; // 11:33:20 UTC
|
||||
|
||||
// Невалидный timezone должен использовать fallback +03:00 -> 14:33
|
||||
assert_eq!(format_timestamp_with_tz(timestamp, "invalid"), "14:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day() {
|
||||
// Первый день эпохи (1970-01-01)
|
||||
assert_eq!(get_day(0), 0);
|
||||
|
||||
// Второй день (1970-01-02)
|
||||
assert_eq!(get_day(86400), 1);
|
||||
|
||||
// Конкретная дата: 2021-12-20 (18976 дней после эпохи)
|
||||
assert_eq!(get_day(1640000000), 18981);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day_grouping() {
|
||||
// Сообщения в один день должны иметь одинаковый day
|
||||
let msg1 = 1640000000; // 2021-12-20 09:33:20
|
||||
let msg2 = 1640040000; // 2021-12-20 20:40:00
|
||||
|
||||
assert_eq!(get_day(msg1), get_day(msg2));
|
||||
|
||||
// Сообщения в разные дни должны различаться
|
||||
let msg3 = 1640100000; // 2021-12-21 13:26:40
|
||||
|
||||
assert_ne!(get_day(msg1), get_day(msg3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_datetime() {
|
||||
// 2021-12-20 11:33:20 UTC -> с MSK (+03:00) = 14:33:20
|
||||
let timestamp = 1640000000;
|
||||
let result = format_datetime(timestamp);
|
||||
|
||||
// Проверяем что результат содержит время с MSK offset
|
||||
assert!(result.contains("14:33"), "Expected '14:33' in '{}'", result);
|
||||
// Проверяем формат (должен быть DD.MM.YYYY HH:MM)
|
||||
assert_eq!(result.chars().filter(|&c| c == '.').count(), 2);
|
||||
assert!(result.contains(":"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timezone_offset_via_format() {
|
||||
// Тестируем parse_timezone_offset через публичную функцию
|
||||
let base_timestamp = 0; // 00:00:00 UTC
|
||||
|
||||
// +03:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "+03:00"), "03:00");
|
||||
|
||||
// -05:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "-05:00"), "19:00");
|
||||
|
||||
// +12:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "+12:00"), "12:00");
|
||||
|
||||
// -11:00
|
||||
assert_eq!(format_timestamp_with_tz(base_timestamp, "-11:00"), "13:00");
|
||||
}
|
||||
}
|
||||
7
src/utils/mod.rs
Normal file
7
src/utils/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod formatting;
|
||||
pub mod retry;
|
||||
pub mod tdlib;
|
||||
|
||||
pub use formatting::*;
|
||||
pub use retry::{with_timeout, with_timeout_msg};
|
||||
pub use tdlib::*;
|
||||
140
src/utils/retry.rs
Normal file
140
src/utils/retry.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Выполняет операцию с таймаутом и возвращает результат.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - Длительность таймаута
|
||||
/// * `operation` - Асинхронная операция для выполнения
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(T)` - если операция успешна
|
||||
/// * `Err(String)` - если операция вернула ошибку или произошел таймаут
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let result = with_timeout(
|
||||
/// Duration::from_secs(5),
|
||||
/// client.load_chats(50)
|
||||
/// ).await;
|
||||
/// ```
|
||||
pub async fn with_timeout<F, T>(duration: Duration, operation: F) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
match timeout(duration, operation).await {
|
||||
Ok(Ok(value)) => Ok(value),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err("Операция превысила время ожидания".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет операцию с таймаутом и кастомным сообщением об ошибке таймаута.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - Длительность таймаута
|
||||
/// * `operation` - Асинхронная операция для выполнения
|
||||
/// * `timeout_msg` - Сообщение об ошибке при таймауте
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(T)` - если операция успешна
|
||||
/// * `Err(String)` - если операция вернула ошибку или произошел таймаут
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let result = with_timeout_msg(
|
||||
/// Duration::from_secs(5),
|
||||
/// client.load_chats(50),
|
||||
/// "Таймаут загрузки чатов"
|
||||
/// ).await;
|
||||
/// ```
|
||||
pub async fn with_timeout_msg<F, T>(
|
||||
duration: Duration,
|
||||
operation: F,
|
||||
timeout_msg: &str,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
match timeout(duration, operation).await {
|
||||
Ok(Ok(value)) => Ok(value),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(timeout_msg.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_success() {
|
||||
let result = with_timeout(Duration::from_secs(1), async {
|
||||
Ok::<_, String>("success".to_string())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "success");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_operation_error() {
|
||||
let result = with_timeout(Duration::from_secs(1), async {
|
||||
Err::<String, _>("operation failed".to_string())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "operation failed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_timeout_error() {
|
||||
let result = with_timeout(Duration::from_millis(10), async {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Ok::<_, String>("too slow".to_string())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("превысила время ожидания"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_msg_success() {
|
||||
let result = with_timeout_msg(
|
||||
Duration::from_secs(1),
|
||||
async { Ok::<_, String>("success".to_string()) },
|
||||
"Custom timeout",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "success");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_msg_timeout_error() {
|
||||
let result = with_timeout_msg(
|
||||
Duration::from_millis(10),
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Ok::<_, String>("too slow".to_string())
|
||||
},
|
||||
"Таймаут загрузки",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Таймаут загрузки");
|
||||
}
|
||||
}
|
||||
23
src/utils/tdlib.rs
Normal file
23
src/utils/tdlib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[link(name = "tdjson")]
|
||||
extern "C" {
|
||||
fn td_execute(request: *const c_char) -> *const c_char;
|
||||
}
|
||||
|
||||
/// Отключаем логи TDLib синхронно, до создания клиента
|
||||
pub fn disable_tdlib_logs() {
|
||||
let request = r#"{"@type":"setLogVerbosityLevel","new_verbosity_level":0}"#;
|
||||
let c_request = CString::new(request).unwrap();
|
||||
unsafe {
|
||||
let _ = td_execute(c_request.as_ptr());
|
||||
}
|
||||
|
||||
// Также перенаправляем логи в никуда
|
||||
let request2 = r#"{"@type":"setLogStream","log_stream":{"@type":"logStreamEmpty"}}"#;
|
||||
let c_request2 = CString::new(request2).unwrap();
|
||||
unsafe {
|
||||
let _ = td_execute(c_request2.as_ptr());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user