refactor: modular architecture with unified error handling and DashMap concurrency

- Break monolithic records.rs into commands/, output/, parsing/ modules
- Add lib.rs with shared types (AppError, Car, BestCarRecord, etc.)
- Replace Arc<Mutex<HashMap>> with DashMap for concurrent access
- Add unified AppError enum with thiserror
- Add configurable CLI paths (--my-cars, --cars-db, --times-dir, --tracks-db)
- Add serde deserialization for Car struct
- Add CSV escaping for fields with commas/quotes/newlines
- Fix track name splitting: split_once -> rsplit_once for _a handling
- Add integration tests, proptest property-based tests, and unit tests
- Add rustfmt.toml configuration
- Update documentation (API, ARCHITECTURE, DATA_STRUCTURES, README)
This commit is contained in:
Jan-Peter von Hunnius 2026-05-08 16:19:01 +02:00
parent 9d57b18a4e
commit a9a46ee551
24 changed files with 3541 additions and 1470 deletions

63
src/commands/mod.rs Normal file
View file

@ -0,0 +1,63 @@
//! Command dispatch and shared helpers.
//!
//! Provides the `run_analysis` orchestration function and `load_my_cars_or_warn`
//! helper used by all command handlers.
pub mod all;
pub mod best;
pub mod details;
pub mod fetch;
pub mod track;
pub mod trash;
pub mod wishlist;
use std::collections::HashSet;
use std::sync::Arc;
use top_drives::{
AppError, AnalysisContext, CarCounts, MyCarsData,
compute_car_counts, load_my_cars_data,
};
use crate::parsing::parse_all_times;
// ---------------------------------------------------------------------------
// Helper: load my_cars.json with graceful degradation
// ---------------------------------------------------------------------------
/// Loads user car data, printing a warning and returning empty data if the file is missing.
///
/// Only `MissingFile` errors are gracefully degraded — parse errors and other
/// failures are propagated to the caller, because running analysis with
/// silently-corrupted ownership data would produce misleading results.
pub fn load_my_cars_or_warn(path: &str) -> Result<MyCarsData, AppError> {
match load_my_cars_data(path) {
Ok(data) => Ok(data),
Err(AppError::MissingFile(msg)) => {
eprintln!("Warning: {msg}");
eprintln!(" Analysis will run without ownership data.");
Ok(MyCarsData::default())
}
Err(e) => Err(e),
}
}
// ---------------------------------------------------------------------------
// Analysis orchestration
// ---------------------------------------------------------------------------
/// Common analysis setup: loads user data, parses all timing files, and computes car counts.
///
/// Returns the analysis context (for stdout commands) and the aggregated car counts
/// (for CSV output commands). The car list is stored inside `CarCounts`.
pub async fn run_analysis(
owned: &HashSet<String>,
cars_db_path: &str,
times_dir: &str,
) -> Result<(AnalysisContext, CarCounts), AppError> {
let ctx = AnalysisContext::new();
let owned = Arc::new(owned.clone());
let car_list = parse_all_times(owned, &ctx, cars_db_path, times_dir).await?;
let counts = compute_car_counts(&ctx, car_list)?;
Ok((ctx, counts))
}