Replace manual CLI parsing with clap derive

- Add clap dependency with derive feature for structured CLI argument parsing
- Replace env::args() manual matching with clap subcommands (fetch, best, missing, wishlist, trash, details, track)
- Change print_best_car() signature from (&String, &String, &String) to (&str, &str, u8) for idiomatic Rust
- Add TODO.md tracking known tech debt and planned features
- Update CHANGELOG.md with clap migration details and set v0.2.0 release date
This commit is contained in:
Jan-Peter von Hunnius 2026-05-07 12:27:11 +02:00
parent 89fa69e3c8
commit 08c96d9981
5 changed files with 227 additions and 99 deletions

View file

@ -28,10 +28,10 @@
//! - `futures`: Future utilities for handling async operations
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::sync::Arc;
use clap::{Parser, Subcommand};
use tokio::sync::Semaphore;
use tokio::task;
@ -48,22 +48,45 @@ use top_drives::{
/// Used as the upper bound when iterating RQ ranges and as a fallback for parse errors.
const MAX_RQ: u8 = 113;
/// Command-line interface for the Top Drives Records Analyzer.
#[derive(Parser)]
#[command(name = "records", about = "Top Drives Records Analyzer", version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
/// Available subcommands for car performance analysis.
#[derive(Subcommand)]
enum Commands {
/// Download latest car and track data from repository
Fetch,
/// Show best performing cars in your collection
Best,
/// Show best cars not owned by you (prefix + if not on wishlist)
Missing,
/// Show wishlist cars that are no longer among the best
Wishlist,
/// Identify cars that can be safely removed from collection
Trash,
/// Show detailed timing data for all tracks
Details,
/// Show best car for a specific track/condition/RQ combination
Track {
/// Track name (e.g. "Laguna Seca")
track: String,
/// Track condition (e.g. "Dry", "Wet")
condition: String,
/// RQ limit (e.g. 85)
rq: u8,
},
}
/// Main entry point for the Top Drives Records Analyzer
///
/// Parses command line arguments and executes the requested analysis operation.
/// Parses command line arguments using `clap` and executes the requested analysis operation.
/// Supports multiple subcommands for different types of car performance analysis.
///
/// # Arguments
///
/// The first argument specifies the operation to perform:
/// - `fetch`: Download latest car and track data from repository
/// - `best`: Show best performing cars in user's collection
/// - `missing`: Show best cars not owned by the user (prefix `+` if not on wishlist)
/// - `trash`: Identify cars that can be safely removed from collection
/// - `wishlist`: Show wishlist cars that are no longer among the best
/// - `details`: Show detailed timing data for all tracks
/// - `track`: Show best car for specific track/condition/RQ combination
///
/// # Example Usage
///
/// ```bash
@ -72,85 +95,63 @@ const MAX_RQ: u8 = 113;
/// ```
#[tokio::main]
async fn main() {
let args: Vec<_> = env::args().collect();
let cli = Cli::parse();
match args.get(1) {
None => {
println!("Please provide a command.")
match cli.command {
Commands::Fetch => {
// Download phase: Get fresh data from all sources
// This is the foundation for all other operations
get_cars_file().await;
get_tracks_file().await;
fetch_times().await;
}
Some(argument) => {
let command = argument.as_str();
match command {
"fetch" => {
// Download phase: Get fresh data from all sources
// This is the foundation for all other operations
get_cars_file().await;
get_tracks_file().await;
fetch_times().await;
}
"best" => {
// Standard best cars analysis
// Shows cars you own that perform well across tracks
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let _car_list = parse_all_times(&my_data.owned, &ctx).await;
print_best_cars(&ctx, false, &my_data.wishlist); // false = include owned cars
}
"missing" => {
// Missing cars analysis
// Shows high-performing cars you don't own
// Cars not yet on the wishlist are prefixed with "+"
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let _car_list = parse_all_times(&my_data.owned, &ctx).await;
print_best_cars(&ctx, true, &my_data.wishlist); // true = filter out owned cars
}
"wishlist" => {
// Wishlist review
// Shows cars on the wishlist that are no longer among the best
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let car_list = parse_all_times(&my_data.owned, &ctx).await;
print_wishlist_stale(&ctx, &my_data.wishlist, &car_list);
}
"trash" => {
// Collection optimization
// Identifies cars that can be safely removed
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let car_list = parse_all_times(&my_data.owned, &ctx).await;
print_trash(&ctx, &car_list, &my_data.owned);
}
"details" => {
// Comprehensive data export
// Outputs all timing data for external analysis
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
parse_all_times(&my_data.owned, &ctx).await;
print_best_times(&ctx);
}
"track" => {
// Track-specific analysis
// Requires exactly 3 additional arguments
if args.get(4).is_none() {
println!("Track name, condition and RQ limit required.");
} else {
// Safe to unwrap here due to above check
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
parse_all_times(&my_data.owned, &ctx).await;
let track = args.get(2).unwrap();
let condition = args.get(3).unwrap();
let rq = args.get(4).unwrap();
print_best_car(&ctx, track, condition, rq);
}
}
&_ => {
// Unknown command handler
// Provides feedback for typos or outdated commands
println!("Unknown command '{}'.", argument)
}
}
Commands::Best => {
// Standard best cars analysis
// Shows cars you own that perform well across tracks
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let _car_list = parse_all_times(&my_data.owned, &ctx).await;
print_best_cars(&ctx, false, &my_data.wishlist); // false = include owned cars
}
Commands::Missing => {
// Missing cars analysis
// Shows high-performing cars you don't own
// Cars not yet on the wishlist are prefixed with "+"
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let _car_list = parse_all_times(&my_data.owned, &ctx).await;
print_best_cars(&ctx, true, &my_data.wishlist); // true = filter out owned cars
}
Commands::Wishlist => {
// Wishlist review
// Shows cars on the wishlist that are no longer among the best
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let car_list = parse_all_times(&my_data.owned, &ctx).await;
print_wishlist_stale(&ctx, &my_data.wishlist, &car_list);
}
Commands::Trash => {
// Collection optimization
// Identifies cars that can be safely removed
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
let car_list = parse_all_times(&my_data.owned, &ctx).await;
print_trash(&ctx, &car_list, &my_data.owned);
}
Commands::Details => {
// Comprehensive data export
// Outputs all timing data for external analysis
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
parse_all_times(&my_data.owned, &ctx).await;
print_best_times(&ctx);
}
Commands::Track { track, condition, rq } => {
// Track-specific analysis
let my_data = load_my_cars_data();
let ctx = AnalysisContext::new();
parse_all_times(&my_data.owned, &ctx).await;
print_best_car(&ctx, &track, &condition, rq);
}
}
}
@ -392,12 +393,12 @@ fn print_best_cars(ctx: &AnalysisContext, filter_out_owned: bool, wishlist: &Has
/// # Note
/// If no car is found for the specified combination, it may indicate that no data
/// is available for that specific track/condition/RQ combination.
fn print_best_car(ctx: &AnalysisContext, track: &String, condition: &String, rq: &String) {
fn print_best_car(ctx: &AnalysisContext, track: &str, condition: &str, rq: u8) {
let best_cars = ctx.best_cars.lock().unwrap();
let key = (
track.clone(),
rq.parse::<u8>().unwrap_or(MAX_RQ), // Default to max RQ if parsing fails
condition.clone(),
track.to_string(),
rq,
condition.to_string(),
);
if let Some(best_list) = best_cars.get(&key) {