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:
parent
89fa69e3c8
commit
08c96d9981
5 changed files with 227 additions and 99 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -15,17 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Data structures documentation for JSON formats
|
||||
- Development setup guide with dependency explanations
|
||||
- `track_names.json` — external config file mapping track codes to human-readable names (loaded at compile time via `include_str!`)
|
||||
- `full_script` command for running fetch + all analysis commands in sequence
|
||||
- `clap` dependency with derive feature for structured CLI argument parsing
|
||||
- Daily workflow documentation with output file descriptions (`best.csv`, `details.csv`, `add.csv`, `remove.csv`, `trash.csv`, `sorted.csv`, `very_best.csv`, `wishlist.csv`)
|
||||
- Test suite covering `update_best_car` logic, RQ range analysis, time validation, track name completeness, and condition completeness
|
||||
|
||||
### Changed
|
||||
- `translate_track_name()` in `lib.rs` now loads track name mappings from `track_names.json` instead of a hardcoded match statement
|
||||
- Magic number 115 replaced with `const MAX_RQ: u8 = 113` in `records.rs` for clarity and maintainability
|
||||
- `parse_all_times()` now uses a semaphore (limit 20) for bounded concurrency instead of unbounded task spawning
|
||||
- Replaced manual `env::args()` CLI parsing with `clap` derive for proper `--help`, `--version`, and argument validation
|
||||
- `print_best_car()` signature changed from `(&String, &String, &String)` to `(&str, &str, u8)` for idiomatic Rust and compile-time RQ validation
|
||||
- Empty test bodies removed from `records.rs`
|
||||
- Enhanced error handling throughout the application
|
||||
- Better progress reporting during data fetching
|
||||
- More descriptive command-line help text
|
||||
- Improved CSV output formatting
|
||||
|
||||
### Removed
|
||||
|
|
@ -34,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **tracks.txt**: Removed track definitions file (only used by club binary)
|
||||
- Club module `Car` struct (separate struct with `class`, `upgrade` fields)
|
||||
|
||||
## [0.2.0] - 2024-12-XX
|
||||
## [0.2.0] - 2026-05-07
|
||||
|
||||
### Added
|
||||
- **Track-Specific Analysis**: New `track` command for targeted recommendations
|
||||
|
|
@ -213,4 +214,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
**Changelog Format**: Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format
|
||||
**Versioning**: Follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) principles
|
||||
**Last Updated**: 2026-05-06
|
||||
**Last Updated**: 2026-05-07
|
||||
|
|
|
|||
121
Cargo.lock
generated
121
Cargo.lock
generated
|
|
@ -2,6 +2,56 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
|
|
@ -54,6 +104,52 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
|
@ -559,6 +655,12 @@ version = "2.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
|
|
@ -662,6 +764,12 @@ version = "1.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.79"
|
||||
|
|
@ -1052,6 +1160,12 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -1198,6 +1312,7 @@ dependencies = [
|
|||
name = "top_drives"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
|
|
@ -1311,6 +1426,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ reqwest = "0.12.15"
|
|||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
serde_json = "1.0.140"
|
||||
futures = "0.3.31"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
|
|
|
|||
4
TODO.md
Normal file
4
TODO.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
1) **Move shell post-processing into Rust** — `full_script` does `sort`, `grep`, file splitting via shell. Add native subcommands (`very-best`, `add`, `remove`) or CLI flags (`--min-count`, `--sort-by`) so the binary produces the final files directly. Modify the script to export to files, nothing else.
|
||||
2) **Correct `+` prefix semantics** — `+` means "not on wishlist". It is not needed in `best`/`missing` output but is used as "add to wishlist". Since we incorporated the wishlist actions into the application, the prefix can go.
|
||||
3) **Add `all` subcommand** — `full_script` runs 5 commands sequentially, each parsing all timing files independently. A single `all` command would parse once and write all outputs, cutting runtime ~80%.
|
||||
4) **Document the daily workflow** — README and USER_GUIDE don't explain `full_script` or the relationship between Rust output and the files (`sorted.csv`, `very_best.csv`, `wishlist.csv`, `add.csv`).
|
||||
|
|
@ -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,23 +95,17 @@ 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.")
|
||||
}
|
||||
Some(argument) => {
|
||||
let command = argument.as_str();
|
||||
match command {
|
||||
"fetch" => {
|
||||
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;
|
||||
}
|
||||
"best" => {
|
||||
Commands::Best => {
|
||||
// Standard best cars analysis
|
||||
// Shows cars you own that perform well across tracks
|
||||
let my_data = load_my_cars_data();
|
||||
|
|
@ -96,7 +113,7 @@ async fn main() {
|
|||
let _car_list = parse_all_times(&my_data.owned, &ctx).await;
|
||||
print_best_cars(&ctx, false, &my_data.wishlist); // false = include owned cars
|
||||
}
|
||||
"missing" => {
|
||||
Commands::Missing => {
|
||||
// Missing cars analysis
|
||||
// Shows high-performing cars you don't own
|
||||
// Cars not yet on the wishlist are prefixed with "+"
|
||||
|
|
@ -105,7 +122,7 @@ async fn main() {
|
|||
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" => {
|
||||
Commands::Wishlist => {
|
||||
// Wishlist review
|
||||
// Shows cars on the wishlist that are no longer among the best
|
||||
let my_data = load_my_cars_data();
|
||||
|
|
@ -113,7 +130,7 @@ async fn main() {
|
|||
let car_list = parse_all_times(&my_data.owned, &ctx).await;
|
||||
print_wishlist_stale(&ctx, &my_data.wishlist, &car_list);
|
||||
}
|
||||
"trash" => {
|
||||
Commands::Trash => {
|
||||
// Collection optimization
|
||||
// Identifies cars that can be safely removed
|
||||
let my_data = load_my_cars_data();
|
||||
|
|
@ -121,7 +138,7 @@ async fn main() {
|
|||
let car_list = parse_all_times(&my_data.owned, &ctx).await;
|
||||
print_trash(&ctx, &car_list, &my_data.owned);
|
||||
}
|
||||
"details" => {
|
||||
Commands::Details => {
|
||||
// Comprehensive data export
|
||||
// Outputs all timing data for external analysis
|
||||
let my_data = load_my_cars_data();
|
||||
|
|
@ -129,28 +146,12 @@ async fn main() {
|
|||
parse_all_times(&my_data.owned, &ctx).await;
|
||||
print_best_times(&ctx);
|
||||
}
|
||||
"track" => {
|
||||
Commands::Track { track, condition, rq } => {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue